Rustプログラミング言語
Steve Klabnik、Carol Nichols、Chris Krycho 著、Rust Community の 協力
この版の本文は、Rust 1.90.0(2025-09-18 リリース)以降を使用し、
すべてのプロジェクトの Cargo.toml ファイルで edition = "2024" を指定して
Rust 2024エディションのイディオムを使うよう設定していることを前提としています。
Rust のインストールまたは更新手順については第1章の「インストール」節を参照し、
エディションについては付録Eを参照してください。
HTML版はオンラインでは
https://doc.rust-lang.org/stable/book/
で利用でき、rustup でインストールされた Rust にはオフライン版も含まれています。開くには rustup doc --book を実行してください。
コミュニティによる翻訳版もいくつか公開されています。
本書は No Starch Press のペーパーバック版および電子書籍版でも入手できます。
🚨 よりインタラクティブな学習体験をお望みですか? クイズ、ハイライト、可視化などを 備えた Rust Book の別バージョンを試してみてください: https://rust-book.cs.brown.edu
序文
Rustプログラミング言語は、このわずか数年のあいだに、小さく生まれたばかりの熱心なコミュニティによって生み出され育まれるところから、世界でもっとも愛され、もっとも求められるプログラミング言語のひとつへと、はるか遠くまで歩んできました。振り返ってみれば、Rustの力と将来性が注目を集め、システムプログラミングの分野で足場を築いたのは必然でした。必然ではなかったのは、オープンソースコミュニティ全体に浸透し、業界全体での大規模な採用を促した、関心とイノベーションの世界的な高まりです。
今の時点では、この関心と採用の爆発的な広がりを説明するのに、Rustが備えている素晴らしい機能を挙げるのはたやすいことです。メモリ安全性、そして高速な性能、そして親切なコンパイラ、そして優れたツール群、そのほかにも数多くの素晴らしい機能を、誰が望まないでしょうか。今日あなたが目にしているRust言語は、システムプログラミングにおける長年の研究と、活気に満ちた情熱的なコミュニティの実践的な知恵とを結び付けたものです。この言語は明確な目的をもって設計され、細心の注意を払って作り上げられており、開発者に対して、安全で、高速で、信頼性の高いコードをより書きやすくするツールを提供します。
しかし、Rustを本当に特別なものにしているのは、ユーザーであるあなたが目標を達成できるよう力を与えることに、その根本がある点です。これは、あなたの成功を願う言語であり、その「力を与える」という原則は、この言語を構築し、保守し、広めているコミュニティの中核に流れています。この決定版の書籍の前版以来、Rustはさらに発展し、真にグローバルで信頼される言語となりました。Rust ProjectはいまやRust Foundationによって力強く支えられており、同財団はRustが安全で、安定しており、持続可能であることを確かなものにするための重要な取り組みにも投資しています。
この版のThe Rust Programming Languageは、長年にわたる言語の進化を反映し、価値ある新しい情報を提供する包括的な改訂版です。しかし、これは単なる構文やライブラリのガイドではありません。品質、性能、そして熟慮された設計を重んじるコミュニティへ参加するための招待状なのです。初めてRustを探求しようとしているベテラン開発者であっても、スキルをさらに磨きたい経験豊富なRustaceanであっても、この版はすべての人に何かしらの価値をもたらしてくれます。
Rustの歩みは、協力と学習、そして反復の歩みでした。言語とそのエコシステムの成長は、それを支える活気に満ちた多様なコミュニティをそのまま映し出しています。中核となる言語設計者から気軽に貢献する人々に至るまで、何千人もの開発者による貢献こそが、Rustをこのようにユニークで強力なツールにしているのです。この本を手に取ることは、単に新しいプログラミング言語を学ぶことではありません。ソフトウェアをより良く、より安全にし、そしてより楽しく扱えるものにしていこうとする運動に加わることなのです。
Rustコミュニティへようこそ!
- Bec Rumbul, Rust Foundation エグゼクティブディレクター
はじめに
注: 本書のこの版は、印刷版および電子書籍版として No Starch Press から入手できる The Rust Programming Language と同じ内容です。
Rust の入門書である The Rust Programming Language へようこそ。 Rust プログラミング言語は、より高速で、より信頼性の高いソフトウェアを 書くのに役立ちます。プログラミング言語の設計では、高水準の使いやすさと 低水準の制御性はしばしば相反します。Rust はその対立に挑みます。強力な 技術的能力と優れた開発者体験のバランスを取ることで、Rust は、従来その ような制御に付きものだった面倒さなしに、低水準の詳細(メモリ使用量など) を制御する選択肢を与えてくれます。
Rust は誰のためのものか
Rust は、さまざまな理由から多くの人にとって理想的です。とくに重要な グループをいくつか見てみましょう。
開発者のチーム
Rust は、システムプログラミングの知識レベルがさまざまな大規模な開発 チームが協働するうえで、生産的なツールであることを示しつつあります。 低水準のコードはさまざまな微妙なバグを生みやすく、ほかの多くの言語では、 それらは広範なテストと経験豊富な開発者による入念なコードレビューに よってしか見つけられません。Rust では、コンパイラが門番の役割を果たし、 並行性バグを含む、このような見つけにくいバグを含んだコードのコンパイルを 拒否します。コンパイラと協調して作業することで、チームはバグを追い回す のではなく、プログラムのロジックに時間を集中できます。
Rust はまた、システムプログラミングの世界に現代的な開発者向けツールを もたらします。
- 同梱の依存関係マネージャー兼ビルドツールである Cargo により、依存関係の追加、 コンパイル、管理を、Rust エコシステム全体で手間なく一貫した形で行えます。
rustfmtフォーマットツールにより、開発者間で一貫したコーディングスタイルが 保たれます。- Rust Language Server は、コード補完やインラインのエラーメッセージのための 統合開発環境(IDE)連携を支えます。
これらやそのほかの Rust エコシステムのツールを使うことで、開発者は システムレベルのコードを書きながらも高い生産性を維持できます。
学生
Rust は、学生やシステムの概念を学びたい人のための言語でもあります。Rust を使って、多くの人がオペレーティングシステム開発のようなトピックを学んで きました。コミュニティはとても歓迎的で、学生の質問にも喜んで答えて くれます。この本のような取り組みを通じて、Rust チームは、特に プログラミング初心者を含むより多くの人に、システムの概念をより身近な ものにしたいと考えています。
企業
大小何百もの企業が、コマンドラインツール、ウェブサービス、DevOps ツール、 組み込みデバイス、音声や映像の解析とトランスコーディング、暗号通貨、 バイオインフォマティクス、検索エンジン、Internet of Things アプリケーション、 機械学習、さらには Firefox ウェブブラウザの主要部分に至るまで、さまざまな 用途で本番環境に Rust を使用しています。
オープンソース開発者
Rust は、Rust プログラミング言語や、そのコミュニティ、開発者ツール、 ライブラリを作りたい人のためのものでもあります。Rust 言語への貢献も ぜひ歓迎します。
速度と安定性を重視する人
Rust は、言語に速度と安定性を強く求める人のためのものです。ここで言う 速度とは、Rust のコードがどれだけ速く実行できるかと、Rust によって どれだけ速くプログラムを書けるかの両方を意味します。Rust コンパイラの 検査は、機能追加やリファクタリングを経ても安定性を確保します。これは、 そのような検査を持たない言語にありがちな、開発者が変更を恐れがちな 脆いレガシーコードとは対照的です。ゼロコスト抽象化――手書きのコードと 同じ速さで低水準コードにコンパイルされる高水準機能――を目指すことで、 Rust は安全なコードも高速なコードであるようにしようとしています。
Rust 言語は、このほかにも多くのユーザーを支えたいと考えています。ここで 挙げたのは、主要な当事者のほんの一部にすぎません。全体として、Rust の 最大の野心は、安全性 と 生産性、速度 と 使いやすさを提供することで、 プログラマが何十年にもわたって受け入れてきたトレードオフをなくすこと です。Rust を試してみて、その選択が自分に合うかどうか確かめてみて ください。
この本は誰のためのものか
この本では、読者が何らかの別のプログラミング言語でコードを書いたことが あることを前提としていますが、それがどの言語かは問いません。さまざまな プログラミング経験を持つ人が幅広く読めるように、この内容を構成しました。 プログラミングとは何か、あるいはどう考えるべきか、といったことには あまり多くの紙幅を割いていません。もしプログラミングがまったく初めて なら、プログラミング入門に特化した本を読むほうがよいでしょう。
この本の使い方
基本的には、この本は最初から最後まで順番に読んでいくことを想定して います。後の章は前の章の概念を土台にしており、前の章では特定の トピックの詳細に立ち入らないことがあっても、後の章でそのトピックを 再び取り上げます。
この本には、概念を扱う章とプロジェクトを扱う章の 2 種類があります。 概念の章では Rust のある側面について学びます。プロジェクトの章では、 それまでに学んだことを適用しながら、小さなプログラムを一緒に作ります。 第2章、第12章、第21章がプロジェクトの章で、それ以外は概念の章です。
第1章 では、Rust のインストール方法、「Hello, world!」プログラムの 書き方、そして Rust のパッケージマネージャー兼ビルドツールである Cargo の使い方を説明します。第2章 は、数当てゲームを作りながら Rust で プログラムを書くことを学ぶ実践的な入門です。ここでは概念を高いレベルで 扱い、後の章でさらに詳しく説明します。すぐに手を動かしたいなら、 第2章がそのための章です。次に進む前に細部まできっちり学びたい学習者で あれば、第2章をいったん飛ばして、ほかのプログラミング言語の機能と似た Rust の機能を扱う 第3章 に直接進むとよいかもしれません。その後、 学んだ詳細をプロジェクトに適用したくなったら、第2章に戻ってくることが できます。
第4章 では、Rust の所有権システムについて学びます。第5章 では、
構造体とメソッドを扱います。第6章 では、列挙型、match 式、
そして if let と let...else の制御フロー構文を扱います。構造体と
列挙型を使って独自の型を作ることになります。
第7章 では、Rust のモジュールシステムと、コードおよびその公開
アプリケーションプログラミングインターフェイス(API)を整理するための
プライバシーのルールについて学びます。第8章 では、標準ライブラリが
提供する一般的なコレクションデータ構造、すなわちベクタ、文字列、
ハッシュマップを扱います。第9章 では、Rust のエラー処理の哲学と
手法を掘り下げます。
第10章では、ジェネリクス、トレイト、ライフタイムを掘り下げます。これらによって、
複数の型に適用できるコードを定義する力が得られます。第11章は
テストについて全面的に扱います。Rust の安全性保証があっても、
プログラムのロジックが正しいことを確実にするためにはテストが必要です。
第12章では、ファイル内のテキストを検索する grep コマンドラインツールの
機能の一部を、自分たちで実装します。このために、
前の章で扱った多くの概念を使います。
第13章では、クロージャとイテレータを扱います。これらは 関数型プログラミング言語に由来する Rust の機能です。第14章では、Cargo を さらに深く調べ、ライブラリを他の人と共有するための ベストプラクティスについて話します。第15章では、標準ライブラリが提供する スマートポインタと、その機能を可能にしているトレイトについて説明します。
第16章では、並行プログラミングのさまざまなモデルを見ていき、 Rust がどのようにして複数スレッドでのプログラミングを 恐れることなく行えるようにしているかを説明します。第17章では、それをさらに発展させて、 Rust の async / await 構文に加え、タスク、future、stream、 そしてそれらが実現する軽量な並行性モデルを探ります。
第18章では、Rust のイディオムが、みなさんがよく知っているかもしれない オブジェクト指向プログラミングの原則と比べてどうなのかを見ていきます。第19章は、 パターンとパターンマッチングに関するリファレンスであり、これらは Rust プログラム全体で 考えを表現するための強力な方法です。第20章には、unsafe Rust、 マクロ、そしてライフタイム、トレイト、型、関数、クロージャに関するさらなる話題など、 興味深い高度なトピックが盛りだくさんに含まれています。
第21章では、低レベルなマルチスレッド Web サーバーを実装する プロジェクトを完成させます!
最後に、いくつかの付録には、この言語に関する便利な情報が、 よりリファレンスに近い形式で収められています。付録 A では Rust のキーワードを、 付録 B では Rust の演算子と記号を、付録 C では標準ライブラリが提供する 導出可能なトレイトを、付録 D では便利な開発ツールを扱い、 付録 E では Rust の edition について説明します。付録 F では 本書の翻訳版を見つけることができ、付録 G では Rust がどのように作られているのか、 そして nightly Rust とは何かを扱います。
この本の読み方に、間違ったやり方はありません。先に進みたければ、そうしてください! 混乱したときには、前の章に戻らなければならないかもしれません。 ですが、自分に合ったやり方で進めてください。
Rust を学ぶ過程の重要な一部は、コンパイラが表示する エラーメッセージの読み方を学ぶことです。これらは、 動作するコードへとあなたを導いてくれます。そのため、 コンパイルできない例も、それぞれの状況でコンパイラが表示する エラーメッセージと一緒に数多く示します。適当な例を入力して 実行した場合、コンパイルできないことがあると理解しておいてください! 実行しようとしている例が、エラーになることを意図しているかどうかを、 前後の本文を読んで必ず確認してください。ほとんどの場合、 コンパイルできないコードについては、正しい版へと導きます。 また、Ferris は、動作することを意図していないコードを見分ける助けにもなります。
| Ferris | 意味 |
|---|---|
| このコードはコンパイルできません! | |
| このコードはパニックを起こします! | |
| このコードは期待した動作をしません。 |
ほとんどの場合、コンパイルできないコードについては、正しい版へと導きます。
ソースコード
この本の生成元となっているソースファイルは GitHub にあります。
はじめに
Rust の旅を始めましょう!学ぶことはたくさんありますが、どんな旅にも始まりがあります。この章では、次の内容を扱います。
- Linux、macOS、Windows に Rust をインストールする
Hello, world!を表示するプログラムを書く- Rust のパッケージマネージャー兼ビルドシステムである
cargoを使う
インストール
インストール
最初のステップは Rust をインストールすることです。rustup を通じて Rust をダウンロードします。rustup は、Rust のバージョンと関連ツールを管理するためのコマンドラインツールです。ダウンロードにはインターネット接続が必要です。
注: 何らかの理由で
rustupを使いたくない場合は、より多くの選択肢について その他の Rust のインストール方法のページ を参照してください。
以下の手順では、Rust コンパイラの最新の安定版をインストールします。Rust の安定性保証により、この本の中でコンパイルできるすべての例は、より新しい Rust のバージョンでも引き続きコンパイルできます。出力はバージョンによってわずかに異なる場合があります。これは、Rust がエラーメッセージや警告をしばしば改善しているためです。言い換えると、これらの手順を使ってインストールした新しい安定版の Rust であれば、この本の内容は期待どおりに動作するはずです。
コマンドライン表記
この章および本書全体を通して、ターミナルで使用するいくつかのコマンドを示します。ターミナルに入力すべき行は、すべて
$で始まります。$文字そのものを入力する必要はありません。これは各コマンドの開始を示すために表示されるコマンドラインプロンプトです。$で始まらない行は、通常、前のコマンドの出力を示します。さらに、PowerShell 固有の例では$ではなく>を使用します。
Linux または macOS への rustup のインストール
Linux または macOS を使用している場合は、ターミナルを開いて次のコマンドを入力してください。
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
このコマンドはスクリプトをダウンロードし、最新の安定版 Rust をインストールする rustup ツールのインストールを開始します。パスワードの入力を求められることがあります。インストールが成功すると、次の行が表示されます。
Rust is installed now. Great!
また、リンカー も必要です。これは Rust がコンパイル済みの出力を 1 つのファイルに結合するために使うプログラムです。おそらく、すでに 1 つは入っているはずです。リンカーエラーが出た場合は、C コンパイラをインストールしてください。通常、C コンパイラにはリンカーが含まれています。一般的な Rust パッケージの中には C のコードに依存しているものもあり、C コンパイラはそのためにも役立ちます。
macOS では、次を実行すると C コンパイラを入手できます。
$ xcode-select --install
Linux ユーザーは、通常は各ディストリビューションのドキュメントに従って GCC または Clang をインストールする必要があります。たとえば Ubuntu を使っている場合は、build-essential パッケージをインストールできます。
Windows への rustup のインストール
Windows では、https://www.rust-lang.org/tools/install にアクセスし、Rust をインストールするための手順に従ってください。インストールの途中で、Visual Studio のインストールを求められます。これにより、プログラムのコンパイルに必要なリンカーとネイティブライブラリが提供されます。この手順でさらに助けが必要な場合は、 https://rust-lang.github.io/rustup/installation/windows-msvc.html を参照してください。
この本の残りの部分では、cmd.exe と PowerShell の両方で動作するコマンドを使用します。特有の違いがある場合は、どちらを使うべきかを説明します。
トラブルシューティング
Rust が正しくインストールされているか確認するには、シェルを開いて次の行を入力してください。
$ rustc --version
次の形式で、リリースされている最新の安定版のバージョン番号、コミットハッシュ、コミット日が表示されるはずです。
rustc x.y.z (abcabcabc yyyy-mm-dd)
この情報が表示されれば、Rust は正常にインストールされています。表示されない場合は、以下のように Rust が %PATH% システム変数に含まれていることを確認してください。
Windows の CMD では、次を使います。
> echo %PATH%
PowerShell では、次を使います。
> echo $env:Path
Linux と macOS では、次を使います。
$ echo $PATH
それらがすべて正しく、それでも Rust が動作しない場合は、助けを得られる場所がいくつもあります。コミュニティページ で、ほかの Rustacean(私たちが自分たちを呼ぶ、ちょっとふざけた愛称)と連絡を取る方法を確認してください。
更新とアンインストール
rustup を使って Rust をインストールした後は、新しくリリースされたバージョンへの更新は簡単です。シェルから、次の更新スクリプトを実行してください。
$ rustup update
Rust と rustup をアンインストールするには、シェルから次のアンインストールスクリプトを実行してください。
$ rustup self uninstall
ローカルドキュメントを読む
Rust のインストールには、ドキュメントのローカルコピーも含まれているので、オフラインで読むことができます。rustup doc を実行すると、ブラウザでローカルドキュメントが開きます。
型や関数が標準ライブラリによって提供されていて、それが何をするのか、どう使うのかがわからないときはいつでも、アプリケーションプログラミングインターフェース(API)ドキュメントを使って調べてください!
テキストエディタと IDE の使用
この本では、Rust コードを書くためにどのツールを使うかについて何も前提としていません。ほとんどどんなテキストエディタでも十分に作業できます。ただし、多くのテキストエディタや統合開発環境(IDE)には Rust の組み込みサポートがあります。Rust の Web サイトにある ツールページ では、多くのエディタと IDE の比較的新しい一覧をいつでも確認できます。
この本をオフラインで使う
いくつかの例では、標準ライブラリ以外の Rust パッケージを使用します。そうした例を進めるには、インターネット接続が必要になるか、あるいはそれらの依存関係を事前にダウンロードしておく必要があります。依存関係を事前にダウンロードするには、次のコマンドを実行できます。(cargo が何であるか、またこれらの各コマンドが何をするかについては、後で詳しく説明します。)
$ cargo new get-dependencies
$ cd get-dependencies
$ cargo add rand@0.8.5 trpl@0.2.0
これにより、これらのパッケージのダウンロードがキャッシュされるので、後でダウンロードする必要がなくなります。この一連のコマンドを実行したら、get-dependencies フォルダーを残しておく必要はありません。この一連のコマンドを実行していれば、この本の残りの部分にあるすべての cargo コマンドで --offline フラグを使い、ネットワークを使おうとする代わりに、これらのキャッシュされたバージョンを使用できます。
Hello, World!
Hello, World!
Rust をインストールしたので、最初の Rust プログラムを書いてみましょう。
新しい言語を学ぶときには、Hello, world! というテキストを画面に表示する小さなプログラムを書くのが慣例なので、ここでも同じことを行います!
注: この本では、コマンドラインの基本的な知識があることを前提としています。Rust は、どのエディタやツールを使うか、コードをどこに置くかについて特に要件を課しません。そのため、コマンドラインではなく IDE を使いたい場合は、使い慣れた IDE を自由に使ってください。現在では多くの IDE がある程度 Rust をサポートしています。詳しくは IDE のドキュメントを確認してください。Rust チームは
rust-analyzerを通じた優れた IDE サポートの実現に注力してきました。詳しくは 付録 D を参照してください。
プロジェクトディレクトリのセットアップ
まず、Rust のコードを保存するためのディレクトリを作成します。Rust にとってコードがどこに置かれているかは重要ではありませんが、この本の練習問題やプロジェクトでは、ホームディレクトリに projects ディレクトリを作成し、すべてのプロジェクトをそこに置くことをお勧めします。
ターミナルを開き、次のコマンドを入力して projects ディレクトリを作成し、その projects ディレクトリの中に「Hello, world!」プロジェクト用のディレクトリを作成してください。
Linux、macOS、および Windows の PowerShell では、次のように入力します:
$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world
Windows の CMD では、次のように入力します:
> mkdir "%USERPROFILE%\projects"
> cd /d "%USERPROFILE%\projects"
> mkdir hello_world
> cd hello_world
Rustプログラムの基本
次に、新しいソースファイルを作成し、main.rs という名前にします。Rust のファイルは常に .rs 拡張子で終わります。ファイル名に複数の単語を使う場合は、アンダースコアで区切るのが慣例です。たとえば、helloworld.rs ではなく hello_world.rs を使います。
では、今作成した main.rs ファイルを開き、リスト 1-1 のコードを入力してください。
fn main() {
println!("Hello, world!");
}
ファイルを保存し、~/projects/hello_world ディレクトリにあるターミナルウィンドウに戻ってください。Linux または macOS では、次のコマンドを入力してファイルをコンパイルし、実行します:
$ rustc main.rs
$ ./main
Hello, world!
Windows では、./main の代わりに .\main を入力します:
> rustc main.rs
> .\main
Hello, world!
オペレーティングシステムに関係なく、文字列 Hello, world! がターミナルに表示されるはずです。この出力が表示されない場合は、インストールのセクションにある 「トラブルシューティング」 を参照して、助けを得る方法を確認してください。
もし Hello, world! が表示されたなら、おめでとうございます! これで正式に Rust プログラムを書いたことになります。つまり、あなたは Rust プログラマーです――ようこそ!
Rustプログラムの構造
この「Hello, world!」プログラムを詳しく見ていきましょう。まずは最初の要素です:
fn main() {
}
これらの行は、main という名前の関数を定義しています。main 関数は特別です。すべての実行可能な Rust プログラムにおいて、常に最初に実行されるコードだからです。ここでは、最初の行で、引数を取らず何も返さない main という名前の関数を宣言しています。引数がある場合は、丸かっこ (()) の中に入ります。
関数本体は {} で囲まれます。Rust では、すべての関数本体を波かっこで囲む必要があります。開始の波かっこは、関数宣言と同じ行に置き、その前に 1 つ空白を入れるのが良いスタイルです。
注: Rust プロジェクト全体で標準的なスタイルに従いたい場合は、
rustfmtという自動フォーマッタツールを使ってコードを特定のスタイルに整形できます(rustfmtについては 付録 D で詳しく説明します)。Rust チームは、rustcと同様に、このツールも標準の Rust 配布物に含めているので、すでにコンピュータにインストールされているはずです!
main 関数の本体には、次のコードが入ります:
#![allow(unused)]
fn main() {
println!("Hello, world!");
}
この行が、この小さなプログラムの仕事をすべて行います。つまり、テキストを画面に表示します。ここでは、注目すべき重要な点が 3 つあります。
まず、println! は Rust のマクロを呼び出しています。もし関数を呼び出しているのであれば、println(! なし)と書きます。Rust のマクロは、Rust の構文を拡張するためにコードを生成するコードを書く手段であり、これについては 第20章 でさらに詳しく説明します。今のところは、! を使っているということは通常の関数ではなくマクロを呼び出していること、そしてマクロは必ずしも関数と同じ規則に従うわけではないことだけ知っておけば十分です。
2 つ目に、"Hello, world!" という文字列があります。この文字列を println! の引数として渡すと、その文字列が画面に表示されます。
3 つ目に、行の末尾にセミコロン (;) を付けています。これは、この式が終わり、次の式を始める準備ができていることを示します。Rust のコードのほとんどの行はセミコロンで終わります。
コンパイルと実行
新しく作成したプログラムを実行したばかりなので、この過程の各ステップを見ていきましょう。
Rust プログラムを実行する前に、Rust コンパイラを使ってコンパイルする必要があります。そのためには、次のように rustc コマンドを入力し、ソースファイル名を渡します:
$ rustc main.rs
C や C++ の経験があるなら、これは gcc や clang に似ていることに気付くでしょう。コンパイルが成功すると、Rust はバイナリ実行ファイルを出力します。
Linux、macOS、および Windows の PowerShell では、シェルで ls コマンドを入力すると実行ファイルを確認できます:
$ ls
main main.rs
Linux と macOS では、2 つのファイルが表示されます。Windows の PowerShell では、CMD を使った場合と同じ 3 つのファイルが表示されます。Windows の CMD では、次のように入力します:
> dir /B %= /B オプションはファイル名のみを表示することを意味します =%
main.exe
main.pdb
main.rs
ここには、.rs 拡張子のソースコードファイル、実行ファイル(Windows では main.exe、それ以外のすべてのプラットフォームでは main)、そして Windows を使用している場合は .pdb 拡張子のデバッグ情報を含むファイルが表示されています。ここから、次のように main または main.exe ファイルを実行します:
$ ./main # または Windows では .\main
main.rs が「Hello, world!」プログラムであれば、この行により Hello, world! がターミナルに表示されます。
Ruby、Python、JavaScript のような動的言語に慣れている場合、プログラムをコンパイルすることと実行することを別々の手順として扱うことに慣れていないかもしれません。Rust は 事前コンパイル型 言語です。つまり、プログラムをコンパイルして実行可能ファイルを誰かに渡せば、その人は Rust をインストールしていなくても実行できます。誰かに .rb、.py、または .js ファイルを渡した場合、その人はそれぞれ Ruby、Python、または JavaScript の実装をインストールしている必要があります。しかし、それらの言語では、プログラムをコンパイルして実行するのに必要なコマンドは 1 つだけです。言語設計では、あらゆることがトレードオフです。
単純なプログラムであれば rustc でコンパイルするだけでも十分ですが、プロジェクトが大きくなるにつれて、すべてのオプションを管理し、コードを共有しやすくしたくなるでしょう。次は、現実の Rust プログラムを書くのに役立つ Cargo ツールを紹介します。
Hello, Cargo!
こんにちは、Cargo!
Cargo は Rust のビルドシステムであり、パッケージマネージャーです。ほとんどの Rust 開発者はこのツール を使って Rust プロジェクトを管理しています。これは、Cargo がコードのビルド、 コードが依存するライブラリのダウンロード、それらのライブラリのビルドなど、 多くの作業を代わりに処理してくれるからです。(コードが必要とするライブラリを 依存関係 と呼びます。)
これまでに書いてきたもののような単純な Rust プログラムには、依存関係が ありません。「Hello, world!」プロジェクトを Cargo でビルドしていたとしても、 使われるのはコードのビルドを担当する Cargo の部分だけです。より複雑な Rust プログラムを 書くようになると依存関係を追加することになりますが、Cargo を使ってプロジェクトを 始めていれば、依存関係の追加はずっと簡単になります。
Rust プロジェクトの大半は Cargo を使っているため、本書の残りの部分でも あなたも Cargo を使っていることを前提に進めます。Cargo は、 「インストール」 節で説明した公式インストーラーを 使って Rust をインストールしたなら一緒にインストールされています。別の方法で Rust を インストールした場合は、次のコマンドをターミナルで入力して、Cargo がインストール されているか確認してください:
$ cargo --version
バージョン番号が表示されれば、Cargo はインストールされています! command not found のようなエラーが表示された場合は、使ったインストール方法のドキュメントを
確認して、Cargo を個別にインストールする方法を調べてください。
Cargo でプロジェクトを作成する
Cargo を使って新しいプロジェクトを作成し、それが元の「Hello, world!」プロジェクトと どう違うのかを見てみましょう。projects ディレクトリ(あるいは、コードを保存する場所として 選んだディレクトリ)に戻ってください。そのうえで、どのオペレーティングシステムでも、 次を実行します:
$ cargo new hello_cargo
$ cd hello_cargo
最初のコマンドは、hello_cargo という名前の新しいディレクトリとプロジェクトを作成します。 ここではプロジェクト名を hello_cargo にしたので、Cargo はそのファイルを 同じ名前のディレクトリに作成します。
hello_cargo ディレクトリに移動して、ファイルを一覧表示してください。Cargo が 2 つのファイルと 1 つのディレクトリを生成してくれていることがわかります。つまり、 Cargo.toml ファイルと、その中に main.rs ファイルを含む src ディレクトリです。
さらに、.gitignore ファイルとともに新しい Git リポジトリも初期化されています。
既存の Git リポジトリ内で cargo new を実行した場合、Git 関連のファイルは生成されません。
この挙動は cargo new --vcs=git を使うことで上書きできます。
注: Git は一般的なバージョン管理システムです。
--vcsフラグを使うと、cargo newを 別のバージョン管理システムを使うようにしたり、バージョン管理を使わないようにしたり できます。利用可能なオプションを見るにはcargo new --helpを実行してください。
お好みのテキストエディターで Cargo.toml を開いてください。リスト 1-2 の コードのようになっているはずです。
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024"
[dependencies]
このファイルは、Cargo の設定フォーマットである TOML (Tom’s Obvious, Minimal Language)形式です。
最初の行 [package] はセクション見出しで、続く記述がパッケージを設定するもので
あることを示しています。このファイルにさらに情報を追加していくと、ほかのセクションも追加していきます。
次の 3 行では、Cargo がプログラムをコンパイルするために必要な設定情報、
つまり名前、バージョン、そして使用する Rust のエディションを設定しています。
edition キーについては 付録 E で説明します。
最後の行 [dependencies] は、プロジェクトの依存関係を列挙するためのセクションの
始まりです。Rust では、コードのパッケージを クレート と呼びます。このプロジェクトでは
ほかのクレートは必要ありませんが、第 2 章の最初のプロジェクトでは必要になるので、
そのときにこの dependencies セクションを使います。
では、src/main.rs を開いて見てみましょう:
ファイル名: src/main.rs
fn main() {
println!("Hello, world!");
}
Cargo は、リスト 1-1 で書いたものと同じ「Hello, world!」プログラムを生成してくれています! ここまでのところ、私たちのプロジェクトと Cargo が生成したプロジェクトの違いは、 Cargo がコードを src ディレクトリに置き、 トップディレクトリに Cargo.toml 設定ファイルがあることです。
Cargo は、ソースファイルが src ディレクトリの中に置かれていることを前提としています。トップレベルの プロジェクトディレクトリは、README ファイル、ライセンス情報、 設定ファイル、そのほかコードに関係のないもののためだけにあります。Cargo を使うと、 プロジェクトを整理しやすくなります。すべてのものに置き場所があり、すべてがその場所に収まります。
「Hello, world!」プロジェクトで行ったように、Cargo を使わないプロジェクトから始めた場合でも、
Cargo を使うプロジェクトに変換できます。プロジェクトのコードを src ディレクトリに移し、適切な Cargo.toml
ファイルを作成してください。その Cargo.toml ファイルを用意する簡単な方法のひとつは
cargo init を実行することで、これにより自動的に作成されます。
Cargo プロジェクトのビルドと実行
では、「Hello, world!」プログラムを Cargo でビルドして実行すると何が違うのかを 見てみましょう! hello_cargo ディレクトリで、次のコマンドを入力して プロジェクトをビルドしてください:
$ cargo build
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs
このコマンドは、現在のディレクトリではなく target/debug/hello_cargo(Windows では target\debug\hello_cargo.exe)に実行可能ファイルを作成します。デフォルトのビルドは デバッグビルドなので、Cargo はバイナリを debug という名前のディレクトリに 配置します。次のコマンドでその実行可能ファイルを実行できます:
$ ./target/debug/hello_cargo # または Windows では .\target\debug\hello_cargo.exe
Hello, world!
すべてがうまくいけば、Hello, world! がターミナルに表示されるはずです。最初に cargo build を実行すると、Cargo はトップレベルに Cargo.lock という新しいファイルも
作成します。このファイルは、プロジェクトの依存関係の正確なバージョンを記録します。
このプロジェクトには依存関係がないので、ファイルの内容は少し簡素です。このファイルを
手動で変更する必要はありません。Cargo がその内容を管理してくれます。
私たちは今、cargo build でプロジェクトをビルドし、
./target/debug/hello_cargo で実行しましたが、cargo run を使えばコードを
コンパイルして、生成された実行可能ファイルを 1 つのコマンドですぐに実行することもできます:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/hello_cargo`
Hello, world!
cargo run を使うほうが、cargo build を実行してからバイナリへの完全なパスを
使うことを覚えておくよりも便利なので、ほとんどの開発者は cargo run を使います。
今回は、Cargo が hello_cargo をコンパイルしていることを示す出力が表示されなかったことに
注目してください。Cargo はファイルが変更されていないことを判断したので、再ビルドせずに
バイナリを実行しただけです。ソースコードを変更していたなら、Cargo は実行前にプロジェクトを
再ビルドし、次のような出力が表示されていたでしょう:
```console
$ cargo run
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs
Running `target/debug/hello_cargo`
Hello, world!
Cargo には cargo check というコマンドも用意されています。このコマンドは、
実行可能ファイルを生成せずに、コードがコンパイルできることをすばやく確認します。
$ cargo check
Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
なぜ実行可能ファイルが不要なのでしょうか。多くの場合、cargo check は
cargo build よりもはるかに高速です。これは、実行可能ファイルを生成する
手順を省くためです。コードを書きながら継続的に作業内容を確認しているなら、
cargo check を使うことで、プロジェクトが引き続きコンパイルできるかどうかを
把握するまでの時間を短縮できます。したがって、多くの Rustacean はプログラムを
書いている間、コンパイルできることを確認するために定期的に cargo check を
実行します。そして、実行可能ファイルを使う準備ができたら cargo build を
実行します。
ここまでに Cargo について学んだことを振り返ってみましょう。
cargo newを使ってプロジェクトを作成できる。cargo buildを使ってプロジェクトをビルドできる。cargo runを使って、プロジェクトのビルドと実行を 1 ステップで行える。cargo checkを使って、バイナリを生成せずにエラーを確認するためのビルドができる。- Cargo は、ビルド結果をコードと同じディレクトリに保存するのではなく、 target/debug ディレクトリに保存する。
Cargo を使うもう 1 つの利点は、どのオペレーティングシステムで作業していても コマンドが同じであることです。したがって、この時点からは、Linux や macOS と Windows を分けた個別の説明はもう行いません。
リリース用にビルドする
プロジェクトがついにリリースの準備ができたら、cargo build --release を使って最適化付きでコンパイルできます。このコマンドは、
実行可能ファイルを target/debug ではなく target/release に作成します。
最適化によって Rust のコードはより高速に実行されますが、それを有効にすると
プログラムのコンパイルにかかる時間は長くなります。そのため、2 つの異なる
プロファイルがあります。1 つは、すばやく何度も再ビルドしたい開発用のもの、
もう 1 つは、繰り返し再ビルドされることはなく、できるだけ高速に動作してほしい、
ユーザーに渡す最終的なプログラムをビルドするためのものです。コードの実行時間を
ベンチマークする場合は、必ず cargo build --release を実行し、
target/release にある実行可能ファイルでベンチマークしてください。
Cargo の規約を活用する
単純なプロジェクトでは、Cargo は単に rustc を使う場合と比べてそれほど大きな
価値を提供しませんが、プログラムがより複雑になるにつれて、その真価を発揮します。
プログラムが複数のファイルにまたがったり依存関係を必要としたりするようになると、
ビルドの調整を Cargo に任せるほうがはるかに簡単です。
hello_cargo プロジェクトは単純ですが、それでも今後の Rust キャリアを通じて
使うことになる本格的なツール群の多くをすでに利用しています。実際、既存の
プロジェクトで作業するには、Git を使ってコードをチェックアウトし、その
プロジェクトのディレクトリに移動してビルドするために、次のコマンドを使えます。
$ git clone example.org/someproject
$ cd someproject
$ cargo build
Cargo の詳細については、ドキュメントを参照してください。
まとめ
Rust の学習は、すでにとても良いスタートを切っています! この章では、次のことを 学びました。
rustupを使って Rust の最新の安定版をインストールする。- より新しい Rust バージョンに更新する。
- ローカルにインストールされたドキュメントを開く。
rustcを直接使って “Hello, world!” プログラムを書いて実行する。- Cargo の規約に従って新しいプロジェクトを作成し、実行する。
ここは、Rust のコードを読んだり書いたりすることに慣れるため、もう少し本格的な プログラムを作ってみる絶好のタイミングです。そこで、第 2 章では数当てゲームの プログラムを作成します。Rust における一般的なプログラミングの概念がどのように 機能するかを先に学びたい場合は、第 3 章を見てから第 2 章に戻ってください。
数当てゲームをプログラミングする
実践的なプロジェクトに一緒に取り組みながら、Rust に飛び込んでいきましょう!この
章では、実際のプログラムの中でそれらをどのように使うかを示しながら、Rust の一般的な
概念をいくつか紹介します。let、match、メソッド、関連
関数、外部クレートなどについて学びます!以降の章では、これらの考え方をさらに詳しく
見ていきます。この章では、まず基礎を
練習するだけです。
初心者向けプログラミングの定番問題である数当てゲームを実装します。仕組みは こうです。プログラムは 1 から 100 までのランダムな整数を生成します。次に、 プレイヤーに予想を入力するよう促します。予想が入力されると、その予想が 小さすぎるか大きすぎるかをプログラムが示します。予想が 正しければ、ゲームは祝福のメッセージを表示して終了します。
新しいプロジェクトをセットアップする
新しいプロジェクトをセットアップするには、1 章で作成した projects ディレクトリに移動し、次のように Cargo を使って新しいプロジェクトを作成します。
$ cargo new guessing_game
$ cd guessing_game
最初のコマンド cargo new は、プロジェクト名 (guessing_game)
を最初の引数として受け取ります。2 つ目のコマンドは、新しいプロジェクトの
ディレクトリに移動します。
生成された Cargo.toml ファイルを見てみましょう。
ファイル名: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
[dependencies]
1 章で見たとおり、cargo new は “Hello, world!” プログラムを
生成してくれます。src/main.rs ファイルを見てみましょう。
ファイル名: src/main.rs
fn main() {
println!("Hello, world!");
}
では、この “Hello, world!” プログラムをコンパイルし、
cargo run コマンドを使って一度に実行してみましょう。
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/guessing_game`
Hello, world!
このゲームでもそうするように、プロジェクトを素早く反復し、
次の反復に進む前に毎回すばやくテストしたいときには、run コマンドが役立ちます。
src/main.rs ファイルをもう一度開いてください。このファイルに これから書くコードをすべて記述します。
予想を処理する
数当てゲームプログラムの最初の部分では、ユーザー入力を受け取り、その 入力を処理し、期待した形式になっているかを確認します。まずは、 プレイヤーが予想を入力できるようにします。リスト 2-1 のコードを src/main.rs に入力してください。
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
このコードには多くの情報が含まれているので、1 行ずつ見ていきましょう。ユーザー
入力を取得し、その結果を出力として表示するには、入出力ライブラリ io
をスコープに導入する必要があります。io ライブラリは、std として知られる
標準ライブラリに含まれています。
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
Rust ではデフォルトで、標準ライブラリで定義された項目の集合が あらゆるプログラムのスコープに導入されます。この集合は prelude と呼ばれ、 その内容は標準ライブラリのドキュメントで確認できます。
使いたい型が prelude に含まれていない場合は、その型を
use 文で明示的にスコープに導入する必要があります。std::io ライブラリ
を使うと、ユーザー入力を受け付ける機能を含む、いくつもの便利な機能を
利用できます。
1 章で見たとおり、main 関数はプログラムの
エントリポイントです。
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
fn 構文は新しい関数を宣言し、丸かっこ () は
引数がないことを示し、波かっこ { は関数本体の始まりを示します。
また 1 章で学んだように、println! は文字列を
画面に表示するマクロです。
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
このコードは、どのようなゲームなのかを説明し、ユーザーに入力 を求めるプロンプトを表示しています。
変数で値を保存する
次に、ユーザー入力を保存するための 変数 を次のように作成します。
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
ここからプログラムが面白くなってきます!この短い 1 行には多くのことが
詰まっています。変数を作成するには let 文を使います。別の例を見てみましょう。
let apples = 5;
この行は、apples という名前の新しい変数を作成し、それを値 5
に束縛します。Rust では、変数はデフォルトで不変です。つまり、いったん変数
に値を与えると、その値は変わりません。この概念については、
3 章の「変数と可変性」
節で詳しく説明します。変数を可変にするには、変数名の前に
mut を付けます。
let apples = 5; // 不変
let mut bananas = 5; // 可変
注:
//構文は、行末まで続くコメントを開始します。 Rust はコメント内のものをすべて無視します。コメントについては、 3 章でさらに詳しく説明します。
数当てゲームのプログラムに戻ると、let mut guess が
guess という名前の可変変数を導入することがわかりました。等号 (=)
は、その時点で何かをその変数に束縛したいことを Rust に伝えます。等号の右側に
あるのは guess に束縛される値であり、それは
String の新しいインスタンスを返す関数 String::new を呼び出した
結果です。String は標準
ライブラリが提供する文字列型で、拡張可能な UTF-8 エンコードのテキストです。
::new の行にある :: 構文は、new が String 型の関連
関数であることを示しています。関連関数 とは、型に対して
実装された関数のことで、この場合は String です。この new 関数は新しい
空の文字列を作成します。何らかの新しい値を作る関数の名前として new は
一般的なので、多くの型に new 関数があるのを目にするでしょう。
つまり、let mut guess = String::new(); という行は、現在
String の新しい空インスタンスに束縛されている可変変数を
作成したのです。ふう!
ユーザー入力を受け取る
プログラムの最初の行で、use std::io; を使って標準
ライブラリの入出力機能を取り込んだことを思い出してください。次に、
io モジュールの stdin 関数を呼び出し、ユーザー
入力を扱えるようにします。
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
プログラムの先頭で use std::io; によって io モジュールをインポートして
いなかったとしても、この関数呼び出しを std::io::stdin と書くことで、この関数を
使うことはできます。stdin 関数は
std::io::Stdin のインスタンスを返します。これは、
端末の標準入力へのハンドルを表す型です。
次に、.read_line(&mut guess) という行は、ユーザーから入力を受け取るために、
標準入力ハンドルに対して read_line メソッドを呼び出しています。また、read_line に
&mut guess を引数として渡して、ユーザー入力をどの文字列に格納するかを指定して
います。read_line の完全な役割は、ユーザーが標準入力に入力した内容をすべて文字列に
追記することです(内容を上書きはしません)。そのため、その文字列を引数として
渡します。このメソッドが文字列の内容を変更できるようにするため、文字列引数は
可変である必要があります。
& は、この引数が 参照 であることを示します。参照を使うと、データを何度も
メモリにコピーしなくても、コードの複数の部分から同じデータにアクセスできます。
参照は複雑な機能ですが、参照を安全かつ簡単に使えることは Rust の大きな利点の
ひとつです。このプログラムを完成させるために、その詳細をたくさん知っている必要は
ありません。今のところ知っておくべきなのは、変数と同じく、参照もデフォルトでは
不変だということだけです。したがって、可変にするには &guess ではなく
&mut guess と書く必要があります。(第4章で参照についてさらに詳しく説明
します。)
Result を使った起こりうる失敗の処理
まだこのコード行について見ています。ここではテキスト上では3行目を説明していますが、 これは論理的には依然として1行のコードの一部であることに注意してください。次の 部分はこのメソッドです。
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
このコードは次のように書くこともできました。
io::stdin().read_line(&mut guess).expect("Failed to read line");
しかし、1つの長い行は読みにくいため、分割したほうがよいです。.method_name()
構文でメソッドを呼び出すときは、改行やその他の空白を入れて長い行を分割すると、
読みやすくなることがよくあります。では、この行が何をしているのかを説明しましょう。
前に述べたように、read_line はユーザーが入力した内容を、渡された文字列に
入れますが、同時に Result 値も返します。Result は、しばしば enum と呼ばれる 列挙型 で、
複数の取りうる状態のいずれかになりうる型です。可能な各状態を バリアント と
呼びます。
第6章 では enum をより詳しく扱います。これらの
Result 型の目的は、エラー処理に関する情報をエンコードすることです。
Result のバリアントは Ok と Err です。Ok バリアントは操作が成功した
ことを示し、正常に生成された値を含みます。Err バリアントは操作が失敗したことを
意味し、その操作がどのように、あるいはなぜ失敗したのかについての情報を含みます。
Result 型の値にも、他のあらゆる型の値と同様に、それに対して定義されたメソッドが
あります。Result のインスタンスには、呼び出すことのできる
expect メソッド があります。この Result の
インスタンスが Err 値なら、expect はプログラムをクラッシュさせ、
expect に引数として渡したメッセージを表示します。read_line メソッドが
Err を返した場合、それはおそらく基盤となるオペレーティングシステムから来た
エラーの結果です。この Result のインスタンスが Ok 値なら、expect は
Ok が保持している戻り値を取り出して、その値だけを返してくれるので、それを使う
ことができます。この場合、その値はユーザー入力のバイト数です。
expect を呼び出さないと、プログラムはコンパイルできますが、警告が出ます。
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
10 | let _ = io::stdin().read_line(&mut guess);
| +++++++
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s
Rust は、read_line から返された Result 値を使っていないことを警告しており、
プログラムが起こりうるエラーを処理していないことを示しています。
警告を抑制する正しい方法は、実際にエラー処理のコードを書くことです。しかし今回の
場合は、問題が発生したときにこのプログラムをクラッシュさせたいだけなので、
expect を使えます。エラーから回復する方法については、第
9章 で学びます。
println! のプレースホルダーで値を表示する
閉じ中かっこを除けば、これまでのコードで説明する行はあと1行だけです。
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
この行は、今やユーザー入力を含んでいる文字列を表示します。{} という
中かっこの組はプレースホルダーです。{} を、値をその場所に保持しておく小さな
カニのはさみだと考えてください。変数の値を表示するときは、その変数名を中かっこの
中に入れられます。式を評価した結果を表示するときは、フォーマット文字列の中に空の
中かっこを置き、その後に、各空の中かっこプレースホルダーに表示する式を、同じ順序で
コンマ区切りでフォーマット文字列の後ろに並べます。1回の println! 呼び出しで
変数と式の評価結果を表示する例は、次のようになります。
#![allow(unused)]
fn main() {
let x = 5;
let y = 10;
println!("x = {x} and y + 2 = {}", y + 2);
}
このコードは x = 5 and y + 2 = 12 と表示します。
最初の部分をテストする
数当てゲームの最初の部分をテストしてみましょう。cargo run を使って実行します。
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
この時点で、ゲームの最初の部分は完成です。キーボードから入力を受け取り、それを 表示できています。
秘密の数字を生成する
次に、ユーザーが当てようとする秘密の数字を生成する必要があります。ゲームを何度でも
楽しく遊べるように、秘密の数字は毎回異なるべきです。ゲームが難しすぎないように、
1 から 100 までの乱数を使います。Rust の標準ライブラリには、まだ乱数機能が
含まれていません。しかし、Rust チームはその機能を備えた rand
crate を提供しています。
クレートで機能を増やす
クレートは Rust のソースコードファイルの集まりだということを思い出してください。
これまで作ってきたプロジェクトは、実行可能ファイルであるバイナリクレートです。
rand crate はライブラリクレートで、他のプログラムで使うことを意図したコードを
含んでおり、単独では実行できません。
外部クレートの調整こそ、Cargo が真価を発揮するところです。rand を使うコードを書く前に、Cargo.toml ファイルを変更して、rand クレートを依存関係として含める必要があります。今すぐそのファイルを開き、Cargo が作成した [dependencies] セクションヘッダーの下、末尾に次の行を追加してください。ここで示すものとまったく同じように、このバージョン番号付きで rand を指定してください。そうしないと、このチュートリアルのコード例が動作しない可能性があります。
ファイル名: Cargo.toml
[dependencies]
rand = "0.8.5"
Cargo.toml ファイルでは、ヘッダーの後に続くものはすべてそのセクションの一部であり、別のセクションが始まるまでそのセクションが続きます。[dependencies] では、プロジェクトが依存する外部クレートと、それらのクレートに必要なバージョンを Cargo に伝えます。この場合、rand クレートをセマンティックバージョン指定子 0.8.5 で指定します。Cargo は セマンティックバージョニング(しばしば SemVer と呼ばれます)を理解します。これはバージョン番号の記述方法に関する標準です。指定子 0.8.5 は実際には ^0.8.5 の省略形で、これは 0.8.5 以上 0.9.0 未満の任意のバージョンを意味します。
Cargo は、これらのバージョンは 0.8.5 と互換性のある公開 API を持つと見なしており、この指定により、この章のコードで引き続きコンパイルできる最新のパッチリリースを取得できます。0.9.0 以上のバージョンは、以降の例で使用するものと同じ API を持つことが保証されません。
では、コードを何も変更せずに、リスト 2-2 に示すようにプロジェクトをビルドしてみましょう。
$ cargo build
Updating crates.io index
Locking 15 packages to latest Rust 1.85.0 compatible versions
Adding rand v0.8.5 (available: v0.9.0)
Compiling proc-macro2 v1.0.93
Compiling unicode-ident v1.0.17
Compiling libc v0.2.170
Compiling cfg-if v1.0.0
Compiling byteorder v1.5.0
Compiling getrandom v0.2.15
Compiling rand_core v0.6.4
Compiling quote v1.0.38
Compiling syn v2.0.98
Compiling zerocopy-derive v0.7.35
Compiling zerocopy v0.7.35
Compiling ppv-lite86 v0.2.20
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.48s
表示されるバージョン番号は異なるかもしれません(ただし、SemVer のおかげで、どれもコードと互換性があります!)。また、行の内容も異なる場合があり(オペレーティングシステムによります)、行の順序も異なることがあります。
外部の依存関係を含めると、Cargo はその依存関係が必要とするすべての最新バージョンを レジストリ から取得します。これは Crates.io のデータのコピーです。Crates.io は、Rust エコシステムの人々が、他の人が使えるようにオープンソースの Rust プロジェクトを公開する場所です。
レジストリを更新した後、Cargo は [dependencies] セクションを確認し、まだダウンロードしていない、そこに列挙されたクレートをダウンロードします。この場合、依存関係として列挙したのは rand だけですが、Cargo は rand が動作するために依存している他のクレートも取得しました。クレートをダウンロードした後、Rust はそれらをコンパイルし、そのうえで依存関係を利用できる状態でプロジェクトをコンパイルします。
変更を何も加えずにすぐもう一度 cargo build を実行すると、Finished の行以外の出力はありません。Cargo は依存関係をすでにダウンロードしてコンパイル済みであり、Cargo.toml ファイル内でもそれらについて何も変更していないことを認識しています。また、コードについても何も変更していないことを認識しているので、それも再コンパイルしません。やることが何もないので、単に終了します。
src/main.rs ファイルを開いてちょっとした変更を加え、保存してから再びビルドすると、出力は 2 行だけになります。
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
これらの行は、Cargo が src/main.rs ファイルに加えたごく小さな変更だけを反映してビルドを更新していることを示しています。依存関係は変わっていないため、Cargo はそれらについて既にダウンロードしてコンパイルしたものを再利用できると判断します。
再現可能なビルドを確保する
Cargo には、自分や他の誰かがコードをビルドするたびに同じアーティファクトを再ビルドできるようにする仕組みがあります。つまり、別途指示しない限り、Cargo は指定した依存関係のバージョンだけを使用します。たとえば、来週 rand クレートのバージョン 0.8.6 が公開され、そのバージョンには重要なバグ修正が含まれている一方で、コードを壊してしまうリグレッションも含まれていたとします。これに対処するため、Rust は cargo build を初めて実行したときに Cargo.lock ファイルを作成するので、これで guessing_game ディレクトリにこのファイルができています。
初めてプロジェクトをビルドするとき、Cargo は条件に合う依存関係のバージョンをすべて解決し、それらを Cargo.lock ファイルに書き込みます。将来そのプロジェクトをビルドするとき、Cargo は Cargo.lock ファイルが存在することを確認し、再びバージョンを解決する作業をすべて行う代わりに、そこに指定されたバージョンを使用します。これにより、自動的に再現可能なビルドを行えるようになります。言い換えると、Cargo.lock ファイルのおかげで、明示的にアップグレードするまでは、プロジェクトは 0.8.5 のままになります。Cargo.lock ファイルは再現可能なビルドにとって重要なので、プロジェクト内の他のコードと一緒にソース管理へチェックインされることがよくあります。
新しいバージョンを取得するためにクレートを更新する
実際にクレートを更新したい場合、Cargo には update コマンドがあり、これは Cargo.lock ファイルを無視して、Cargo.toml の指定に合う最新バージョンをすべて解決します。その後、Cargo はそれらのバージョンを Cargo.lock ファイルに書き込みます。それ以外では、デフォルトで、Cargo は 0.8.5 より大きく 0.9.0 より小さいバージョンだけを探します。rand クレートが 0.8.6 と 0.999.0 という 2 つの新しいバージョンをリリースしていた場合、cargo update を実行すると次のように表示されます。
$ cargo update
Updating crates.io index
Locking 1 package to latest Rust 1.85.0 compatible version
Updating rand v0.8.5 -> v0.8.6 (available: v0.999.0)
Cargo は 0.999.0 リリースを無視します。この時点で、現在使用している rand クレートの
バージョンが 0.8.6 であることを示す変更が Cargo.lock ファイルにも見られる
はずです。rand のバージョン 0.999.0、または 0.999.x 系列の任意の
バージョンを使うには、代わりに Cargo.toml ファイルを次のように更新する
必要があります(以下の例では rand 0.8 を使っている前提なので、実際にはこの変更を
行わないでください):
[dependencies]
rand = "0.999.0"
次に cargo build を実行すると、Cargo は利用可能なクレートのレジストリを
更新し、指定した新しいバージョンに従って rand の要件を再評価します。
Cargo と そのエコシステム については、 第14章でさらに詳しく説明しますが、今のところ知っておく必要があるのはこれだけです。 Cargo によってライブラリの再利用がとても簡単になるため、Rustacean は 複数のパッケージを組み合わせて構成される、より小さなプロジェクトを 書けます。
乱数を生成する
rand を使って、当てる対象となる数を生成してみましょう。次の手順は、
リスト 2-3 に示すように src/main.rs を更新することです。
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
まず、use rand::Rng; という行を追加します。Rng トレイトは、乱数
ジェネレーターが実装するメソッドを定義しており、それらのメソッドを使うには
このトレイトがスコープ内になければなりません。トレイトについては第10章で
詳しく扱います。
次に、途中に 2 行追加します。1 行目では、使用する特定の乱数
ジェネレーターを返す rand::thread_rng 関数を呼び出します。これは、現在
実行中のスレッドにローカルであり、オペレーティングシステムによってシードされる
ものです。次に、乱数ジェネレーターに対して gen_range
メソッドを呼び出します。このメソッドは、use rand::Rng; 文によって
スコープに導入した Rng トレイトで定義されています。
gen_range メソッドは、引数として範囲式を受け取り、その範囲内の
乱数を生成します。ここで使用している範囲式は start..=end
という形を取り、下限と上限の両方を含みます。そのため、1 から 100 までの数を
要求するには 1..=100 を指定する必要があります。
注: どのトレイトを使い、クレートのどのメソッドや関数を呼び出せばよいかが 自然にわかるわけではないので、各クレートにはその使い方を説明する ドキュメントがあります。Cargo のもう 1 つの便利な機能は、
cargo doc --openコマンドを実行すると、依存関係が提供するすべてのドキュメントを ローカルでビルドしてブラウザーで開いてくれることです。たとえば、randクレートのほかの機能に興味があるなら、cargo doc --openを実行して、 左側のサイドバーにあるrandをクリックしてください。
2 行目の新しい行では、秘密の数を表示します。これはプログラムを開発している 間、テストできるようにするために便利ですが、最終版では削除します。 起動した瞬間にプログラムが答えを表示してしまっては、あまりゲームになりません!
プログラムを何回か実行してみましょう:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5
毎回異なる乱数が得られるはずで、しかもそれらはすべて 1 から 100 の間の数に なっているはずです。すばらしいです!
予想と秘密の数を比較する
ユーザー入力と乱数が得られたので、それらを比較できます。その手順を リスト 2-4 に示します。ただし、後で説明するように、このコードはまだ コンパイルできないことに注意してください。
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
// --snip--
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
まず、もう 1 つ use 文を追加し、標準ライブラリから
std::cmp::Ordering という型をスコープに導入します。Ordering 型も
別の enum であり、Less、Greater、Equal というバリアントを
持っています。これらは 2 つの値を比較したときにあり得る 3 つの結果です。
次に、末尾に Ordering 型を使う 5 行を新しく追加します。
cmp メソッドは 2 つの値を比較し、比較可能なものなら何に対してでも
呼び出せます。これは比較対象への参照を受け取ります。ここでは、
guess と secret_number を比較しています。そして、use 文で
スコープに導入した Ordering enum のバリアントを返します。guess と
secret_number の値で cmp を呼び出した結果として Ordering の
どのバリアントが返されたかに基づいて、次に何をするかを決めるために
match 式を使います。
match 式は アーム で構成されます。アームは、照合対象となる パターン と、
match に渡された値がそのアームのパターンに当てはまった場合に実行される
コードで構成されます。Rust は match に渡された値を受け取り、
各アームのパターンを順番に調べます。パターンと match 構文は
強力な Rust の機能です。これらにより、コードが遭遇しうるさまざまな状況を
表現でき、それらすべてを確実に処理できます。これらの機能については、
それぞれ第6章と第19章で詳しく扱います。
ここで使っている match 式の例を見ていきましょう。たとえば、
ユーザーの予想が 50 で、今回ランダムに生成された秘密の数が 38 だとします。
コードが 50 と 38 を比較すると、50 は 38 より大きいため、cmp
メソッドは Ordering::Greater を返します。match 式は
Ordering::Greater の値を受け取り、各アームのパターンのチェックを
始めます。まず最初のアームのパターン Ordering::Less を見て、
値 Ordering::Greater は Ordering::Less に一致しないので、その
アームのコードは無視して次のアームに進みます。次のアームのパターンは
Ordering::Greater で、これは Ordering::Greater に一致します!
そのアームに関連付けられたコードが実行され、画面に Too big! と
表示します。match 式は最初に成功した一致の後で終了するため、
この状況では最後のアームは調べません。
しかし、リスト 2-4 のコードはまだコンパイルできません。試してみましょう:
$ cargo build
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_core v0.6.2
Compiling rand_chacha v0.3.0
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:23:21
|
23 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
| |
| arguments to this method are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: method defined here
--> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/cmp.rs:979:8
For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error
エラーの核心は、型の不一致 があるということです。Rust には強力な静的型システムがあります。しかし、型推論も備えています。let mut guess = String::new() と書いたとき、Rust は guess が String であるべきだと推論できたので、型を書かなくてもよかったのです。一方、secret_number は数値型です。Rust の数値型のうち、1 から 100 までの値を取りうるものには、32ビット整数の i32、符号なし32ビット整数の u32、64ビット整数の i64 などがあります。特に指定しない限り、Rust は i32 をデフォルトとするため、ほかの場所で Rust が別の数値型を推論するような型情報を追加しない限り、secret_number の型は i32 になります。エラーの理由は、Rust では文字列型と数値型を比較できないからです。
最終的には、プログラムが入力として読み取った String を数値型に変換して、秘密の数と数値として比較できるようにしたいわけです。そのために、main 関数本体に次の行を追加します:
ファイル名: src/main.rs
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
// --snip--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
その行は次のとおりです:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
私たちは guess という名前の変数を作成します。ですが、ちょっと待ってください。プログラムにはすでに guess という名前の変数があるのではないでしょうか。あります。しかし、ありがたいことに Rust では、新しい値で以前の guess の値をシャドーイングできます。シャドーイング を使うと、たとえば guess_str と guess のように 2 つの別々の変数を作らなくても、guess という変数名を再利用できます。これについては 第3章 で詳しく扱いますが、今は、この機能がある値をある型から別の型へ変換したいときによく使われる、ということだけ知っておいてください。
この新しい変数を、guess.trim().parse() という式に束縛します。この式の guess は、入力を文字列として保持していた元の guess 変数を指しています。String インスタンスの trim メソッドは、先頭と末尾のあらゆる空白を取り除きます。これは、文字列を u32 に変換する前に必要な処理です。というのも、u32 には数値データしか入れられないからです。ユーザーは read_line を完了させて予想を入力するために enter を押す必要があり、その結果、文字列に改行文字が追加されます。たとえば、ユーザーが 5 と入力して enter を押すと、guess は次のようになります: 5\n。\n は「改行」を表します。(Windows では、enter を押すとキャリッジリターンと改行、つまり \r\n になります。)trim メソッドは \n または \r\n を取り除くので、結果は単に 5 になります。
文字列に対する parse メソッド は、文字列を別の型に変換します。ここでは、文字列から数値への変換に使っています。必要な正確な数値型は、let guess: u32 を使って Rust に伝える必要があります。guess の後のコロン(:)は、変数の型注釈を書くことを Rust に伝えています。Rust には組み込みの数値型がいくつかあり、ここでの u32 は符号なし32ビット整数です。小さな正の数には適切なデフォルトの選択です。第3章 でほかの数値型について学びます。
さらに、このサンプルプログラムにある u32 の注釈と secret_number との比較によって、Rust は secret_number も u32 であるべきだと推論します。これで比較は同じ型の 2 つの値の間で行われるようになります!
parse メソッドは、論理的に数値へ変換できる文字に対してしか機能しないため、簡単にエラーの原因になります。たとえば、文字列に A👍% が入っていたなら、それを数値に変換する方法はありません。失敗する可能性があるので、parse メソッドは read_line メソッドと同じように Result 型を返します(これは前の 「Result による起こりうる失敗の処理」 で説明しました)。この Result も、再び expect メソッドを使って同じように扱います。文字列から数値を作れず parse が Err の Result バリアントを返した場合、expect の呼び出しによってゲームはクラッシュし、与えたメッセージが表示されます。parse が文字列を数値にうまく変換できた場合は、Result の Ok バリアントを返し、expect は Ok の値から欲しい数値を返します。
では、ここでプログラムを実行してみましょう:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
いいですね! 予想の前に空白が追加されていても、プログラムはユーザーが 76 と予想したことを正しく理解できました。プログラムを何度か実行して、入力の種類によって挙動が変わることを確認してみましょう。数を正しく当てる場合、高すぎる数を予想する場合、低すぎる数を予想する場合です。
これでゲームの大部分は動くようになりましたが、ユーザーは 1 回しか予想できません。ループを追加して、これを変えましょう!
ループで複数回予想できるようにする
loop キーワードは無限ループを作ります。ユーザーに数字を当てる機会をもっと与えるために、ループを追加しましょう:
ファイル名: src/main.rs
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
// --snip--
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
// --snip--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
}
ご覧のとおり、予想の入力を促す箇所以降のすべてをループの中に移動しました。ループの内側の行はそれぞれさらに 4 スペース分インデントするのを忘れずに、もう一度プログラムを実行してください。これでプログラムは延々と次の予想を求めるようになりますが、実は新しい問題が生まれます。ユーザーが終了できなさそうなのです!
もちろん、ユーザーはキーボードショートカット ctrl-C を使ってプログラムを中断できます。しかし、この飽くことのない怪物から抜け出す別の方法もあります。「予想と秘密の数を比較する」 の parse の説明で述べたように、ユーザーが数値以外の答えを入力すると、プログラムはクラッシュします。これを利用して、次のようにユーザーが終了できるようにできます:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at src/main.rs:28:47:
Please type a number!: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
quit と入力するとゲームは終了しますが、お気づきのとおり、数値以外の
ほかの入力をしても同様に終了してしまいます。控えめに言っても、これは最適
ではありません。正しい数を当てたときにもゲームが終了するようにしたいです。
正しく当てたら終了する
break 文を追加して、ユーザーが勝ったときにゲームが終了するようにして
みましょう。
ファイル名: src/main.rs
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
You win! の後に break の行を追加すると、ユーザーが秘密の数を正しく
当てたときにプログラムがループを抜けるようになります。ループを抜けること
は、プログラムを終了することも意味します。というのも、ループは main の
最後の部分だからです。
無効な入力の処理
ゲームの動作をさらに洗練させるために、ユーザーが数値以外を入力したときに
プログラムをクラッシュさせるのではなく、数値以外の入力を無視してユーザー
が予想を続けられるようにしてみましょう。それは、リスト 2-5 に示すように、
guess を String から u32 に変換している行を変更することで実現できます。
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
// --snip--
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
expect 呼び出しから match 式に切り替えることで、エラー時にクラッシュ
するのではなく、エラーを処理するようにしています。parse は Result
型を返し、Result は Ok と Err というバリアントを持つ enum である
ことを思い出してください。ここでは、cmp メソッドの Ordering の結果
で行ったのと同じように、match 式を使っています。
parse が文字列を数値に正しく変換できた場合、結果として得られた数値を
含む Ok 値を返します。その Ok 値は最初のアームのパターンにマッチし、
match 式は parse が生成して Ok 値の中に入れた num の値をそのまま
返します。その数値は、新しく作成している guess 変数の、まさに欲しい
場所に入ることになります。
parse が文字列を数値に変換_できない_場合は、エラーについての詳しい情報を
含む Err 値を返します。この Err 値は最初の match アームの
Ok(num) パターンにはマッチしませんが、2 番目のアームの Err(_)
パターンにはマッチします。アンダースコア _ はワイルドカードです。この
例では、中にどんな情報が入っていても、すべての Err 値にマッチさせたいと
言っています。したがって、プログラムは 2 番目のアームのコードである
continue を実行します。これは、プログラムに loop の次の繰り返しへ
進んで、もう一度予想を尋ねるように指示します。つまり実質的には、
プログラムは parse が遭遇しうるすべてのエラーを無視することになります!
これで、プログラム内のすべてが期待どおりに動作するはずです。試してみま しょう。
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!
すばらしいですね! あと最後に小さな調整を 1 つ加えれば、数当てゲームは
完成です。プログラムがまだ秘密の数を表示していることを思い出してください。
これはテストには便利でしたが、ゲームとしては台無しです。秘密の数を出力し
ている println! を削除しましょう。リスト 2-6 に最終的なコードを示し
ます。
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
これで、数当てゲームを無事に作り上げることができました。おめでとうござい ます!
まとめ
このプロジェクトは、let、match、関数、外部 crate の利用など、多くの
新しい Rust の概念を実際に手を動かしながら導入する方法でした。これから
数章にわたって、これらの概念をさらに詳しく学んでいきます。第 3 章では、
変数、データ型、関数など、ほとんどのプログラミング言語にある概念を扱い、
それらを Rust でどう使うかを示します。第 4 章では、Rust を他の言語と
異なるものにしている機能である所有権を掘り下げます。第 5 章では構造体と
メソッド構文について説明し、第 6 章では enum の仕組みを説明します。
一般的なプログラミングの概念
この章では、ほとんどすべてのプログラミング言語に登場する概念と、それらがRustでどのように機能するかを扱います。多くのプログラミング言語は、その根幹において多くの共通点を持っています。この章で紹介する概念はいずれもRust特有のものではありませんが、Rustの文脈でそれらを取り上げ、それらを使う際の慣習について説明します。
具体的には、変数、基本的な型、関数、コメント、制御フローについて学びます。これらの基礎はあらゆるRustプログラムに含まれるものであり、早い段階で学ぶことで、しっかりとした土台を築いてスタートできます。
キーワード
Rust言語には、他の言語と同様に、その言語だけが使用するために予約されている_キーワード_の集合があります。これらの単語は、変数名や関数名として使用できないことに注意してください。キーワードのほとんどには特別な意味があり、Rustプログラムでさまざまな処理を行うために使用します。いくつかは現時点では対応する機能を持っていませんが、将来Rustに追加されるかもしれない機能のために予約されています。キーワードの一覧は付録Aにあります。
変数と可変性
変数と可変性
「変数による値の格納」節で触れたとおり、デフォルトでは、 変数は不変です。これは、Rust が提供する安全性と容易な並行性を活かす 形でコードを書くように Rust が促す、数多くの後押しの1つです。しかし、 それでも変数を可変にする選択肢はあります。Rust がどのように、そして なぜ不変性を優先するよう促すのか、また、ときにはなぜその方針から 外れたくなるのかを見ていきましょう。
変数が不変である場合、ひとたび値が名前に束縛されると、その値を変更
することはできません。これを示すために、cargo new variables を
使って、projects ディレクトリ内に variables という新しい
プロジェクトを作成してください。
次に、新しい variables ディレクトリで src/main.rs を開き、その コードを次のコードに置き換えてください。この時点ではまだコンパイル できません。
ファイル名: src/main.rs
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
保存して、cargo run を使ってプログラムを実行してください。次の出力に
示すように、不変性に関するエラーメッセージが表示されるはずです。
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| - first assignment to `x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
この例は、コンパイラがプログラム内のエラーを見つけるのをどのように 助けてくれるかを示しています。コンパイラエラーは苛立たしいものかも しれませんが、実際には、あなたのプログラムがまだ安全に望んだことを していないというだけの意味です。決して、あなたが優れた プログラマではないという意味ではありません! 経験豊富な Rustacean でも、コンパイラエラーは出ます。
cannot assign twice to immutable variable `x` という
エラーメッセージを受け取ったのは、不変の x 変数に2つ目の値を
代入しようとしたからです。
不変として指定された値を変更しようとしたときにコンパイル時エラーが
出ることは重要です。まさにこのような状況がバグにつながる可能性が
あるからです。コードのある部分が、値は決して変わらないという前提で
動作している一方で、別の部分がその値を変更してしまうと、最初の部分の
コードが設計どおりに動かなくなる可能性があります。この種のバグの原因は、
事後に追跡するのが難しいことがあります。特に、2つ目のコード片がその値を
changed only sometimes? Wait translation error.
このプログラムでは、まず x を 5 という値に束縛します。次に、let x = を
繰り返すことで新しい変数 x を作成し、元の値に 1 を加えるため、
x の値は 6 になります。さらに、中かっこで作られた内側のスコープ内では、
3つ目の let 文も x をシャドーイングして新しい変数を作成し、1つ前の値を
2 倍して、x の値を 12 にします。そのスコープが終わると、内側の
シャドーイングも終了し、x は再び 6 に戻ります。
このプログラムを実行すると、次のように出力されます。
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
シャドーイングは、変数を mut としてマークすることとは異なります。というのも、
let キーワードを使わずにこの変数へ誤って再代入しようとすると、コンパイル時
エラーになるからです。let を使うことで、値に対していくつかの変換を行いつつ、
それらの変換が完了したあとは変数を不変のままにできます。
mut とシャドーイングのもう1つの違いは、再び let キーワードを使うと
実質的に新しい変数を作成しているため、同じ名前を再利用しながら値の型を
変更できることです。たとえば、あるプログラムが、テキストの間にいくつ空白を
入れたいかをスペース文字の入力でユーザーに示してもらい、その入力をその後
数値として格納したいとします。
fn main() {
let spaces = " ";
let spaces = spaces.len();
}
最初の spaces 変数は文字列型で、2つ目の spaces 変数は数値型です。
そのため、シャドーイングを使うと、spaces_str や spaces_num のような
別の名前を考え出さずに済み、その代わりによりシンプルな spaces という名前を
再利用できます。しかし、ここに示すように、このために mut を使おうとすると、
コンパイル時エラーになります。
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}
このエラーは、変数の型を変更することは許可されていないと述べています。
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error
変数がどのように機能するかを見てきたので、次は変数が取り得る、より多くの データ型を見ていきましょう。
データ型
データ型
Rust におけるすべての値は、ある データ型 を持っています。データ型は、どの種類の データが指定されているのかを Rust に伝え、そのデータをどう扱うべきかを Rust が理解 できるようにします。ここでは、データ型の 2 つのサブセットであるスカラー型と複合型を 見ていきます。
Rust は 静的型付け 言語であることを覚えておいてください。つまり、コンパイル時に
すべての変数の型がわかっていなければなりません。コンパイラは通常、値とその使い方に
基づいて、私たちが使いたい型を推論できます。第 2 章の
「予想と秘密の数を比較する」 節で
parse を使って String を数値型に変換したときのように、取り得る型が複数ある場合
には、次のように型注釈を追加しなければなりません。
#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}
前のコードに示した : u32 という型注釈を追加しないと、Rust は次のエラーを表示しま
す。これは、どの型を使いたいのかをコンパイラが判断するために、私たちからさらに多く
の情報を必要としていることを意味します。
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
|
2 | let guess: /* Type */ = "42".parse().expect("Not a number!");
| ++++++++++++
For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error
他のデータ型では、異なる型注釈を目にすることになります。
スカラー型
スカラー 型は単一の値を表します。Rust には 4 つの主要なスカラー型があります。 整数、浮動小数点数、ブール値、文字です。これらは他のプログラミング言語でも見覚えが あるかもしれません。Rust でどのように機能するのかを見ていきましょう。
整数型
整数 とは、小数部分を持たない数です。第 2 章では、整数型の 1 つである u32 型を
使いました。この型宣言は、関連付けられる値が、32 ビットの領域を使用する符号なし整数
であるべきことを示しています(符号付き整数型は u ではなく i で始まります)。
表 3-1 は Rust に組み込まれている整数型を示しています。整数値の型を宣言するには、
これらのバリアントのいずれでも使えます。
表 3-1: Rust における整数型
| 長さ | 符号付き | 符号なし |
|---|---|---|
| 8 ビット | i8 | u8 |
| 16 ビット | i16 | u16 |
| 32 ビット | i32 | u32 |
| 64 ビット | i64 | u64 |
| 128 ビット | i128 | u128 |
| アーキテクチャ依存 | isize | usize |
各バリアントは符号付きまたは符号なしのいずれかで、明示的なサイズを持ちます。 符号付き と 符号なし は、その数が負になり得るかどうかを指します。言い換えると、 その数に符号が必要か(符号付き)、あるいは常に正であり、そのため符号なしで表現 できるか(符号なし)ということです。これは紙に数を書くときと似ています。符号が 重要なときは数はプラス記号またはマイナス記号とともに書かれますが、その数が正だと 考えて差し支えないときは符号なしで書かれます。符号付き数は 2 の補数 表現で格納されます。
各符号付きバリアントは、−(2n − 1) から 2n − 1 − 1 まで
(両端を含む)の数を格納できます。ここで n はそのバリアントが使用するビット数
です。したがって、i8 は −(27) から 27 − 1、つまり
−128 から 127 までの数を格納できます。符号なしバリアントは 0 から
2n − 1 までの数を格納できるので、u8 は 0 から
28 − 1、つまり 0 から 255 までの数を格納できます。
さらに、isize と usize 型は、プログラムが実行されるコンピューターの
アーキテクチャに依存します。64 ビットアーキテクチャでは 64 ビット、
32 ビットアーキテクチャでは 32 ビットです。
整数リテラルは、表 3-2 に示す任意の形式で記述できます。複数の数値型になり得る
数値リテラルでは、57u8 のように型サフィックスを付けて型を指定できることに注意
してください。数値リテラルでは _ を視覚的な区切りとして使って読みやすくする
こともできます。たとえば 1_000 は 1000 を指定した場合と同じ値になります。
表 3-2: Rust における整数リテラル
| 数値リテラル | 例 |
|---|---|
| 10 進数 | 98_222 |
| 16 進数 | 0xff |
| 8 進数 | 0o77 |
| 2 進数 | 0b1111_0000 |
バイト(u8 のみ) | b'A' |
では、どの整数型を使えばよいのでしょうか。よくわからない場合は、Rust のデフォルト
から始めるのが一般的にはよいでしょう。整数型のデフォルトは i32 です。isize
または usize を使う主な場面は、何らかのコレクションにインデックスを付けるとき
です。
整数オーバーフロー
0 から 255 までの値を保持できる
u8型の変数があるとします。その変数を 256 の ような範囲外の値に変更しようとすると、整数オーバーフロー が発生し、2 つの 振る舞いのいずれかになります。デバッグモードでコンパイルすると、Rust は整数 オーバーフローを検査するチェックを組み込み、この挙動が発生した場合に実行時に プログラムを パニック させます。Rust では、プログラムがエラーで終了することを パニックする と呼びます。パニックについては、第 9 章の 「panic!による回復不能なエラー」 節で さらに詳しく説明します。
--releaseフラグ付きのリリースモードでコンパイルすると、Rust はパニックを 引き起こす整数オーバーフローのチェックを 含めません。その代わり、オーバー フローが起こると、Rust は 2 の補数によるラップアラウンド を行います。要するに、 その型が保持できる最大値より大きい値は、その型が保持できる最小値へと「折り返され」 ます。u8の場合、256 は 0 になり、257 は 1 になり、以下同様です。プログラムは パニックしませんが、その変数にはおそらく期待していたものとは異なる値が入ります。 整数オーバーフローのラップアラウンド動作に依存することは、エラーと見なされます。オーバーフローの可能性を明示的に扱うには、標準ライブラリがプリミティブ数値型に 提供している次のメソッド群を利用できます。
wrapping_addなどのwrapping_*メソッドで、すべてのモードでラップする。checked_*メソッドで、オーバーフローがあればNone値を返す。overflowing_*メソッドで、値と、オーバーフローがあったかどうかを示す ブール値を返す。saturating_*メソッドで、値の最小値または最大値で飽和させる。
浮動小数点型
Rust には 浮動小数点数 のためのプリミティブ型も 2 つあります。これは小数点を
持つ数です。Rust の浮動小数点型は f32 と f64 で、それぞれ 32 ビットと
64 ビットのサイズです。デフォルトの型は f64 です。これは、現代の CPU では
f32 とほぼ同じ速度でありながら、より高い精度を持てるためです。すべての
浮動小数点型は符号付きです。
次は、浮動小数点数がどのように動作するかを示す例です。
ファイル名: src/main.rs
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
浮動小数点数は IEEE-754 標準に従って表現されます。
数値演算
Rust は、すべての数値型に対して、期待どおりの基本的な数学演算をサポートしています。すなわち、加算、減算、乗算、除算、剰余です。整数の除算は、ゼロ方向に切り捨てて最も近い整数になります。次のコードは、各数値演算を let 文でどのように使うかを示しています。
ファイル名: src/main.rs
fn main() {
// addition
let sum = 5 + 10;
// subtraction
let difference = 95.5 - 4.3;
// multiplication
let product = 4 * 30;
// division
let quotient = 56.7 / 32.2;
let truncated = -5 / 3; // Results in -1
// remainder
let remainder = 43 % 5;
}
これらの文に含まれる各式は数学演算子を使用し、単一の値に評価され、その後変数に束縛されます。付録 B には、Rust が提供するすべての演算子の一覧があります。
真偽値型
他の多くのプログラミング言語と同様に、Rust の真偽値型は true と false の 2 つの値を取りえます。真偽値のサイズは 1 バイトです。Rust における真偽値型は bool で表されます。たとえば次のとおりです。
ファイル名: src/main.rs
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
真偽値を使う主な方法は、if 式のような条件分岐を通じてです。Rust における if 式の動作については、「制御フロー」 の節で扱います。
文字型
Rust の char 型は、この言語における最も基本的なアルファベット型です。char 値の宣言例をいくつか示します。
ファイル名: src/main.rs
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}
char リテラルはダブルクォートを使う文字列リテラルとは異なり、シングルクォートで指定する点に注意してください。Rust の char 型は 4 バイトの大きさを持ち、Unicode スカラー値を表します。つまり、単なる ASCII よりもはるかに多くのものを表現できます。アクセント付き文字、中国語・日本語・韓国語の文字、絵文字、ゼロ幅スペースはいずれも Rust では有効な char 値です。Unicode スカラー値の範囲は U+0000 から U+D7FF および U+E000 から U+10FFFF までです。ただし、「文字」は Unicode において実際には厳密な概念ではないため、「文字」とは何かについての人間の直感は、Rust における char と一致しない場合があります。この話題については、第 8 章の 「文字列による UTF-8 エンコード済みテキストの格納」 で詳しく説明します。
複合型
複合型 は、複数の値を 1 つの型にまとめることができます。Rust には 2 つの基本的な複合型があります。タプルと配列です。
タプル型
タプル は、さまざまな型の複数の値を 1 つの複合型にまとめる一般的な方法です。タプルの長さは固定です。いったん宣言すると、大きくしたり小さくしたりはできません。
タプルは、丸括弧の中にカンマ区切りの値のリストを書いて作成します。タプル内の各位置には型があり、タプル内の各値の型は同じである必要はありません。この例では、任意の型注釈を追加しています。
ファイル名: src/main.rs
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
変数 tup は、タプル全体に束縛されます。これは、タプルが 1 つの複合要素と見なされるためです。タプルから個々の値を取り出すには、次のようにパターンマッチングを使ってタプル値を分解できます。
ファイル名: src/main.rs
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
このプログラムはまずタプルを作成し、それを変数 tup に束縛します。次に、let とともにパターンを使って tup を取り出し、3 つの別々の変数 x、y、z にします。これは、1 つのタプルを 3 つの部分に分解するため、分配束縛 と呼ばれます。最後に、プログラムは y の値、すなわち 6.4 を表示します。
また、アクセスしたい値のインデックスをピリオド (.) に続けて指定することで、タプル要素に直接アクセスすることもできます。たとえば次のとおりです。
ファイル名: src/main.rs
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
このプログラムはタプル x を作成し、その後それぞれのインデックスを使って各要素にアクセスします。ほとんどのプログラミング言語と同様に、タプルの最初のインデックスは 0 です。
値をまったく持たないタプルには、特別な名前として unit があります。この値とそれに対応する型はどちらも () と書き、空の値または空の戻り値型を表します。式が他の値を返さない場合、暗黙的に unit 値を返します。
配列型
複数の値のコレクションを持つもう 1 つの方法は、配列 を使うことです。タプルとは異なり、配列のすべての要素は同じ型でなければなりません。また、一部の他言語の配列とは異なり、Rust の配列は長さが固定です。
配列の値は、角括弧の中にカンマ区切りのリストとして書きます。
ファイル名: src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
}
配列は、ここまでに見てきた他の型と同じように、データをヒープではなくスタックに確保したい場合(スタックとヒープについては 第 4 章 でさらに詳しく説明します)、あるいは常に固定数の要素を持つことを保証したい場合に便利です。ただし、配列はベクタ型ほど柔軟ではありません。ベクタは標準ライブラリが提供する類似のコレクション型で、その内容がヒープ上に置かれるため、サイズを大きくしたり小さくしたりできます。配列とベクタのどちらを使うべきか迷うなら、たいていの場合はベクタを使うべきです。第 8 章 ではベクタについてさらに詳しく説明します。
ただし、要素数が変わる必要がないことが分かっている場合には、配列のほうが有用です。たとえば、プログラムで月の名前を使う場合、常に 12 個の要素を含むことが分かっているため、ベクタではなくおそらく配列を使うでしょう。
#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
}
配列の型は、各要素の型、セミコロン、そして配列内の要素数を角括弧で囲んで、次のように書きます。
#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}
ここで、i32 は各要素の型です。セミコロンの後の数字 5 は、その配列が 5 個の要素を含むことを示しています。
また、初期値、その後にセミコロン、そして角括弧内に配列の長さを指定することで、各要素に同じ値を持つ配列を初期化することもできます。次のようになります。
#![allow(unused)]
fn main() {
let a = [3; 5];
}
a という名前の配列には 5 個の要素が含まれ、それらは最初すべて値 3 に設定されます。これは let a = [3, 3, 3, 3, 3]; と書くのと同じですが、より簡潔です。
配列要素へのアクセス
配列は、既知の固定サイズを持つ単一のメモリ領域であり、スタックに割り当てることができます。配列の要素には、次のようにインデックスを使ってアクセスできます。
<span class="filename">ファイル名: src/main.rs</span>
```rust
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
この例では、first という名前の変数は値 1 を受け取ります。これは、その値が配列のインデックス [0] にあるためです。second という名前の変数は、配列のインデックス [1] から値 2 を受け取ります。
無効な配列要素へのアクセス
配列の末尾を越えた要素にアクセスしようとすると何が起こるのかを見てみましょう。第2章の数当てゲームと同様に、ユーザーから配列のインデックスを受け取るために、次のコードを実行するとします。
ファイル名: src/main.rs
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}
このコードは問題なくコンパイルされます。このコードを cargo run で実行して 0、1、2、3、または 4 を入力すると、プログラムは配列内のそのインデックスに対応する値を出力します。代わりに、配列の末尾を越える数、たとえば 10 を入力すると、次のような出力が表示されます。
thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
このプログラムは、インデックス操作で無効な値を使った時点で実行時エラーになりました。プログラムはエラーメッセージを表示して終了し、最後の println! 文は実行されませんでした。インデックスを使って要素にアクセスしようとすると、Rust は指定したインデックスが配列の長さより小さいことを確認します。インデックスが長さ以上であれば、Rust は panic します。このチェックは実行時に行われる必要があります。特にこの場合、ユーザーが後でコードを実行するときにどんな値を入力するかを、コンパイラが知ることは不可能だからです。
これは、Rust のメモリ安全性の原則が実際に機能している一例です。多くの低水準言語では、この種のチェックは行われず、誤ったインデックスを与えると無効なメモリにアクセスできてしまいます。Rust は、メモリアクセスを許可して処理を継続するのではなく、直ちに終了することで、この種のエラーから保護してくれます。第9章では、Rust のエラーハンドリングについてさらに詳しく扱い、panic せず、また無効なメモリアクセスも許さない、読みやすく安全なコードをどのように書けるかを説明します。
関数
関数
関数は Rust のコードのいたるところで使われます。すでに、この言語で最も
重要な関数の1つである main 関数を見てきました。これは多くのプログラムの
エントリポイントです。また、新しい関数を宣言できる fn キーワードも
見てきました。
Rust のコードでは、関数名や変数名の慣習的なスタイルとして スネークケース を使用します。これは、すべての文字を小文字にし、単語をアンダースコアで区切る 形式です。次は、関数定義の例を含むプログラムです。
ファイル名: src/main.rs
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
Rust では、fn の後に関数名と一組の丸括弧を書いて関数を定義します。
波括弧は、関数本体の開始位置と終了位置をコンパイラに伝えます。
定義した関数はどれでも、その名前の後に一組の丸括弧を書くことで呼び出せます。
another_function はプログラム内で定義されているので、main 関数の内側から
呼び出せます。ソースコードでは main 関数の 後 に another_function を
定義していますが、前に定義してもかまいません。Rust が気にするのは、関数を
どこで定義したかではなく、呼び出し元から見えるスコープのどこかで定義されて
いることだけです。
関数をさらに詳しく調べるために、functions という名前の新しいバイナリ
プロジェクトを始めましょう。another_function の例を src/main.rs に
置いて実行してください。次の出力が表示されるはずです。
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/functions`
Hello, world!
Another function.
各行は、main 関数に書かれている順序で実行されます。最初に
「Hello, world!」メッセージが表示され、その後 another_function が
呼び出されて、そのメッセージが表示されます。
パラメータ
関数には パラメータ を持たせることができます。これは関数のシグネチャの 一部である特別な変数です。関数にパラメータがある場合、そのパラメータに 具体的な値を与えることができます。厳密には、その具体的な値は 引数 と 呼ばれます。しかし日常的な会話では、関数定義中の変数を指す場合でも、関数を 呼び出すときに渡される具体的な値を指す場合でも、パラメータ と 引数 という 言葉は区別せずに使われることがよくあります。
この版の another_function では、パラメータを1つ追加しています。
ファイル名: src/main.rs
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
}
このプログラムを実行してみてください。次の出力が得られるはずです。
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
another_function の宣言には、x という名前のパラメータが1つあります。x
の型は i32 として指定されています。5 を another_function に渡すと、
println! マクロはフォーマット文字列内の x を含む一対の波括弧の位置に 5
を入れます。
関数シグネチャでは、各パラメータの型を 必ず 宣言しなければなりません。 これは Rust の設計上の意図的な決定です。関数定義で型注釈を必須にすることで、 コンパイラは、どの型を意味しているのかを判断するために、コードの他の場所で それらを要求する必要がほとんどなくなります。また、関数がどの型を期待して いるかが分かっていれば、コンパイラはより役に立つエラーメッセージを出せます。
複数のパラメータを定義するときは、次のようにパラメータ宣言をカンマで 区切ります。
ファイル名: src/main.rs
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
この例では、print_labeled_measurement という名前の関数を2つのパラメータ付きで
作成しています。最初のパラメータの名前は value で、i32 です。2つ目は
unit_label という名前で、型は char です。その後、この関数は value と
unit_label の両方を含むテキストを表示します。
このコードを実行してみましょう。あなたの functions プロジェクトの
src/main.rs ファイルに現在入っているプログラムを先ほどの例で置き換え、
cargo run を使って実行してください。
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
関数を、value の値として 5、unit_label の値として 'h' を渡して
呼び出したので、プログラムの出力にはそれらの値が含まれます。
文と式
関数本体は、一連の文で構成され、末尾に式がある場合もあります。ここまでに 扱ってきた関数には末尾の式は含まれていませんでしたが、文の一部としての式は すでに見ています。Rust は式ベースの言語なので、この違いを理解することは 重要です。他の言語では同じような区別がないこともあるので、文と式とは何か、 そしてその違いが関数本体にどのような影響を与えるのかを見ていきましょう。
- 文 は何らかの動作を実行する命令で、値を返しません。
- 式 は結果の値に評価されます。
いくつか例を見てみましょう。
実は、私たちはすでに文と式を使っています。変数を作成し、let キーワードを
使ってその変数に値を代入することは文です。リスト3-1では、let y = 6; は
文です。
fn main() {
let y = 6;
}
関数定義も文です。前の例全体が、それ自体で1つの文になっています。 (ただし、すぐに見るように、関数呼び出しは文ではありません。)
文は値を返しません。したがって、次のコードがそうしようとしているように、
let 文を別の変数に代入することはできません。エラーになります。
ファイル名: src/main.rs
fn main() {
let x = (let y = 6);
}
このプログラムを実行すると、表示されるエラーは次のようになります。
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
2 - let x = (let y = 6);
2 + let x = let y = 6;
|
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted
let y = 6 文は値を返さないので、x が束縛するものは何もありません。これは、
C や Ruby のような、代入が代入された値を返す他の言語で起こることとは異なり
ます。それらの言語では x = y = 6 と書いて、x と y の両方を値 6 に
できますが、Rust ではそうではありません。
式は値に評価され、Rust でこれから書くコードの残りの大部分を占めます。
5 + 6 のような数式を考えてみましょう。これは値 11 に評価される式です。
式は文の一部にもなれます。リスト3-1では、文 let y = 6; の中の 6 は、
値 6 に評価される式です。関数呼び出しは式です。マクロ呼び出しも式です。
波括弧で作られた新しいスコープブロックも式です。たとえば、
<span class="filename">ファイル名: src/main.rs</span>
```rust
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
}
この式:
{
let x = 3;
x + 1
}
はブロックであり、この場合は 4 に評価されます。この値は let 文の
一部として y に束縛されます。末尾にセミコロンがない x + 1 の行に
注目してください。これは、これまでに見てきたほとんどの行とは異なります。
式には末尾のセミコロンは含まれません。式の末尾にセミコロンを追加すると、
それは文になり、値を返さなくなります。次に関数の戻り値と式を見ていく際は、
この点を覚えておいてください。
戻り値のある関数
関数は、それを呼び出すコードに値を返すことができます。戻り値に名前は付けません
が、矢印 (->) の後にその型を宣言しなければなりません。Rust では、関数の
戻り値は、関数本体のブロックにおける最後の式の値と同義です。return
キーワードを使って値を指定することで、関数から早期に戻ることもできますが、
ほとんどの関数は最後の式を暗黙的に返します。以下は値を返す関数の例です:
ファイル名: src/main.rs
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}
five 関数には、関数呼び出しも、マクロも、let 文すらありません。あるのは
それ単体の数値 5 だけです。これは Rust では完全に有効な関数です。関数の
戻り値の型も -> i32 として指定されていることに注目してください。このコードを
実行してみてください。出力は次のようになるはずです:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
The value of x is: 5
five の中の 5 は関数の戻り値であり、それが戻り値の型が i32 である理由です。
これをもう少し詳しく見てみましょう。重要な点は 2 つあります。まず、
let x = five(); という行は、関数の戻り値を使って変数を初期化していることを
示しています。関数 five は 5 を返すので、この行は次と同じです:
#![allow(unused)]
fn main() {
let x = 5;
}
次に、five 関数には引数がなく、戻り値の型が定義されていますが、関数本体は
セミコロンのない 5 だけです。これは、返したい値を持つ式だからです。
別の例を見てみましょう:
ファイル名: src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1
}
このコードを実行すると、The value of x is: 6 と表示されます。では、x + 1
を含む行の末尾にセミコロンを置いて、それを式から文に変えるとどうなるでしょうか?
ファイル名: src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
このコードをコンパイルすると、次のようなエラーが発生します:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
--> src/main.rs:7:24
|
7 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8 | x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` (bin "functions") due to 1 previous error
主要なエラーメッセージである mismatched types は、このコードの核心的な問題を
明らかにしています。関数 plus_one の定義では、i32 を返すとされていますが、
文は値に評価されず、それはユニット型である () で表されます。したがって、
何も返されず、それが関数定義と矛盾してエラーになります。この出力では、Rust は
この問題を修正する助けになる可能性のあるメッセージも提供しています。セミコロン
を削除するよう提案しており、それによってエラーは修正されます。
コメント
コメント
すべてのプログラマーは、自分のコードを理解しやすくしようと努めますが、追加の説明が必要になることもあります。そうした場合、プログラマーはソースコード内に コメント を残します。コメントはコンパイラには無視されますが、ソースコードを読む人にとっては役立つことがあります。
簡単なコメントを以下に示します。
#![allow(unused)]
fn main() {
// こんにちは、世界
}
Rust では、慣用的なコメントスタイルでは 2 つのスラッシュでコメントを始め、そのコメントは行末まで続きます。1 行を超えるコメントの場合は、次のように各行に // を付ける必要があります。
#![allow(unused)]
fn main() {
// ここでは複雑なことをしているため、その説明には十分な長さがあり、
// そのために複数行のコメントが必要です! ふう! このコメントが
// 何が起こっているのかを説明してくれるといいのですが。
}
コメントは、コードを含む行の末尾に置くこともできます。
ファイル名: src/main.rs
fn main() {
let lucky_number = 7; // I'm feeling lucky today
}
ただし、よりよく見かけるのは、コメントを注釈対象のコードの上の別行に置く、この形式です。
ファイル名: src/main.rs
fn main() {
// I'm feeling lucky today
let lucky_number = 7;
}
Rust には、もう 1 種類のコメントであるドキュメンテーションコメントもあります。これについては、第 14 章の 「Crate を Crates.io に公開する」 の節で説明します。
制御フロー
制御フロー
条件が true であるかどうかに応じて何らかのコードを実行する機能や、条件が true である間に何らかのコードを繰り返し実行する機能は、ほとんどのプログラミング言語における基本的な構成要素です。Rust コードの実行フローを制御するためのもっとも一般的な構文は、if 式とループです。
if 式
if 式を使うと、条件に応じてコードを分岐できます。条件を指定し、「この条件が満たされたら、このコードブロックを実行する。条件が満たされなければ、このコードブロックは実行しない」と記述します。
if 式を試すために、projects ディレクトリに branches という新しいプロジェクトを作成してください。src/main.rs ファイルに、次の内容を入力します。
ファイル名: src/main.rs
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
すべての if 式は、キーワード if で始まり、その後に条件が続きます。この場合、条件は変数 number の値が 5 未満かどうかをチェックしています。条件が true のときに実行するコードブロックは、中かっこで囲んで条件の直後に置きます。if 式の条件に関連付けられたコードブロックは、「推測と秘密の数字を比較する」 で第2章で説明した match 式のアームと同じように、アーム と呼ばれることがあります。
必要に応じて、ここで行っているように else 式を含めることもできます。これにより、条件が false と評価された場合に実行する別のコードブロックをプログラムに与えられます。else 式を用意せず、条件が false の場合、プログラムは単に if ブロックをスキップして次のコードへ進みます。
このコードを実行してみてください。次のような出力が表示されるはずです。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was true
では、条件が false になる値に number の値を変更して、何が起こるか見てみましょう。
fn main() {
let number = 7;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
もう一度プログラムを実行し、出力を見てみてください。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was false
このコードの条件は、必ず bool でなければならないことにも注意してください。条件が bool でなければ、エラーになります。たとえば、次のコードを実行してみてください。
ファイル名: src/main.rs
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
今回は if の条件が値 3 に評価されるため、Rust はエラーを返します。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected `bool`, found integer
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
このエラーは、Rust が bool を期待していたのに整数を受け取ったことを示しています。Ruby や JavaScript のような言語とは異なり、Rust はブール値でない型をブール値へ自動的に変換しようとはしません。常に明示的に、if に条件としてブール値を与える必要があります。たとえば、数値が 0 と等しくないときだけ if のコードブロックを実行したいなら、if 式を次のように変更できます。
ファイル名: src/main.rs
fn main() {
let number = 3;
if number != 0 {
println!("number was something other than zero");
}
}
このコードを実行すると、number was something other than zero と表示されます。
else if で複数の条件を扱う
if と else を組み合わせた else if 式を使うことで、複数の条件を扱えます。たとえば、次のようになります。
ファイル名: src/main.rs
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
このプログラムには、取りうる経路が 4 つあります。実行すると、次のような出力が表示されるはずです。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
number is divisible by 3
このプログラムが実行されると、各 if 式を順番にチェックし、条件が true と評価された最初の本体を実行します。6 は 2 でも割り切れるにもかかわらず、出力に number is divisible by 2 は表示されず、else ブロックの number is not divisible by 4, 3, or 2 というテキストも表示されないことに注目してください。これは、Rust が最初に true になった条件に対応するブロックだけを実行し、1 つ見つけた時点で残りは確認すらしないからです。
else if 式を使いすぎるとコードが煩雑になることがあるため、複数ある場合はコードのリファクタリングを検討するとよいでしょう。こうしたケースのための強力な Rust の分岐構文である match については、第6章で説明します。
let 文で if を使う
if は式なので、リスト 3-2 のように let 文の右辺で使って、その結果を変数に代入できます。
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
変数 number は、if 式の結果に応じた値に束縛されます。このコードを実行して、何が起こるか見てみましょう。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
The value of number is: 5
コードブロックはその中の最後の式に評価され、数値それ自体も式であることを思い出してください。この場合、if 式全体の値は、どのコードブロックが実行されるかに依存します。つまり、if の各アームから結果として返されうる値は、同じ型でなければなりません。リスト 3-2 では、if アームと else アームの両方の結果は i32 整数でした。次の例のように型が一致しない場合は、エラーになります。
ファイル名: src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
このコードをコンパイルしようとすると、エラーになります。if アームと else アームは互換性のない値の型を持っており、Rust はプログラム内のどこに問題があるかを正確に示してくれます。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
if ブロック内の式は整数に評価され、else ブロック内の式は文字列に評価されます。これはうまくいきません。変数は単一の型を持たなければならず、Rust はコンパイル時に number 変数の型が何であるかを明確に知っている必要があるからです。number の型が分かっていれば、コンパイラは number を使うあらゆる場所でその型が正しいことを検証できます。もし number の型が実行時にしか決まらないとしたら、Rust はそれを行えません。どの変数についても複数の仮の型を追跡しなければならないなら、コンパイラはより複雑になり、コードに対して与えられる保証も少なくなってしまいます。
ループによる繰り返し
コードのブロックを複数回実行したいことはよくあります。このために、 Rust にはいくつかの_ループ_が用意されており、ループ本体の中のコードを 最後まで実行してから、すぐに先頭に戻って再び開始します。ループを試して みるために、loops という名前の新しいプロジェクトを作成しましょう。
Rust には 3 種類のループがあります: loop、while、for です。それぞれを試してみましょう。
loop でコードを繰り返す
loop キーワードは、コードのブロックを何度も繰り返し実行するよう Rust に
指示します。明示的に停止するよう伝えるまで、あるいは永遠に実行されます。
例として、loops ディレクトリ内の src/main.rs ファイルを次のように変更して ください:
ファイル名: src/main.rs
fn main() {
loop {
println!("again!");
}
}
このプログラムを実行すると、手動で停止するまで again! が途切れることなく
繰り返し表示されます。ほとんどのターミナルは、ループし続けている
プログラムを中断するためのキーボードショートカット
ctrl-C をサポートしています。試してみてください:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
記号 ^C は、ctrl-C を押した位置を表しています。
割り込みシグナルを受け取ったときにコードがループのどこを実行していたかに
よっては、^C の後に again! が表示されることもあれば、表示されないこともあります。
幸い、Rust にはコードを使ってループを抜ける方法もあります。ループの中に
break キーワードを置くことで、いつループの実行を停止するかをプログラムに
指示できます。第 2 章の
「正しい予想をした後に終了する」 節で、ユーザーが正しい数を当ててゲームに勝ったときにプログラムを終了するために
これを行ったことを思い出してください。
推測ゲームでは continue も使いました。これはループ内で、その反復にある
残りのコードをスキップして、次の反復へ進むようプログラムに指示します。
ループから値を返す
loop の用途のひとつは、スレッドが処理を完了したかどうかを確認する場合の
ように、失敗する可能性があると分かっている操作を再試行することです。また、
その操作の結果をループの外に出して、コードの残りの部分へ渡す必要があるかも
しれません。これを行うには、ループを停止するために使う break 式の後に
返したい値を追加します。その値はループの外へ返されるので、次のように
利用できます:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
ループの前で、counter という名前の変数を宣言し、0 で初期化します。
次に、ループから返される値を保持するための result という名前の変数を宣言します。
ループの各反復で、counter 変数に 1 を加え、その後 counter が 10 と
等しいかどうかを確認します。そうなったら、counter * 2 という値とともに
break キーワードを使います。ループの後では、result に値を代入する文を
セミコロンで終えます。最後に、result の値を表示します。この場合は 20 です。
ループの内側から return することもできます。break は現在のループだけを
抜けますが、return は常に現在の関数を抜けます。
ループラベルで対象を明確にする
ループの中にさらにループがある場合、break と continue はその位置で最も
内側にあるループに適用されます。必要に応じて、ループに_ループラベル_を
指定できます。そうすると、そのラベルを break や continue と一緒に使って、
それらのキーワードが最も内側のループではなく、ラベル付きのループに適用
されることを指定できます。ループラベルはシングルクォートで始める必要が
あります。次は 2 つの入れ子になったループの例です:
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
外側のループには 'counting_up というラベルがあり、0 から 2 まで数え上げます。
ラベルのない内側のループは 10 から 9 まで数え下げます。ラベルを指定しない
最初の break は内側のループだけを抜けます。break 'counting_up; 文は外側のループを抜けます。このコードは次を表示します:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
while を使った条件付きループの簡潔化
プログラムでは、ループ内で条件を評価する必要が生じることがよくあります。
条件が true である間はループが実行されます。条件が true でなくなると、
プログラムは break を実行してループを停止します。このような振る舞いは、
loop、if、else、break を組み合わせても実装できます。望むなら、
今ここでプログラムとして試してみてもよいでしょう。しかし、このパターンは
非常によく使われるため、Rust には while ループと呼ばれる組み込みの言語構文が
あります。リスト 3-3 では、while を使ってプログラムを 3 回繰り返し、
毎回カウントダウンし、その後ループの後でメッセージを表示して終了します。
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
この構文により、loop、if、else、break を使う場合に必要になる多くの
入れ子構造が不要になり、より明確になります。条件が true と評価されている
間はコードが実行され、それ以外の場合はループを抜けます。
for でコレクションを反復処理する
配列のようなコレクションの要素を順に処理するために、while 構文を使うことも
できます。たとえば、リスト 3-4 のループは配列 a の各要素を表示します。
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index += 1;
}
}
ここでは、コードは配列の要素を順に数え上げながら進みます。インデックス
0 から始まり、配列の最後のインデックスに達するまでループします(つまり、
index < 5 がもはや true でなくなるまでです)。このコードを実行すると、
配列内のすべての要素が表示されます:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50
予想どおり、5 つの配列の値がすべてターミナルに表示されます。index
はいつか 5 になりますが、ループは配列から 6 つ目の値を取得しようとする前に
実行を停止します。
しかし、このアプローチはエラーを起こしやすく、インデックス値やテスト条件が正しくない場合、プログラムをパニックさせてしまう可能性があります。たとえば、a 配列の定義を4要素に変更したにもかかわらず、条件を while index < 4 に更新し忘れた場合、このコードはパニックします。また、これには速度面の問題もあります。というのも、コンパイラはループの各反復ごとに、インデックスが配列の境界内にあるかどうかを条件チェックするための実行時コードを追加するからです。
より簡潔な代替手段として、for ループを使い、コレクション内の各要素に対してコードを実行できます。for ループはリスト3-5のコードのようになります。
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
このコードを実行すると、リスト3-4と同じ出力が表示されます。さらに重要なのは、コードの安全性が向上し、配列の末尾を超えてしまったり、十分に進まずにいくつかの要素を見逃したりすることで生じるバグの可能性を排除できたことです。for ループから生成されるマシンコードは、各反復でインデックスを配列の長さと比較する必要がないため、より効率的になる場合もあります。
for ループを使えば、リスト3-4で使った方法とは異なり、配列内の値の個数を変更したときに、ほかのコードを変更し忘れる心配がありません。
for ループの安全性と簡潔さにより、これは Rust で最も一般的に使われるループ構文になっています。リスト3-3で while ループを使ったカウントダウンの例のように、あるコードを一定回数実行したい状況であっても、たいていの Rustacean は for ループを使うでしょう。その方法は、標準ライブラリが提供する Range を使うことです。これは、ある数から始まり、別の数の直前で終わるまでのすべての数を順に生成します。
for ループと、まだ説明していない別のメソッドである rev を使って範囲を逆順にすると、カウントダウンは次のようになります。
ファイル名: src/main.rs
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
このコードのほうが、少し見やすいでしょう?
まとめ
やり切りました! かなり大きな章でした。変数、スカラー型と複合型のデータ型、関数、コメント、if 式、そしてループについて学びました! この章で取り上げた概念を練習するために、次のことを行うプログラムを作ってみてください。
- 華氏と摂氏の間で温度を変換する。
- フィボナッチ数列の第 n 項を生成する。
- クリスマスキャロル “The Twelve Days of Christmas” の歌詞を出力する。その際、歌の繰り返しを活用すること。
先に進む準備ができたら、Rust における、他のプログラミング言語には一般的には 存在しない 概念、所有権について説明します。
所有権を理解する
所有権は Rust の最も独自性の高い機能であり、言語の他の部分にも 深い影響を及ぼします。これにより、Rust はガベージコレクタを必要と せずにメモリ安全性を保証できます。そのため、所有権がどのように機能 するのかを理解することが重要です。この章では、所有権に加えて、借用、 スライス、そして Rust がメモリ内でデータをどのように配置するかという、 関連するいくつかの機能についても説明します。
所有権とは何か?
所有権とは何か?
所有権 とは、Rust プログラムがどのようにメモリを管理するかを規定する一連のルールです。 すべてのプログラムは、実行中にコンピュータのメモリをどのように使うかを管理しなければなりません。 言語によっては、プログラムの実行中に、もはや使われていない メモリを定期的に探すガベージコレクションを備えています。別の言語では、プログラマーが明示的に メモリを確保し、解放しなければなりません。Rust は第三のアプローチを採用しています。つまり、メモリは コンパイラが検査する一連のルールを持つ所有権システムを通じて管理されます。これらのルールの いずれかに違反すると、プログラムはコンパイルされません。所有権の機能はいずれも、 実行中のプログラムを遅くすることはありません。
所有権は多くのプログラマーにとって新しい概念であるため、慣れるまでには少し時間がかかります。 朗報なのは、Rust と所有権システムのルールに習熟すればするほど、 安全で効率的なコードを自然に書けるようになることです。ぜひ取り組み続けてください!
所有権を理解すると、Rust を独自のものにしている機能を理解するための しっかりした土台が得られます。この章では、非常によく使われるデータ構造である 文字列に焦点を当てたいくつかの例を通して、所有権を学んでいきます。
スタックとヒープ
多くのプログラミング言語では、スタックや ヒープについて頻繁に考える必要はありません。しかし、Rust のようなシステムプログラミング言語では、値が スタック上にあるのかヒープ上にあるのかが、言語の振る舞いや、 なぜ特定の判断をしなければならないのかに影響します。この章の後半では、所有権の一部を スタックとヒープとの関係の中で説明するので、その準備として ここで簡単に説明しておきます。
スタックとヒープはどちらも、実行時にコードが利用できるメモリの一部ですが、 その構造は異なります。スタックは、 値を受け取った順に格納し、取り出すときはその逆順に取り出します。 これは 後入れ先出し(LIFO) と呼ばれます。皿の 積み重ねを考えてみてください。皿を追加するときは山の一番上に置き、 皿が必要なときは一番上から 1 枚取ります。真ん中や下から 皿を追加したり取り出したりしても、うまくいきません! データを追加することは スタックにプッシュすること と呼ばれ、 データを取り除くことは スタックからポップすること と呼ばれます。スタックに格納される すべてのデータは、既知の固定サイズでなければなりません。コンパイル時点でサイズが不明なデータや、 サイズが変わる可能性のあるデータは、代わりにヒープに格納しなければなりません。
ヒープはあまり整理されていません。ヒープにデータを置くときは、 一定量の領域を要求します。メモリアロケータは、ヒープの中から 十分な大きさの空いている場所を見つけ、それを使用中としてマークし、その場所のアドレスである ポインタ を返します。この処理は ヒープへの割り当て と呼ばれ、 単に 割り当て と略されることもあります(スタックに値をプッシュすることは 割り当てとは見なされません)。ヒープへのポインタは既知の固定サイズなので、 そのポインタ自体はスタックに格納できますが、実際のデータが欲しい場合は、 そのポインタをたどらなければなりません。レストランで席に案内される場面を 考えてみてください。店に入ると、グループの人数を伝え、 案内係が全員が座れる空いているテーブルを見つけて、そこへ案内してくれます。 グループの誰かが遅れて来た場合、その人はあなたたちがどこに案内されたかを聞いて、 あなたたちを見つけることができます。
スタックへのプッシュがヒープへの割り当てよりも速いのは、 アロケータが新しいデータを格納する場所を探す必要がまったくなく、その場所が 常にスタックの先頭だからです。対照的に、ヒープに領域を割り当てるには より多くの作業が必要です。というのも、アロケータはまずデータを保持するのに十分な大きさの 領域を見つけ、その後、次の 割り当てに備えるための管理処理を行わなければならないからです。
ヒープ上のデータへのアクセスは、一般にスタック上のデータへのアクセスよりも遅くなります。 そこに到達するにはポインタをたどらなければならないからです。現代の プロセッサは、メモリ上を飛び回ることが少ないほど高速に動作します。 このたとえを続けると、レストランの給仕が複数のテーブルから注文を取る場面を考えてください。 1 つのテーブルの注文をすべて取ってから次のテーブルへ移るのが、 最も効率的です。テーブル A の注文を取り、次にテーブル B の注文を取り、 さらに আবার A、その後また B というように進めるのは、はるかに遅い 処理になります。同じように、プロセッサも、ほかのデータに近い場所にあるデータ (スタック上のように)を扱うほうが、遠く離れたデータ (ヒープ上にあり得るように)を扱うよりも、たいていうまく仕事ができます。
コードが関数を呼び出すと、関数に渡された値 (場合によってはヒープ上のデータへのポインタも含みます)と、その関数の ローカル変数はスタックにプッシュされます。関数の処理が終わると、それらの 値はスタックからポップされます。
コードのどの部分がヒープ上のどのデータを使っているかを追跡すること、 ヒープ上の重複データの量を最小限にすること、そして 領域不足にならないようヒープ上の未使用データを片づけることは、 いずれも所有権が扱う問題です。いったん所有権を理解すれば、 スタックやヒープについて頻繁に考える必要はなくなります。 しかし、所有権の主な目的がヒープデータを管理することだと知っておくと、 所有権がそのように機能する理由を理解する助けになります。
所有権のルール
まず、所有権のルールを見てみましょう。これらを説明する例を 見ていく間、次のルールを念頭に置いてください。
- Rust の各値には 所有者 がいます。
- ある時点で所有者になれるのは 1 つだけです。
- 所有者がスコープを外れると、その値はドロップされます。
変数のスコープ
基本的な Rust 構文はすでに終えたので、これ以降の例では fn main() {
のコードをすべて含めません。したがって、もし手元で試しながら読み進めているなら、
次の例を手動で main 関数の中に入れるようにしてください。その結果、
例は少し簡潔になり、定型的なコードではなく実際の詳細に集中できます。
所有権の最初の例として、いくつかの変数のスコープを見ていきます。スコープ とは、 ある項目が有効であるプログラム内の範囲のことです。次の 変数を見てください。
#![allow(unused)]
fn main() {
let s = "hello";
}
変数 s は文字列リテラルを参照しており、その文字列の値は
プログラムのテキスト内にハードコードされています。この変数は、宣言された時点から
現在のスコープの終わりまで有効です。リスト 4-1 は、
変数 s がどこで有効になるかをコメントで注釈したプログラムを示しています。
fn main() {
{ // s is not valid here, since it's not yet declared
let s = "hello"; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
}
言い換えると、ここでは 2 つの重要な時点があります。
sがスコープ 内に入る と、有効になります。- スコープ 外に出る まで、有効なままです。
この時点では、スコープと変数が有効であるタイミングとの関係は、
ほかのプログラミング言語と似ています。ここからさらに理解を積み上げるために、
String 型を導入します。
String 型
所有権のルールを説明するには、第3章の「データ型」節で扱ったものよりも複雑なデータ型が必要です。これまでに扱った型はサイズが既知で、スタックに格納でき、スコープが終わるとスタックからポップされます。また、コードの別の部分が別のスコープで同じ値を使う必要がある場合でも、新しい独立したインスタンスを作るために、すばやく簡単にコピーできます。しかし、ここではヒープに格納されるデータを見て、Rustがいつそのデータを解放すべきかをどのように知るのかを探りたいので、String型はそのよい例です。
ここでは、所有権に関係する String の部分に集中します。これらの側面は、標準ライブラリが提供するものでも、自分で作成したものでも、ほかの複雑なデータ型にも当てはまります。所有権に関係しない String の側面については、第8章で説明します。
文字列リテラルについてはすでに見てきました。これは、文字列値がプログラム内にハードコードされているものです。文字列リテラルは便利ですが、テキストを使いたいあらゆる状況に適しているわけではありません。理由の1つは、それらがイミュータブルであることです。もう1つは、コードを書く時点ですべての文字列値がわかっているとは限らないことです。たとえば、ユーザー入力を受け取って保存したい場合はどうでしょうか。こうした状況のために、Rustには String 型があります。この型はヒープに確保されたデータを管理するため、コンパイル時には未知の量のテキストを格納できます。次のように、文字列リテラルから from 関数を使って String を作成できます。
#![allow(unused)]
fn main() {
let s = String::from("hello");
}
二重コロン :: 演算子を使うと、この特定の from 関数を string_from のような名前にするのではなく、String 型の名前空間の下に置けます。この構文については、第5章の「メソッド」節でさらに説明し、第7章の「モジュールツリーの要素を参照するためのパス」でモジュールによる名前空間分けについて話すときにも触れます。
この種の文字列は 変更できます:
fn main() {
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{s}"); // this will print `hello, world!`
}
では、ここでの違いは何でしょうか。なぜ String は変更できるのにリテラルは変更できないのでしょうか。違いは、この2つの型がメモリをどのように扱うかにあります。
メモリと割り当て
文字列リテラルの場合、その内容はコンパイル時にわかっているので、テキストは最終的な実行可能ファイルに直接ハードコードされます。これが、文字列リテラルが高速かつ効率的である理由です。しかし、これらの性質は文字列リテラルがイミュータブルであることによってのみ成り立っています。残念ながら、コンパイル時にはサイズが不明で、プログラム実行中にサイズが変わるかもしれないテキストの断片ごとに、メモリの塊をバイナリに埋め込むことはできません。
String 型では、変更可能で伸長可能なテキスト片をサポートするために、その内容を保持するための、コンパイル時には不明な量のメモリをヒープ上に確保する必要があります。これは次のことを意味します。
- そのメモリは実行時にメモリアロケータへ要求しなければなりません。
Stringを使い終えたときに、このメモリをアロケータへ返す方法が必要です。
最初の部分は私たちが行います。String::from を呼び出すと、その実装が必要なメモリを要求します。これは、プログラミング言語ではほぼ普遍的なことです。
しかし、2つ目の部分は異なります。ガベージコレクタ (GC) のある言語では、GCがもう使われていないメモリを追跡して片付けてくれるため、私たちはそれを気にする必要がありません。GCのないほとんどの言語では、メモリがもう使われていないことを見極めて、それを明示的に解放するコードを呼び出すのは私たちの責任です。これは、メモリを要求したときと同じです。これを正しく行うことは、歴史的に難しいプログラミング上の問題でした。忘れればメモリを浪費します。早すぎれば無効な変数ができてしまいます。2回行えば、それもバグです。ちょうど1回の allocate に対して、ちょうど1回の free を対応させる必要があります。
Rustは異なる道を取ります。所有している変数がスコープを抜けると、そのメモリは自動的に返されます。文字列リテラルの代わりに String を使った、リスト4-1のスコープの例のバージョンを示します。
fn main() {
{
let s = String::from("hello"); // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no
// longer valid
}
String が必要とするメモリをアロケータに返せる自然なタイミングがあります。それは、s がスコープを抜けるときです。変数がスコープを抜けると、Rustは特別な関数を呼び出してくれます。この関数は drop と呼ばれ、String の作者はここにメモリを返すコードを書けます。Rustは閉じ波括弧で自動的に drop を呼び出します。
注: C++では、オブジェクトのライフタイムの終わりにリソースを解放するこのパターンは、Resource Acquisition Is Initialization (RAII) と呼ばれることがあります。 RAIIパターンを使ったことがあるなら、Rustの
drop関数にはなじみがあるでしょう。
このパターンは、Rustコードの書かれ方に深い影響を与えます。今は単純に見えるかもしれませんが、ヒープに確保したデータを複数の変数で使いたい、より複雑な状況では、コードの振る舞いが予想外に思えることがあります。では、そのような状況をいくつか見ていきましょう。
ムーブによる変数とデータの相互作用
Rustでは、複数の変数が同じデータとさまざまな方法で相互作用できます。リスト4-2は、整数を使った例を示しています。
fn main() {
let x = 5;
let y = x;
}
おそらく、これが何をしているかは想像できるでしょう。すなわち、「値 5 を x に束縛し、その後、x の値のコピーを作って y に束縛する」ということです。これで、x と y という2つの変数があり、どちらも 5 に等しくなります。実際、そのとおりのことが起きています。整数はサイズが既知で固定された単純な値であり、この2つの 5 の値はスタックに積まれるからです。
では、String の版を見てみましょう。
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}
これはとてもよく似ているので、動作も同じだと思うかもしれません。つまり、2行目で s1 の値のコピーが作られ、それが s2 に束縛される、ということです。しかし、実際には少し違います。
String の内部で何が起きているのかを図4-1で見てみましょう。String は左側に示されている3つの部分から成ります。すなわち、文字列の内容を保持するメモリへのポインタ、長さ、容量です。このデータのまとまりはスタックに格納されます。右側は、その内容を保持するヒープ上のメモリです。
図4-1:
s1 に束縛された値 "hello" を保持する String のメモリ内表現
長さは、String の内容が現在使用しているメモリ量をバイト単位で表したものです。容量は、String がアロケータから受け取ったメモリの総量をバイト単位で表したものです。長さと容量の違いは重要ですが、この文脈では重要ではないので、今のところ容量は無視してかまいません。
s1 を s2 に代入すると、String のデータがコピーされます。つまり、スタック上にあるポインタ、長さ、容量をコピーします。ポインタが参照しているヒープ上のデータはコピーしません。言い換えると、メモリ内のデータ表現は図4-2のようになります。
図4-2: s1 のポインタ、長さ、容量のコピーを持つ変数
s2 のメモリ内表現
この表現は図4-3のようには なりません 。図4-3は、Rust が代わりにヒープ上のデータもコピーした場合にメモリがどう見えるかを示したものです。もし Rust がそうしていたら、ヒープ上のデータが大きい場合、s2 = s1 という操作は実行時性能の面で非常に高コストになり得ます。
図4-3: Rust がヒープ上のデータもコピーした場合に s2 = s1 が
行う可能性のある別の動作
先ほど、変数がスコープを抜けると、Rust は自動的に drop 関数を呼び出し、その変数のヒープメモリを片付けると述べました。しかし、図4-2では両方のデータポインタが同じ場所を指しています。これは問題です。s2 と s1 がスコープを抜けると、両方が同じメモリを解放しようとするからです。これは 二重解放 エラーとして知られており、先ほど触れたメモリ安全性バグの1つです。メモリを2回解放するとメモリ破壊につながる可能性があり、それがセキュリティ上の脆弱性につながるおそれがあります。
メモリ安全性を確保するために、let s2 = s1; という行の後、Rust は s1 をもはや有効ではないとみなします。したがって、s1 がスコープを抜けるときに Rust は何も解放する必要がありません。s2 が作成された後で s1 を使おうとするとどうなるか見てみましょう。うまく動きません。
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
}
無効化された参照を使えないように Rust が防ぐため、次のようなエラーが出ます。
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:16
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{s1}, world!");
| ^^ 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
|
3 | let s2 = s1.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
他の言語を扱っているときに shallow copy と deep copy という用語を聞いたことがあるなら、データをコピーせずにポインタ、長さ、容量をコピーするという概念は、シャローコピーを行っているように聞こえるでしょう。しかし、Rust は最初の変数も無効化するため、これはシャローコピーと呼ばれるのではなく、ムーブ として知られています。この例では、s1 は s2 に ムーブされた と言います。つまり、実際に起きていることは図4-4に示されているとおりです。
図4-4: s1 が無効化された後のメモリ内表現
これで問題は解決です! 有効なのは s2 だけなので、これがスコープを抜けるときにそれだけがメモリを解放し、これで完了です。
さらに、ここから暗黙に示される設計上の選択があります。Rust はあなたのデータの「ディープ」コピーを自動的に作成することは決してありません。したがって、どのような 自動的な コピーも、実行時性能の面では低コストであると考えられます。
スコープと代入
これとは逆のことが、スコープ、所有権、そして drop 関数によるメモリ解放の関係についても当てはまります。既存の変数にまったく新しい値を代入すると、Rust は drop を呼び出し、元の値のメモリを即座に解放します。たとえば、次のコードを考えてみましょう。
fn main() {
let mut s = String::from("hello");
s = String::from("ahoy");
println!("{s}, world!");
}
最初に、値 "hello" を持つ String に束縛された変数 s を宣言します。次に、すぐに値 "ahoy" を持つ新しい String を作成し、それを s に代入します。この時点では、ヒープ上の元の値を参照しているものは何もありません。図4-5は、この時点のスタックとヒープのデータを示しています。
図4-5: 初期値が完全に置き換えられた後のメモリ内表現
したがって、元の文字列は即座にスコープを抜けます。Rust はそれに対して drop 関数を実行し、そのメモリはただちに解放されます。最後に値を表示すると、それは "ahoy, world!" になります。
Clone と相互作用する変数とデータ
String のヒープデータを、スタックデータだけでなく深くコピーしたい 場合は 、clone という一般的なメソッドを使えます。メソッド構文については第5章で説明しますが、メソッドは多くのプログラミング言語に共通する機能なので、おそらく以前に見たことがあるでしょう。
以下は、clone メソッドが動作している例です。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {s1}, s2 = {s2}");
}
これはまったく問題なく動作し、ヒープデータが 実際に コピーされる図4-3の挙動を明示的に生み出します。
clone の呼び出しを見ると、何らかの任意のコードが実行され、そのコードは高コストである可能性があるとわかります。これは、何か異なることが起きていることを視覚的に示す指標です。
スタックのみのデータ: Copy
まだ触れていない別のひとひねりがあります。整数を使ったこのコード――その一部はリスト4-2で示しました――は動作し、有効です。
fn main() {
let x = 5;
let y = x;
println!("x = {x}, y = {y}");
}
しかし、このコードは今学んだことと矛盾しているように見えます。clone の呼び出しはありませんが、x は依然として有効で、y にムーブされてもいません。
その理由は、コンパイル時にサイズが既知である整数のような型は、
実際の値全体がスタックに格納されるため、それらのコピーをすばやく
作成できるからです。つまり、変数 y を作成したあとで x が有効で
なくなるのを防ぎたい理由はありません。言い換えると、ここではディープ
コピーとシャローコピーの違いはないので、clone を呼び出しても通常の
シャローコピーと何も変わらず、それを省略できます。
Rust には Copy トレイトと呼ばれる特別な注釈があり、整数のように
スタックに格納される型にこれを付けることができます(トレイトについては
第10章 で詳しく説明します)。ある型が Copy
トレイトを実装している場合、その型を使う変数はムーブされず、単純に
コピーされるため、別の変数への代入後も引き続き有効です。
型またはその一部が Drop トレイトを実装している場合、Rust はその型に
Copy を付けることを許しません。値がスコープを外れるときに何らかの
特別な処理が必要な型に Copy 注釈を追加すると、コンパイル時エラーが
発生します。型に Copy 注釈を追加してこのトレイトを実装する方法については、
付録 C の 「導出可能なトレイト」 を
参照してください。
では、どのような型が Copy トレイトを実装しているのでしょうか。確実を
期すには対象の型のドキュメントを確認できますが、一般的な規則としては、
単純なスカラー値の集まりは Copy を実装でき、メモリ割り当てを必要とする
ものや何らかのリソースであるものは Copy を実装できません。以下は
Copy を実装している型の一部です。
u32など、すべての整数型。- 値
trueとfalseを持つ論理値型bool。 f64など、すべての浮動小数点数型。- 文字型
char。 - タプル。ただし、含まれる要素がすべて
Copyを実装している場合に限ります。 たとえば、(i32, i32)はCopyを実装しますが、(i32, String)は 実装しません。
所有権と関数
値を関数に渡す仕組みは、値を変数に代入するときの仕組みと似ています。 変数を関数に渡すと、代入の場合と同様にムーブまたはコピーが行われます。 リスト 4-3 には、変数がスコープに入る場所と外れる場所を示す注釈付きの 例があります。
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // Because i32 implements the Copy trait,
// x does NOT move into the function,
// so it's okay to use x afterward.
} // Here, x goes out of scope, then s. However, because s's value was moved,
// nothing special happens.
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{some_string}");
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{some_integer}");
} // Here, some_integer goes out of scope. Nothing special happens.
takes_ownership の呼び出し後に s を使おうとすると、Rust は
コンパイル時エラーを発生させます。これらの静的検査は、私たちをミスから
守ってくれます。s と x を使うコードを main に追加して、どこで
使えるのか、また所有権のルールによってどこで使えなくなるのかを確認して
みてください。
戻り値とスコープ
値を返すことでも所有権は移動します。リスト 4-4 は、何らかの値を返す 関数の例を示しており、リスト 4-3 と同様の注釈が付いています。
fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1
let s2 = String::from("hello"); // s2 comes into scope
let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.
fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it
let some_string = String::from("yours"); // some_string comes into scope
some_string // some_string is returned and
// moves out to the calling
// function
}
// This function takes a String and returns a String.
fn takes_and_gives_back(a_string: String) -> String {
// a_string comes into
// scope
a_string // a_string is returned and moves out to the calling function
}
変数の所有権は、毎回同じパターンに従います。ある値を別の変数に代入すると、
その値はムーブされます。ヒープ上のデータを含む変数がスコープを外れると、
そのデータの所有権が別の変数にムーブされていない限り、その値は drop に
よってクリーンアップされます。
これは機能しますが、すべての関数で所有権を受け取っては返すというのは、 少し面倒です。関数に値を使わせたいだけで、所有権は奪わせたくない場合は どうでしょうか。さらに、渡したものをもう一度使いたいなら返してもらう必要が あるうえに、関数本体の結果として返したいデータも別にあるかもしれないので、 かなり煩わしいです。
Rust では、リスト 4-5 に示すように、タプルを使って複数の値を返すことが できます。
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{s2}' is {len}.");
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String
(s, length)
}
しかし、これは本来よくあるはずの概念に対して、儀式的すぎて手間がかかり すぎます。幸いなことに、Rust には所有権を移動せずに値を使うための機能が あります。それが参照です。
参照と借用
参照と借用
Listing 4-5 のタプルコードの問題は、String が calculate_length にムーブされるため、calculate_length の呼び出し後も String を使い続けられるように、呼び出し元の関数に String を返さなければならないことです。代わりに、String の値への参照を渡すことができます。参照はポインタに似ており、そのアドレスに格納されているデータへアクセスするためにたどれるアドレスです。そのデータは他の変数に所有されています。ポインタとは異なり、参照は、その参照の存続期間中、特定の型の有効な値を必ず指すことが保証されています。
値の所有権を受け取る代わりに、オブジェクトへの参照を引数として受け取る calculate_length 関数は、次のように定義して使います。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
まず、変数宣言と関数の戻り値にあったタプルのコードがすべてなくなっていることに注目してください。次に、calculate_length に &s1 を渡し、その定義では String ではなく &String を受け取っていることにも注目してください。これらのアンパサンドは参照を表しており、所有権を受け取らずに何らかの値を参照できるようにします。図4-6はこの概念を示しています。
図4-6: String s1 を指す &String s の図
注:
&を使った参照の反対は 参照外し で、参照外し演算子*で行います。参照外し演算子のいくつかの使い方は第8章で見て、参照外しの詳細は第15章で議論します。
ここでの関数呼び出しを、もう少し詳しく見てみましょう。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
&s1 という構文により、s1 の値を 参照する けれども所有はしない参照を作れます。参照は所有権を持たないので、それが指している値は、参照が使われなくなってもドロップされません。
同様に、関数シグネチャでは & を使って、引数 s の型が参照であることを示しています。説明の注釈をいくつか加えてみましょう。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
// it refers to, the String is not dropped.
変数 s が有効なスコープは、他のどの関数引数のスコープとも同じです。しかし、s は所有権を持っていないため、参照が指している値は s が使われなくなってもドロップされません。関数が実際の値ではなく参照を引数として受け取る場合、所有権を返すために値を返す必要はありません。そもそも所有権を受け取っていないからです。
参照を作るこの動作を 借用 と呼びます。現実世界と同じように、誰かが何かを所有しているなら、それを借りることができます。使い終わったら返さなければなりません。あなたのものではないのです。
では、借用しているものを変更しようとするとどうなるでしょうか。Listing 4-6 のコードを試してみてください。ネタバレすると、うまくいきません!
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
エラーは次のとおりです。
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn change(some_string: &mut String) {
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
変数がデフォルトで不変であるのと同じように、参照もデフォルトで不変です。参照しているものを変更することは許されません。
可変参照
Listing 4-6 のコードは、代わりに 可変参照 を使うようにいくつか小さな修正を加えるだけで、借用した値を変更できるように直せます。
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
まず、s を mut に変更します。次に、change 関数を呼び出す箇所で &mut s を使って可変参照を作り、関数シグネチャも some_string: &mut String として可変参照を受け取るように更新します。これにより、change 関数が借用した値を変更することが非常に明確になります。
可変参照には大きな制約が1つあります。ある値への可変参照がある場合、その値への他の参照を持つことはできません。s への可変参照を2つ作ろうとする次のコードは失敗します。
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
}
エラーは次のとおりです。
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{r1}, {r2}");
| -- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
このエラーは、このコードが無効であることを示しています。なぜなら、一度に2回以上 s を可変として借用することはできないからです。最初の可変借用は r1 にあり、それは println! で使われるまで継続しなければなりません。しかし、その可変参照を作ってから実際に使うまでの間に、r1 と同じデータを借用する別の可変参照を r2 に作ろうとしました。
同じデータに対する複数の可変参照を同時に禁止するこの制約により、変更は可能でありながらも、非常に制御された形でしか行えません。ほとんどの言語では好きなときに変更できるため、これは Rust を始めたばかりの Rustacean が苦労する点です。この制約の利点は、Rust がデータ競合をコンパイル時に防げることです。データ競合 は競合状態に似たもので、次の3つの振る舞いが起きると発生します。
- 2つ以上のポインタが同時に同じデータへアクセスしている。
- そのポインタの少なくとも1つが、データへの書き込みに使われている。
- データへのアクセスを同期する仕組みが使われていない。
データ競合は未定義動作を引き起こし、実行時に追跡して診断・修正するのは難しいことがあります。Rust はデータ競合のあるコードのコンパイルを拒否することで、この問題を防ぎます!
いつものように、中かっこを使って新しいスコープを作れば、複数の可変参照を使えます。ただし、同時に ではありません。
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
}
Rust は、可変参照と不変参照を組み合わせる場合にも同様のルールを強制します。このコードはエラーになります。
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{r1}, {r2}, and {r3}");
}
エラーは次のとおりです。
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{r1}, {r2}, and {r3}");
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
ふう! 同じ値への不変参照がある間は、やはり 可変参照を持つこともできません。 不変参照を使っている人は、その値がいつの間にか突然変わってしまうとは思っていません! しかし、複数の不変参照は許されています。データを読み取っているだけの人は、ほかの人のデータの読み取りに影響を与えることができないからです。
参照のスコープは、その参照が導入された場所から始まり、その参照が最後に使用されるところまで続くことに注意してください。たとえば、次のコードはコンパイルできます。というのも、不変参照が最後に使われるのは println! の中であり、その後に可変参照が導入されるからです。
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// Variables r1 and r2 will not be used after this point.
let r3 = &mut s; // no problem
println!("{r3}");
}
不変参照 r1 と r2 のスコープは、それらが最後に使われる println! の後で終わり、これは可変参照 r3 が作成される前です。これらのスコープは重なっていないため、このコードは許可されます。コンパイラは、スコープの終わりより前の時点で、その参照がもはや使われていないことを判断できます。
借用エラーはときどきイライラさせられるかもしれませんが、それは Rust コンパイラが潜在的なバグを早い段階で(実行時ではなくコンパイル時に)指摘し、問題がどこにあるのかを正確に示してくれているのだと覚えておいてください。そうすれば、なぜデータが自分の思っていたものと違っていたのか、その原因を追跡する必要はありません。
ダングリング参照
ポインタを持つ言語では、そのメモリへのポインタを保持したままメモリを解放することで、誤って ダングリングポインタ を簡単に作ってしまいます。これは、すでにほかの誰かに渡されているかもしれないメモリ上の位置を参照するポインタです。これに対して Rust では、コンパイラが参照が決してダングリング参照にならないことを保証します。あるデータへの参照があるなら、コンパイラは、そのデータへの参照より先にそのデータがスコープを抜けないようにします。
コンパイル時エラーによって Rust がどのようにダングリング参照を防ぐのかを見るために、実際にダングリング参照を作ってみましょう。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
エラーは次のとおりです。
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
5 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
このエラーメッセージは、まだ扱っていない機能であるライフタイムに言及しています。ライフタイムについては第 10 章で詳しく説明します。しかし、ライフタイムに関する部分をひとまず脇に置いても、このメッセージには、なぜこのコードが問題なのかを示す鍵が含まれています。
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
dangle のコードの各段階で何が起きているのかを、もう少し詳しく見てみましょう。
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope and is dropped, so its memory goes away.
// Danger!
s は dangle の内部で作成されるため、dangle のコードが終わると、s は解放されます。しかし、私たちはそれへの参照を返そうとしました。つまり、この参照は無効な String を指すことになります。これはだめです! Rust はこれを許しません。
ここでの解決策は、String を直接返すことです。
fn main() {
let string = no_dangle();
}
fn no_dangle() -> String {
let s = String::from("hello");
s
}
これは何の問題もなく動作します。所有権がムーブされ、何も解放されません。
参照のルール
ここで、参照についてこれまでに説明したことを振り返りましょう。
- 任意の時点で、持てるのは 1 つの可変参照 か 任意個の不変参照 のどちらか一方です。
- 参照は常に有効でなければなりません。
次に、別の種類の参照であるスライスを見ていきます。
スライス型
スライス型
スライス を使うと、コレクション 内の要素の連続した並びを参照できます。スライスは参照の一種なので、所有権を持ちません。
ここで、小さなプログラミング上の問題を考えてみましょう。空白で区切られた単語からなる文字列を受け取り、その文字列の中で最初に見つかった単語を返す関数を書いてください。関数が文字列内に空白を見つけられない場合、その文字列全体が 1 つの単語でなければならないので、文字列全体を返すべきです。
注: スライスを導入する目的上、この節では ASCII のみを前提にしています。UTF-8 の扱いについてのより詳しい説明は、第 8 章の 「文字列で UTF-8 エンコードされたテキストを保持する」 節にあります。
スライスが解決する問題を理解するために、まずはスライスを使わずにこの関数のシグネチャをどのように書くかを順に見ていきましょう。
fn first_word(s: &String) -> ?
first_word 関数は、型 &String の引数を持っています。所有権は必要ないので、これは問題ありません。(Rust の慣用的な書き方では、必要でない限り関数は引数の所有権を受け取りません。その理由は、この先を読み進めるにつれて明らかになります。)しかし、何を返せばよいのでしょうか。文字列の 一部 について語る方法は、実のところありません。しかし、空白によって示される単語の終端のインデックスを返すことはできます。リスト 4-7 に示すように、それを試してみましょう。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
String を要素ごとにたどって、その値が空白かどうかを調べる必要があるため、as_bytes メソッドを使って String をバイト配列に変換します。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
次に、iter メソッドを使ってそのバイト配列に対するイテレータを作成します。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
イテレータについては 第 13 章 でより詳しく説明します。今のところは、iter はコレクション内の各要素を返すメソッドであり、enumerate は iter の結果をラップして、各要素をタプルの一部として返すものだと知っておいてください。enumerate から返されるタプルの最初の要素はインデックスで、2 番目の要素はその要素への参照です。これは、自分でインデックスを計算するより少し便利です。
enumerate メソッドはタプルを返すので、そのタプルを分解するためにパターンを使えます。パターンについては 第 6 章 でさらに説明します。for ループでは、タプル中のインデックスに i、単一バイトに &item を対応させるパターンを指定しています。.iter().enumerate() から得られるのは要素への参照なので、パターン内で & を使います。
for ループの中では、バイトリテラル構文を使って空白を表すバイトを探します。空白が見つかったら、その位置を返します。そうでなければ、s.len() を使って文字列の長さを返します。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
これで、文字列内の最初の単語の終端のインデックスを見つける方法は手に入りました。しかし、問題があります。usize を単独で返していますが、それは &String という文脈でのみ意味を持つ数値です。言い換えると、String とは別の値であるため、将来にわたってそれが有効であり続ける保証はありません。リスト 4-7 の first_word 関数を使う、リスト 4-8 のプログラムを考えてみてください。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word will get the value 5
s.clear(); // this empties the String, making it equal to ""
// word still has the value 5 here, but s no longer has any content that we
// could meaningfully use with the value 5, so word is now totally invalid!
}
このプログラムはエラーなしでコンパイルされ、s.clear() を呼び出した後に word を使ったとしても同様です。word は s の状態とまったく結び付いていないため、word には依然として値 5 が入っています。その値 5 を変数 s と組み合わせて最初の単語を取り出そうとすることもできてしまいますが、word に 5 を保存してから s の内容が変わっているので、これはバグになります。
word のインデックスが s のデータとずれてしまわないように気を配らなければならないのは、面倒でエラーが起きやすいことです。second_word 関数を書くなら、このようなインデックスの管理はさらに脆くなります。そのシグネチャは次のようになるでしょう。
fn second_word(s: &String) -> (usize, usize) {
今度は開始インデックス と 終了インデックスを追跡することになり、特定の状態のデータから計算されたにもかかわらず、その状態とはまったく結び付いていない値がさらに増えます。同期を保つ必要がある、互いに無関係な 3 つの変数がばらばらに存在することになります。
幸いなことに、Rust にはこの問題に対する解決策があります。文字列スライスです。
文字列スライス
文字列スライス は String の要素の連続した並びへの参照で、次のようになります。
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
}
String 全体への参照ではなく、hello は String の一部への参照であり、その範囲は追加の [0..5] の部分で指定されています。スライスは、角括弧内に範囲を指定することで作成します。[starting_index..ending_index] のように書きます。ここで、starting_index はスライス内の最初の位置であり、ending_index はスライス内の最後の位置の 1 つ後です。内部的には、スライスのデータ構造は開始位置とスライスの長さを保持しており、その長さは ending_index から starting_index を引いた値に対応します。したがって、let world = &s[6..11]; の場合、world は s のインデックス 6 のバイトへのポインタと、長さの値 5 を持つスライスになります。
図 4-7 はこれを図で示しています。
図 4-7: String の一部を参照する文字列スライス
Rust の .. 範囲構文では、インデックス 0 から始めたい場合、2 つのピリオドの前の値を省略できます。言い換えると、これらは同じです。
#![allow(unused)]
fn main() {
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
}
同様に、スライスに String の最後のバイトが含まれている場合は、末尾の数字を省略できます。つまり、これらは同じです。
#![allow(unused)]
fn main() {
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
}
文字列全体のスライスを取得するために、両方の値を省略することもできます。したがって、これらは同じです。
#![allow(unused)]
fn main() {
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
}
注: 文字列スライスの範囲インデックスは、有効な UTF-8 文字境界上になければなりません。 マルチバイト文字の途中で文字列スライスを作成しようとすると、 プログラムはエラーで終了します。
ここまでの情報を踏まえて、first_word を書き直し、スライスを返すようにしましょう。「文字列スライス」を表す型は &str と書きます:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {}
単語の終端のインデックスは、リスト 4-7 と同じように、最初に現れる空白を探すことで取得します。空白が見つかったら、文字列の先頭とその空白のインデックスを開始・終了インデックスとして使い、文字列スライスを返します。
これで first_word を呼び出すと、基になるデータに結び付いた単一の値が返ってきます。その値は、スライスの開始位置への参照と、スライス内の要素数で構成されます。
スライスを返す方法は、second_word 関数にも同様に使えます:
fn second_word(s: &String) -> &str {
これで、String 内への参照が有効なままであることをコンパイラが保証してくれるため、ずっと扱いやすく、間違えにくい API になりました。リスト 4-8 のプログラムで、最初の単語の終端インデックスを取得したあとに文字列を空にしてしまい、その結果インデックスが無効になったバグを覚えていますか? あのコードは論理的には誤っていましたが、すぐには何のエラーも示しませんでした。問題が表面化するのは、空にした文字列に対して最初の単語のインデックスを使い続けようとした、もっと後になってからです。スライスを使えばこのバグは起こりえず、コードに問題があることもずっと早い段階で分かります。first_word のスライス版を使うと、コンパイル時エラーが発生します:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {word}");
}
コンパイラエラーは次のとおりです:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:18:5
|
16 | let word = first_word(&s);
| -- immutable borrow occurs here
17 |
18 | s.clear(); // error!
| ^^^^^^^^^ mutable borrow occurs here
19 |
20 | println!("the first word is: {word}");
| ---- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
借用の規則から、何かへの不変参照がある場合、その対象への可変参照を同時に取得することはできない、と説明しました。clear は String を切り詰める必要があるため、可変参照を取得する必要があります。clear の呼び出し後にある println! は word に入っている参照を使うので、その時点では不変参照はまだ有効でなければなりません。Rust は、clear 内の可変参照と word 内の不変参照が同時に存在することを許さないため、コンパイルは失敗します。Rust は API を使いやすくしただけでなく、エラーの一群全体をコンパイル時に排除してくれたのです!
スライスとしての文字列リテラル
文字列リテラルがバイナリ内部に格納されることについてはすでに説明しました。スライスが分かった今なら、文字列リテラルを正しく理解できます:
#![allow(unused)]
fn main() {
let s = "Hello, world!";
}
ここでの s の型は &str です。これは、バイナリ内のその特定の位置を指すスライスです。文字列リテラルが不変である理由もこれです。&str は不変参照だからです。
引数としての文字列スライス
リテラルと String 値のどちらからもスライスを取れると分かったので、first_word にはもう 1 つ改善できる点があります。それがシグネチャです:
fn first_word(s: &String) -> &str {
経験を積んだ Rustacean なら、代わりにリスト 4-9 に示したシグネチャを書くでしょう。そうすれば、同じ関数を &String 値にも &str 値にも使えるからです。
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// `first_word` works on slices of `String`s, whether partial or whole.
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s.
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word` works on slices of string literals, whether partial or
// whole.
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
文字列スライスがあるなら、それをそのまま渡せます。String があるなら、String のスライスか String への参照を渡せます。この柔軟性は Deref 型強制を活用したものです。これは第 15 章の 「関数とメソッドで Deref 型強制を使う」 節で扱います。
関数が String への参照ではなく文字列スライスを受け取るように定義すると、機能を何も失うことなく、API をより汎用的で便利なものにできます:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// `first_word` works on slices of `String`s, whether partial or whole.
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s.
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word` works on slices of string literals, whether partial or
// whole.
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
その他のスライス
想像どおり、文字列スライスは文字列に特化したものです。しかし、もっと一般的なスライス型もあります。次の配列を考えてみましょう:
#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}
文字列の一部を参照したいのと同じように、配列の一部を参照したくなることもあります。その場合は次のようにします:
#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
}
このスライスの型は &[i32] です。これは文字列スライスと同じように、最初の要素への参照と長さを保持することで機能します。この種のスライスは、ほかのさまざまなコレクションでも使います。こうしたコレクションについては、第 8 章でベクタを扱うときに詳しく説明します。
まとめ
所有権、借用、そしてスライスという概念により、Rust プログラムではコンパイル時にメモリ安全性が保証されます。Rust 言語は、ほかのシステムプログラミング言語と同じように、メモリ使用を自分で制御する力を与えてくれます。しかし、データの所有者がスコープを外れたときに、そのデータが自動的にクリーンアップされるため、この制御を得るための余分なコードを書いたりデバッグしたりする必要はありません。
所有権は Rust のほかの多くの部分の動作にも影響するので、この本の残りを通して、これらの概念についてさらに取り上げていきます。第 5 章に進み、struct で複数のデータをひとまとめにする方法を見ていきましょう。
関連するデータを構造化するために構造体を使う
struct は structure とも呼ばれ、意味のあるひとまとまりを構成する複数の関連する値をまとめ、それらに名前を付けられるカスタムデータ型です。オブジェクト指向言語に慣れているなら、構造体はオブジェクトのデータ属性のようなものです。この章では、すでに知っていることを土台にしながらタプルと構造体を比較対照し、データをグループ化する方法として構造体のほうが適している場面を示します。
構造体をどのように定義し、インスタンス化するかを示します。また、構造体型に関連づけられた振る舞いを指定するために、関連関数、特に メソッド と呼ばれる種類の関連関数をどのように定義するかについても説明します。構造体と enum(第6章で説明します)は、Rust のコンパイル時型チェックを最大限に活用するために、プログラムのドメインで新しい型を作成するための構成要素です。
構造体を定義し、インスタンス化する
構造体を定義し、インスタンス化する
構造体は、「タプル型」節で説明したタプルに似ています。どちらも複数の関連する値を保持するためです。タプルと同様に、構造体を構成する各データはそれぞれ異なる型にできます。タプルとは異なり、構造体では各データ片に名前を付けるので、値が何を意味するのかが明確になります。こうした名前があることで、構造体はタプルより柔軟になります。インスタンスの値を指定したりアクセスしたりする際に、データの順序に頼る必要がありません。
構造体を定義するには、キーワード struct を記述し、構造体全体に名前を付けます。構造体の名前は、ひとまとめにされるデータ片の意味を表すものであるべきです。続いて、波括弧の内側で、各データ片の名前と型を定義します。これらを_フィールド_と呼びます。たとえば、リスト 5-1 はユーザーアカウントに関する情報を格納する構造体を示しています。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {}
定義した構造体を使うには、各フィールドに具体的な値を指定して、その構造体の_インスタンス_を生成します。インスタンスを作るには、構造体名を書いたあとに、波括弧の中に_key: value_ の組を並べます。ここで、キーはフィールド名、値はそのフィールドに格納したいデータです。フィールドは、構造体で宣言した順番どおりに指定する必要はありません。言い換えると、構造体定義はその型の一般的なテンプレートのようなものであり、各インスタンスはそのテンプレートに具体的なデータを埋め込むことで、その型の値を作ります。たとえば、リスト 5-2 に示すように、特定のユーザーを宣言できます。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
構造体から特定の値を取得するには、ドット記法を使います。たとえば、このユーザーのメールアドレスにアクセスするには、user1.email を使います。インスタンスが可変なら、ドット記法を使って特定のフィールドに代入することで値を変更できます。リスト 5-3 は、可変な User インスタンスの email フィールドの値を変更する方法を示しています。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
}
インスタンス全体が可変でなければならないことに注意してください。Rust では、特定のフィールドだけを可変としてマークすることはできません。どんな式でもそうであるように、関数本体の最後の式として構造体の新しいインスタンスを構築すれば、その新しいインスタンスが暗黙に返されます。
リスト 5-4 は、与えられた email と username を受け取り、User インスタンスを返す build_user 関数を示しています。active フィールドには true が入り、sign_in_count には 1 が入ります。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
}
関数パラメータに構造体のフィールドと同じ名前を付けるのは理にかなっていますが、email と username のフィールド名と変数名を繰り返し書かなければならないのは少々面倒です。構造体にさらに多くのフィールドがあれば、それぞれの名前を繰り返すのはいっそう煩わしくなります。幸い、便利な省略記法があります。
フィールド初期化省略記法を使う
リスト 5-4 ではパラメータ名と構造体フィールド名がまったく同じなので、_フィールド初期化省略記法_を使って build_user を書き換えられます。これにより、挙動はまったく同じまま、username と email の繰り返しがなくなります。これをリスト 5-5 に示します。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
}
ここでは、email という名前のフィールドを持つ User 構造体の新しいインスタンスを生成しています。email フィールドの値には、build_user 関数の email パラメータの値を設定したいのです。email フィールドと email パラメータは同名なので、email: email ではなく単に email と書くだけで済みます。
構造体更新記法でインスタンスを生成する
同じ型の別のインスタンスからほとんどの値を引き継ぎつつ、その一部だけを変更した新しい構造体インスタンスを作りたいことはよくあります。これは構造体更新記法を使うと実現できます。
まずリスト 5-6 では、更新記法を使わずに、通常の方法で user2 に新しい User インスタンスを作る方法を示します。email には新しい値を設定しますが、それ以外にはリスト 5-2 で作成した user1 と同じ値を使います。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
構造体更新記法を使うと、リスト 5-7 に示すように、より少ないコードで同じ効果を得られます。.. という構文は、明示的に設定していない残りのフィールドについて、与えられたインスタンス内の対応するフィールドと同じ値を使うことを指定します。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
リスト 5-7 のコードでも、user2 に対して email には異なる値を持ちながら、username、active、sign_in_count フィールドには user1 と同じ値を持つインスタンスを作成しています。..user1 は、残りのすべてのフィールドが user1 の対応するフィールドから値を取得することを示すため、最後に書かなければなりません。ただし、構造体定義におけるフィールドの順序に関係なく、いくつのフィールドに対してでも、どの順番でも値を指定できます。
構造体更新構文では代入のように = を使うことに注意してください。これは、「変数とデータの相互作用:ムーブ」 節で見たのと同じように、データをムーブするためです。この例では、user1 の username フィールド内の String が user2 にムーブされたため、user2 を作成した後は user1 を使えなくなります。もし user2 に対して email と username の両方に新しい String 値を与え、user1 からは active と sign_in_count の値だけを使っていたなら、user2 を作成した後でも user1 は引き続き有効です。active と sign_in_count はどちらも Copy トレイトを実装する型なので、「スタックのみのデータ:Copy」 節で説明した動作が適用されます。この例では user1.email も引き続き使用できます。これは、その値が user1 からムーブされていないためです。
タプル構造体で異なる型を作る
Rust は、タプルに似た見た目の構造体である タプル構造体 もサポートしています。タプル構造体は、構造体名が与える追加の意味を持ちますが、各フィールドに関連付けられた名前はありません。代わりに、フィールドの型だけを持ちます。タプル構造体は、タプル全体に名前を付けて、そのタプルを他のタプルとは異なる型にしたい場合や、通常の構造体のように各フィールドに名前を付けると冗長または重複になる場合に便利です。
タプル構造体を定義するには、struct キーワードと構造体名を書き、その後にタプル内の型を続けます。たとえば、ここでは Color と Point という 2 つのタプル構造体を定義して使用しています。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
black と origin の値は、異なるタプル構造体のインスタンスであるため、別の型であることに注意してください。定義した各構造体は、それ自体が独自の型です。たとえ構造体内のフィールドが同じ型を持っていても同様です。たとえば、型 Color の引数を取る関数は、引数として Point を受け取ることはできません。両方の型が 3 つの i32 値で構成されていたとしても同じです。それ以外の点では、タプル構造体のインスタンスはタプルと似ており、分解して個々の要素を取り出せますし、. に続けてインデックスを書くことで個々の値にアクセスできます。タプルとは異なり、タプル構造体を分解するときには構造体の型名を書く必要があります。たとえば、origin ポイント内の値を x、y、z という変数に分解するには、let Point(x, y, z) = origin; と書きます。
ユニット様構造体を定義する
フィールドをまったく持たない構造体を定義することもできます。これらは ユニット様構造体 と呼ばれます。これは、「タプル型」 節で触れたユニット型 () と似た振る舞いをするためです。ユニット様構造体は、ある型に対してトレイトを実装する必要があるものの、その型自体に格納したいデータがない場合に便利です。トレイトについては第 10 章で説明します。以下は、AlwaysEqual という名前のユニット構造体を宣言してインスタンス化する例です。
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
AlwaysEqual を定義するには、struct キーワードと望む名前を書き、その後にセミコロンを付けます。波かっこも丸かっこも不要です。次に、subject 変数の中に AlwaysEqual のインスタンスを同様の方法で取得できます。つまり、定義した名前をそのまま使い、波かっこも丸かっこも付けません。後でこの型に対して、AlwaysEqual のあらゆるインスタンスが、場合によってはテスト目的で既知の結果を得るために、他のあらゆる型のインスタンスと常に等しくなるような振る舞いを実装すると想像してみてください。その振る舞いを実装するのに、データはまったく必要ありません。第 10 章では、ユニット様構造体を含むあらゆる型に対してトレイトを定義し、それを実装する方法を見ていきます。
構造体データの所有権
リスト 5-1 の
User構造体定義では、&str文字列スライス型ではなく、所有権を持つString型を使いました。これは意図的な選択です。なぜなら、この構造体の各インスタンスがそのすべてのデータを所有し、そのデータが構造体全体が有効である限り有効であってほしいからです。構造体に、他の何かが所有するデータへの参照を格納することも可能ですが、そのためには ライフタイム の使用が必要になります。ライフタイムは Rust の機能であり、第 10 章で説明します。ライフタイムは、構造体が参照しているデータが、その構造体と同じだけの期間有効であることを保証します。たとえば、以下の src/main.rs のように、ライフタイムを指定せずに構造体へ参照を格納しようとすると、これは動作しません。
struct User { active: bool, username: &str, email: &str, sign_in_count: u64, } fn main() { let user1 = User { active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1, }; }コンパイラは、ライフタイム指定子が必要だと報告します。
$ cargo run Compiling structs v0.1.0 (file:///projects/structs) error[E0106]: missing lifetime specifier --> src/main.rs:3:15 | 3 | username: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 ~ username: &'a str, | error[E0106]: missing lifetime specifier --> src/main.rs:4:12 | 4 | email: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 | username: &str, 4 ~ email: &'a str, | For more information about this error, try `rustc --explain E0106`. error: could not compile `structs` (bin "structs") due to 2 previous errors第 10 章では、こうしたエラーを修正して構造体に参照を格納できるようにする方法を説明しますが、今のところは、
&strのような参照の代わりにStringのような所有権を持つ型を使うことで、このようなエラーを回避します。
構造体を使ったプログラム例
構造体を使うプログラムの例
構造体を使いたくなる場面を理解するために、長方形の面積を計算するプログラムを書いてみましょう。最初は単一の変数を使って始め、その後、最終的に構造体を使うようになるまでプログラムをリファクタリングしていきます。
Cargo で rectangles という新しいバイナリプロジェクトを作成し、ピクセル単位で指定した長方形の幅と高さを受け取って、その長方形の面積を計算するようにしましょう。リスト 5-8 は、プロジェクトの src/main.rs でそれを実現する短いプログラムの一例です。
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
では、このプログラムを cargo run で実行してみましょう。
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
このコードは、各次元を指定して area 関数を呼び出すことで長方形の面積を正しく求めていますが、このコードをより明確で読みやすくするために、さらにできることがあります。
このコードの問題は、area のシグネチャを見ると明らかです。
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
area 関数は 1 つの長方形の面積を計算するはずですが、私たちが書いた関数には 2 つの引数があり、その引数どうしが関係していることはプログラムのどこを見ても明確ではありません。幅と高さをひとまとめにしたほうが、より読みやすく、扱いやすくなるでしょう。これを行う方法の 1 つについては、すでに第 3 章の 「タプル型」 の節で、タプルを使う方法として説明しました。
タプルでのリファクタリング
リスト 5-9 は、タプルを使ったこのプログラムの別バージョンを示しています。
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
ある意味では、このプログラムは改善されています。タプルによって少し構造を持たせることができ、今では引数を 1 つだけ渡せばよくなっています。しかし別の意味では、このバージョンはより不明瞭です。タプルの要素には名前がないため、タプルの各部分にインデックスでアクセスしなければならず、計算の意図が分かりにくくなっています。
面積の計算では幅と高さを取り違えても問題にはなりませんが、長方形を画面に描画したい場合には問題になります。その場合、width はタプルのインデックス 0、height はタプルのインデックス 1 であることを覚えておかなければなりません。ほかの人が私たちのコードを使う場合、それを理解して覚えておくのはさらに難しくなるでしょう。コードの中でデータの意味を伝えられていないため、今はエラーを入り込みやすくしてしまっています。
構造体でのリファクタリング
構造体は、データにラベルを付けることで意味を持たせるために使います。リスト 5-10 に示すように、使っているタプルを、全体に対する名前と各部分に対する名前を持つ構造体に変換できます。
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
ここでは、構造体を定義して Rectangle という名前を付けました。波かっこの内側で、フィールドとして width と height を定義しており、どちらも型は u32 です。続いて main では、幅 30、高さ 50 を持つ Rectangle の具体的なインスタンスを作成しました。
area 関数は今では 1 つの引数で定義されており、その名前を rectangle としています。この引数の型は、構造体 Rectangle のインスタンスへの不変借用です。第 4 章で述べたように、構造体の所有権を受け取るのではなく借用したいのです。そうすることで、main は所有権を保持したまま rect1 を引き続き使えます。これが、関数シグネチャと関数呼び出しで & を使っている理由です。
area 関数は Rectangle インスタンスの width フィールドと height フィールドにアクセスします(借用された構造体インスタンスのフィールドにアクセスしても、そのフィールドの値はムーブされないことに注意してください。そのため、構造体の借用をよく見かけます)。area の関数シグネチャは、今や私たちの意図を正確に表しています。つまり、Rectangle の width フィールドと height フィールドを使って、その面積を計算するということです。これにより、幅と高さが互いに関連していることが伝わり、タプルのインデックス値 0 や 1 を使う代わりに、値に説明的な名前を与えられます。これは明快さという点で大きな改善です。
derive したトレイトで機能を追加する
プログラムをデバッグしているときに Rectangle のインスタンスを出力して、すべてのフィールドの値を確認できると便利です。リスト 5-11 では、これまでの章で使ってきた println! マクロ を使おうとしています。しかし、これはうまくいきません。
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1}");
}
このコードをコンパイルすると、中心となるメッセージとして次のようなエラーが出ます。
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
println! マクロはさまざまな種類の整形を行えますが、デフォルトでは波かっこは Display として知られる整形を使うよう println! に指示します。これは、エンドユーザーが直接読むことを意図した出力です。これまで見てきたプリミティブ型は、ユーザーに 1 やその他のプリミティブ型を見せる方法が実質 1 つしかないため、デフォルトで Display を実装しています。しかし構造体では、println! が出力をどう整形すべきかはそれほど明確ではありません。表示の仕方には複数の可能性があるからです。カンマは必要でしょうか。それとも不要でしょうか。波かっこも出力したいでしょうか。すべてのフィールドを表示すべきでしょうか。この曖昧さのため、Rust は私たちの意図を推測しようとはせず、構造体には println! と {} プレースホルダーで使える Display の実装が用意されていません。
エラーを読み進めると、次のような役立つ注記が見つかります。
| |`Rectangle` cannot be formatted with the default formatter
| required by this formatting parameter
試してみましょう! println! マクロ呼び出しは、今度は println!("rect1 is {rect1:?}"); のようになります。波かっこの中に指定子 :? を入れると、Debug と呼ばれる出力形式を使いたいことを println! に伝えます。Debug トレイトにより、開発者にとって役立つ形で構造体を出力できるようになり、コードをデバッグしている間にその値を確認できます。
この変更を加えた状態でコードをコンパイルしてください。しまった! まだエラーが出ます。
error[E0277]: `Rectangle` doesn't implement `Debug`
しかし今回も、コンパイラは役立つ注記を出してくれます。
| required by this formatting parameter
|
Rust には、デバッグ情報を出力する機能が 確かに 含まれていますが、その
機能をこの構造体で使えるようにするには、明示的に有効化しなければなりません。
そのために、リスト 5-12 に示すように、構造体定義の直前に外側属性
#[derive(Debug)] を追加します。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1:?}");
}
これでプログラムを実行しても、エラーは発生せず、次のような出力が表示されます。
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }
いいですね! これは最も見栄えのよい出力ではありませんが、このインスタンスの
すべてのフィールドの値を表示してくれるので、デバッグ時には確実に役立ちます。
より大きな構造体では、もう少し読みやすい出力があると便利です。そのような場合
には、println! の文字列で {:?} の代わりに {:#?} を使えます。この例で
は、{:#?} スタイルを使うと、次のように出力されます。
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}
Debug 形式を使って値を出力するもう 1 つの方法は、dbg!
macro を使うことです。これは式の所有権を受け取ります
(参照を受け取る println! とは対照的です)。そして、コード内でその dbg!
マクロ呼び出しがあるファイル名と行番号を、その式の結果の値とともに出力し、
その値の所有権を返します。
注:
dbg!マクロを呼び出すと、println!のように標準出力のコンソール ストリーム (stdout) に出力するのではなく、標準エラーのコンソール ストリーム (stderr) に出力されます。stderrとstdoutについては、 第 12 章の 「エラーを標準エラーにリダイレクトする」セクション でさらに詳しく説明します。
次は、width フィールドに代入される値と、rect1 に入っている構造体全体の
値の両方に関心がある例です。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
式 30 * scale を dbg! で囲むことができます。dbg! は式の値の所有権を
返すので、width フィールドには、そこに dbg! 呼び出しがなかった場合と
同じ値が入ります。dbg! に rect1 の所有権を奪わせたくないので、次の呼び
出しでは rect1 への参照を使います。この例の出力は次のようになります。
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
最初の出力部分は、式 30 * scale をデバッグしている src/main.rs の 10 行目
から来ており、その結果の値は 60 です(整数に実装されている Debug
フォーマットは、その値だけを出力します)。src/main.rs の 14 行目にある
dbg! 呼び出しは、&rect1 の値を出力しており、これは Rectangle 構造体です。
この出力では、Rectangle 型の整形された Debug フォーマットが使われてい
ます。dbg! マクロは、コードが何をしているのかを突き止めようとしているとき
に、とても役立ちます!
Debug トレイトに加えて、Rust は derive 属性とともに使えるトレイトをいく
つも用意しており、カスタム型に便利な振る舞いを追加できます。それらのトレイト
とその振る舞いは 付録 C に一覧されています。第 10 章では、これらのトレイトにカスタムの振る
舞いを実装する方法と、独自のトレイトを作成する方法を説明します。derive
以外にも多くの属性があります。詳しくは、Rust Reference の「Attributes」
セクション を参照してください。
私たちの area 関数は非常に限定的です。長方形の面積しか計算しません。この
振る舞いを Rectangle 構造体にもっと密接に結び付けると便利でしょう。なぜな
ら、ほかのどの型に対しても動作しないからです。area 関数を、Rectangle 型
に定義された area メソッドに変えることで、このコードをさらにリファクタリン
グする方法を見ていきましょう。
メソッド
メソッド
メソッドは関数に似ています。fn キーワードと名前で宣言でき、引数と戻り値を
持つことができ、メソッドがほかの場所から呼び出されたときに実行される
コードを含みます。関数とは異なり、メソッドは構造体(または enum や
トレイトオブジェクト。これらはそれぞれ 第6章 と 第
18章 で扱います)のコンテキスト内で定義され、
その第1引数は常に self です。これは、そのメソッドが呼び出される対象で
ある構造体のインスタンスを表します。
メソッド構文
引数として Rectangle インスタンスを受け取る area 関数を変更し、代わりに
Rectangle 構造体に定義された area メソッドにしてみましょう。これは
リスト5-13に示されています。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
Rectangle のコンテキスト内で関数を定義するために、まず Rectangle 用の
impl(実装)ブロックを開始します。この impl ブロック内にあるものは
すべて Rectangle 型に関連付けられます。次に、area 関数を impl の
波括弧の内側に移し、シグネチャおよび本体内のすべての箇所で、第1引数
(この場合は唯一の引数)を self に変更します。main では、area
関数を呼び出して rect1 を引数として渡していましたが、代わりに
メソッド構文 を使って Rectangle インスタンスに対して area
メソッドを呼び出せます。メソッド構文はインスタンスの後ろに続けて
書きます。ドットの後にメソッド名、丸括弧、必要なら引数を続けます。
area のシグネチャでは、rectangle: &Rectangle の代わりに &self を
使います。&self は実際には self: &Self の短縮形です。impl
ブロック内では、型 Self はその impl ブロックが対象としている型の
エイリアスです。メソッドは第1引数として、型 Self の self という
名前の引数を持たなければならないため、Rust では第1引数の位置では
名前 self だけでこれを省略して書けます。ここで使った
rectangle: &Rectangle と同じく、このメソッドが Self
インスタンスを借用することを示すために、self
の短縮記法の前に & を付ける必要があることに注意してください。
メソッドは、ほかの引数と同じように、self の所有権を取得したり、
ここで行っているように self を不変で借用したり、あるいは self
を可変で借用したりできます。
ここで &self を選んだのは、関数版で &Rectangle
を使ったのと同じ理由です。所有権は取得したくなく、構造体のデータを
読み取りたいだけで、書き込みたくはないからです。もしメソッドの処理の
一部として、メソッドを呼び出したインスタンスを変更したいなら、第1引数
として &mut self を使います。第1引数に単なる self
を使ってインスタンスの所有権を取得するメソッドはまれです。この手法は
通常、メソッドが self
を別のものに変換するときに使われ、変換後に呼び出し元が元の
インスタンスを使えないようにしたい場合に用いられます。
関数ではなくメソッドを使う主な理由は、メソッド構文を提供でき、
各メソッドのシグネチャで毎回 self の型を繰り返さなくてよいことに
加えて、整理のためです。将来このコードを使うユーザーが、私たちが提供
するライブラリのさまざまな場所で Rectangle
の機能を探し回らなくて済むように、ある型のインスタンスに対して
できることをすべて1つの impl ブロックにまとめたのです。
構造体のフィールドの1つと同じ名前をメソッドに付けることもできる点に
注意してください。たとえば、Rectangle に width
という名前のメソッドを定義できます。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
if rect1.width() {
println!("The rectangle has a nonzero width; it is {}", rect1.width);
}
}
ここでは、インスタンスの width フィールドの値が 0 より大きければ
width メソッドが true を返し、値が 0 なら false
を返すようにしています。同じ名前のメソッドの中で、そのフィールドを
どのような目的にも使うことができます。main で rect1.width
の後ろに丸括弧を付けると、Rust はそれが width
メソッドを意味すると判断します。丸括弧を付けない場合、Rust はそれが
width フィールドを意味すると判断します。
常にそうとは限りませんが、フィールドと同じ名前をメソッドに付けるとき、 そのメソッドにはフィールドの値を返すだけでほかには何もしないことを 期待する場合がよくあります。このようなメソッドは ゲッター と呼ばれ、 Rust は他のいくつかの言語のように、構造体のフィールドに対してこれを 自動実装しません。ゲッターは、フィールドを非公開にしつつメソッドを 公開にでき、その結果、そのフィールドへの読み取り専用アクセスをその型の 公開 API の一部として提供できるので便利です。公開と非公開とは何か、 またフィールドやメソッドを公開または非公開として指定する方法については、 第7章 で説明します。
->演算子はどこへ行ったのか?C と C++ では、メソッド呼び出しに2種類の演算子を使います。オブジェクト 自体に対して直接メソッドを呼び出す場合は
.を使い、オブジェクトへのポインタに対してメソッドを呼び出し、その前に ポインタをデリファレンスする必要がある場合は->を使います。言い換えると、objectがポインタであるなら、object->something()は(*object).something()に似ています。Rust には
->演算子に相当するものはありません。その代わりに、Rust には 自動参照および自動デリファレンス と呼ばれる機能があります。 メソッド呼び出しは、Rust でこの振る舞いを持つ数少ない箇所の1つです。仕組みは次のとおりです。
object.something()のようにメソッドを呼び出す と、Rust はobjectがそのメソッドのシグネチャに合うように、&、&mut、または*を自動的に補います。言い換えると、次の2つは同じです。#![allow(unused)] fn main() { #[derive(Debug,Copy,Clone)] struct Point { x: f64, y: f64, } impl Point { fn distance(&self, other: &Point) -> f64 { let x_squared = f64::powi(other.x - self.x, 2); let y_squared = f64::powi(other.y - self.y, 2); f64::sqrt(x_squared + y_squared) } } let p1 = Point { x: 0.0, y: 0.0 }; let p2 = Point { x: 5.0, y: 6.5 }; p1.distance(&p2); (&p1).distance(&p2); }1つ目のほうがずっとすっきり見えます。この自動参照の振る舞いが機能する のは、メソッドには明確なレシーバー、つまり
selfの型があるからです。レシーバーとメソッド名が分かれば、Rust はそのメソッドが読み取り(&self)、変更(&mut self)、消費(self) のどれを行うかを明確に判断できます。Rust がメソッドレシーバーに対する借用を暗黙にしていることは、所有権を実際に 扱いやすくしている大きな要因です。
より多くの引数を持つメソッド
Rectangle 構造体に 2 つ目のメソッドを実装して、メソッドの使い方を練習してみましょう。
今回は、Rectangle のあるインスタンスが別の Rectangle のインスタンスを受け取り、
2 つ目の Rectangle が self(最初の Rectangle)の中に完全に収まる場合は
true を返し、そうでない場合は false を返すようにしたいと考えています。
つまり、can_hold メソッドを定義したら、リスト 5-14 に示す
プログラムを書けるようにしたいのです。
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
期待される出力は次のようになります。rect2 の両方の寸法が
rect1 の寸法より小さい一方で、rect3 は rect1 より幅が広いためです。
Can rect1 hold rect2? true
Can rect1 hold rect3? false
メソッドを定義したいことは分かっているので、それは impl Rectangle
ブロックの中に置くことになります。メソッド名は can_hold で、別の
Rectangle への不変借用を引数として取ります。引数の型は、メソッドを
呼び出しているコードを見ると分かります。
rect1.can_hold(&rect2) では &rect2 が渡されており、これは
Rectangle のインスタンスである rect2 への不変借用です。これは理にかなっています。
必要なのは rect2 を読むことだけであり(書き込むなら可変借用が必要になります)、
さらに can_hold メソッドを呼び出したあとでも main が rect2 の所有権を
保持して、再び使えるようにしたいからです。can_hold の戻り値は
ブール値になり、実装では self の幅と高さが、もう一方の
Rectangle の幅と高さよりそれぞれ大きいかどうかを確認します。新しい
can_hold メソッドを、リスト 5-15 に示すように、リスト 5-13 の impl ブロックに
追加してみましょう。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
このコードをリスト 5-14 の main 関数と一緒に実行すると、望んでいた
出力が得られます。メソッドは複数の引数を取ることができ、それらは
self 引数の後にシグネチャへ追加します。そうした引数は、関数の引数と
まったく同じように機能します。
関連関数
impl ブロック内で定義されるすべての関数は 関連関数
と呼ばれます。これは、それらが impl の後に書かれた型に関連付けられているためです。
最初の引数に self を持たない(したがってメソッドではない)
関連関数を定義することもできます。そうした関数は、処理対象となる型の
インスタンスを必要としないからです。すでにこの種の関数を 1 つ使っています。
String 型に定義されている String::from 関数です。
メソッドではない関連関数は、新しい構造体インスタンスを返す
コンストラクタとしてよく使われます。こうした関数はしばしば new と
呼ばれますが、new は特別な名前ではなく、言語に組み込まれているわけでも
ありません。たとえば、square という名前の関連関数を用意して、
1 つの寸法引数を受け取り、それを幅と高さの両方に使うこともできます。そうすれば、
同じ値を 2 回指定しなくても、正方形の Rectangle を簡単に作れます。
ファイル名: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(3);
}
戻り値の型と関数本体にある Self キーワードは、impl キーワードの後に現れる
型、この場合は Rectangle の別名です。
この関連関数を呼び出すには、構造体名とともに :: 構文を使います。
let sq = Rectangle::square(3); がその一例です。この関数は構造体によって
名前空間化されています。:: 構文は、関連関数と、モジュールによって作られる
名前空間の両方に使われます。モジュールについては 第7章 で説明します。
複数の impl ブロック
各構造体は複数の impl ブロックを持てます。たとえば、リスト
5-15 はリスト 5-16 に示すコードと等価で、そこでは各メソッドが
それぞれ独自の impl ブロックに入っています。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
ここではこれらのメソッドを複数の impl ブロックに分ける理由はありませんが、
これは有効な構文です。複数の impl ブロックが役に立つケースは、
第10章でジェネリックな型とトレイトを扱う際に見ていきます。
まとめ
構造体を使うと、自分のドメインにとって意味のある独自の型を作れます。
構造体を使うことで、互いに関連するデータを結び付けたままにでき、
各要素に名前を付けてコードを明確にできます。impl ブロックでは、
その型に関連付けられた関数を定義でき、メソッドは関連関数の一種で、
構造体のインスタンスが持つ振る舞いを指定できます。
しかし、独自の型を作る方法は構造体だけではありません。Rust の enum 機能に進み、 ツールボックスにもう 1 つ道具を加えましょう。
列挙型とパターンマッチング
この章では、列挙型について見ていきます。列挙型は enums とも呼ばれます。
列挙型を使うと、取りうるバリアントを列挙することで型を定義できます。まず、
enum がデータとともに意味をどのようにエンコードできるかを示すために、enum を
定義して使ってみます。次に、値が何かであるか、あるいは何もないかのどちらかで
あることを表現する、Option と呼ばれる特に便利な enum を探ります。続いて、
match 式におけるパターンマッチングによって、enum の異なる値に対して異なる
コードを簡単に実行できることを見ていきます。最後に、if let 構文が、コード中で
enum を扱うための、もう 1 つの便利で簡潔なイディオムであることを取り上げます。
列挙型を定義する
enumを定義する
構造体は、width と height を持つ Rectangle のように、関連する
フィールドやデータをひとまとめにする方法を提供します。一方、enum は、
ある値が取りうる値の集合のうちの1つであることを表現する方法を提供しま
す。たとえば、Rectangle が、Circle や Triangle も含む図形の候補
の集合の1つである、と表したいことがあります。これを行うために、Rust で
はこれらの可能性を enum としてエンコードできます。
コードで表現したくなる場面を見て、この場合になぜ enum が便利で、構造体 よりも適しているのかを確認しましょう。IPアドレスを扱う必要があるとしま す。現在、IPアドレスには主要な標準が2つあります。バージョン4とバージョ ン6です。プログラムが遭遇しうる IPアドレスの可能性はこの2つしかないの で、取りうるすべてのバリアントを 列挙 できます。enumeration という名 前はここから来ています。
どの IPアドレスも、バージョン4のアドレスかバージョン6のアドレスのどち らかであり、同時に両方であることはありません。IPアドレスのこの性質によ って、enum というデータ構造が適切になります。なぜなら、enum の値はその バリアントのうち1つにしかなれないからです。バージョン4のアドレスもバー ジョン6のアドレスも、根本的にはどちらも IPアドレスです。そのため、コー ドがあらゆる種類の IPアドレスに当てはまる状況を扱うときには、同じ型と して扱うべきです。
この概念は、IpAddrKind という enum を定義し、IPアドレスが取りうる種
類として V4 と V6 を列挙することで、コードで表現できます。これらが
enum のバリアントです。
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
これで IpAddrKind は、コードのほかの場所で使えるカスタムデータ型にな
りました。
enumの値
IpAddrKind の2つのバリアントそれぞれのインスタンスは、次のように作成
できます。
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
enum のバリアントはその識別子の名前空間の下にあり、その2つを区切るため
に二重コロンを使う点に注目してください。これは便利です。というのも、両
方の値 IpAddrKind::V4 と IpAddrKind::V6 は、同じ型 IpAddrKind の
値だからです。そのため、たとえば任意の IpAddrKind を受け取る関数を定
義できます。
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
そして、この関数はどちらのバリアントでも呼び出せます。
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
enum を使うことには、さらに多くの利点があります。私たちの IPアドレス型 についてもう少し考えると、現時点では実際の IPアドレスの データ を保存 する方法がありません。わかっているのは、それがどの 種類 かだけです。 5章で構造体について学んだばかりなので、リスト6-1に示すように、この問題 を構造体で解決したくなるかもしれません。
fn main() {
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
}
ここでは、2つのフィールドを持つ IpAddr 構造体を定義しています。1つは
型が IpAddrKind(先ほど定義した enum)である kind フィールド、もう
1つは型が String である address フィールドです。この構造体のインス
タンスは2つあります。1つ目は home で、kind には
IpAddrKind::V4 の値が入り、関連付けられたアドレスデータとして
127.0.0.1 を持ちます。2つ目のインスタンスは loopback です。こちら
は kind の値として IpAddrKind のもう一方のバリアントである V6 を
持ち、アドレス ::1 が関連付けられています。kind と address の値
をひとまとめにするために構造体を使ったので、これでバリアントが値に関連
付けられるようになりました。
しかし、同じ概念を enum だけで表すほうが、より簡潔です。構造体の中に
enum を入れるのではなく、各 enum バリアントに直接データを持たせられま
す。IpAddr enum のこの新しい定義では、V4 と V6 の両方のバリアン
トが関連する String 値を持つことになります。
fn main() {
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
}
データを各 enum バリアントに直接結び付けているので、余分な構造体は必要
ありません。ここでは、enum の動作に関するもう1つの詳細もわかりやすく見
て取れます。つまり、定義した各 enum バリアントの名前は、その enum のイ
ンスタンスを構築する関数にもなるということです。言い換えると、
IpAddr::V4() は String 引数を受け取り、IpAddr 型のインスタンスを
返す関数呼び出しです。enum を定義した結果として、このコンストラクタ関数
が自動的に定義されます。
構造体ではなく enum を使うことには、もう1つ利点があります。各バリアン
トは、関連付けられるデータの型や量をそれぞれ変えられるのです。バージョ
ン4の IPアドレスは、常に0から255までの値を取る4つの数値コンポーネント
を持ちます。もし V4 アドレスを4つの u8 値として保存しつつ、V6 ア
ドレスは1つの String 値として表したいなら、構造体ではそれはできませ
ん。enum はこのケースを簡単に扱えます。
fn main() {
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
}
ここまで、バージョン4とバージョン6の IPアドレスを保存するためのデータ
構造を定義するいくつかの異なる方法を示してきました。しかし実際のところ、
IPアドレスを保存し、その種類を表現したいという要件は非常によくあるた
め、標準ライブラリには使える定義が用意されています!
標準ライブラリが IpAddr をどのように定義しているかを見てみましょう。
そこには、私たちが定義して使ってきたものとまったく同じ enum とバリアン
トがありますが、アドレスデータは2つの異なる構造体の形でバリアントの内
部に埋め込まれており、それぞれのバリアントごとに異なる定義になっていま
す。
#![allow(unused)]
fn main() {
struct Ipv4Addr {
// --省略--
}
struct Ipv6Addr {
// --省略--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
}
このコードは、enum のバリアントの中にどんな種類のデータでも入れられる ことを示しています。たとえば、文字列、数値型、構造体などです。別の enum を含めることさえできます。また、標準ライブラリの型は、たいてい あなたが思いつくものと比べてそれほど複雑ではありません。
標準ライブラリに IpAddr の定義が含まれているにもかかわらず、競合せず
に独自の定義を作成して使えることに注意してください。これは、標準ライブ
ラリの定義をまだ自分たちのスコープに持ち込んでいないからです。型をスコ
ープに持ち込むことについては、7章でさらに詳しく説明します。
リスト6-2にある enum の別の例を見てみましょう。こちらは、バリアントに さまざまな型が埋め込まれています。
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {}
この enum には、型の異なる4つのバリアントがあります。
Quit: 関連付けられたデータをまったく持たないMove: 構造体のように名前付きフィールドを持つWrite: 単一のStringを含むChangeColor: 3つのi32値を含む リスト6-2のようなバリアントを持つ列挙型の定義は、さまざまな種類の構造体定義を 行うのと似ています。ただし、列挙型ではstructキーワードを使わず、すべての バリアントがMessage型の下にひとまとめにされます。次の構造体は、前述の列挙型の バリアントが保持するのと同じデータを保持できます。
struct QuitMessage; // unit struct
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct
fn main() {}
しかし、それぞれが独自の型を持つこれらの異なる構造体を使うと、リスト6-2で定義した
Message 列挙型のように、単一の型としてこれらのあらゆる種類のメッセージを受け取る
関数を簡単には定義できません。
列挙型と構造体には、もう1つ似ている点があります。impl を使って構造体にメソッドを
定義できるのと同じように、列挙型にもメソッドを定義できます。以下は、Message
列挙型に定義できる call という名前のメソッドです。
fn main() {
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// method body would be defined here
}
}
let m = Message::Write(String::from("hello"));
m.call();
}
このメソッドの本体では、self を使って、そのメソッドを呼び出した値を取得します。
この例では、Message::Write(String::from("hello")) という値を持つ変数 m を作成
しており、m.call() が実行されると、call メソッドの本体で self はその値に
なります。
次に、標準ライブラリにある、非常に一般的で便利な別の列挙型 Option を見てみましょう。
Option 列挙型
この節では、標準ライブラリで定義されている別の列挙型である Option のケース
スタディを見ていきます。Option 型は、値が何かである場合もあれば、何もない
場合もあるという、非常によくある状況を表現します。
たとえば、空でないリストの最初の要素を要求すれば、値が得られます。空のリストの 最初の要素を要求すれば、何も得られません。この概念を型システムで表現することで、 コンパイラは、処理すべきすべてのケースをあなたが処理したかどうかを検査できます。 この機能は、他のプログラミング言語で非常によくあるバグを防ぐことができます。
プログラミング言語設計は、どの機能を含めるかという観点で考えられることが多い ですが、除外する機能も同様に重要です。Rust には、多くの他の言語にある null 機能がありません。Null とは、そこに値が存在しないことを意味する値です。null を持つ言語では、変数は常に null か null ではないかの2つの状態のいずれかに なり得ます。
2009年の講演「Null References: The Billion Dollar Mistake」で、null の発明者 である Tony Hoare は次のように述べています。
私はこれを、自分の10億ドルの失敗だと呼んでいます。当時、私はオブジェクト指向 言語における参照のための、最初の包括的な型システムを設計していました。私の 目標は、参照のあらゆる利用が絶対に安全であり、コンパイラが自動的に検査を 行うようにすることでした。しかし、あまりに実装が簡単だったため、null 参照を 入れたいという誘惑に抗えませんでした。その結果、数え切れないほどのエラー、 脆弱性、システムクラッシュを招き、この40年間でおそらく10億ドル分の苦痛と 損害を引き起こしてきました。
null 値の問題は、null 値を null ではない値として使おうとすると、何らかの 種類のエラーが発生することです。この null か null ではないかという性質は いたるところに関わるため、この種のエラーは非常に起こしやすいのです。
しかし、null が表現しようとしている概念自体は、依然として有用です。null は、 何らかの理由で現在は無効であるか、存在しない値です。
問題は、実際には概念そのものではなく、その特定の実装にあります。そのため、
Rust には null はありませんが、値が存在するか存在しないかという概念を表現
できる列挙型があります。この列挙型は Option<T> であり、標準ライブラリで
次のように定義されています。
#![allow(unused)]
fn main() {
enum Option<T> {
None,
Some(T),
}
}
Option<T> 列挙型は非常に有用なので、prelude にも含まれています。これを
明示的にスコープに導入する必要はありません。そのバリアントも prelude に含まれて
います。Option:: 接頭辞なしで Some と None を直接使えます。Option<T>
列挙型は、依然としてただの通常の列挙型であり、Some(T) と None は依然として
Option<T> 型のバリアントです。
<T> 構文は、Rust の機能の1つで、まだ説明していません。これはジェネリックな
型パラメータであり、ジェネリクスについては第10章でより詳しく扱います。今の
ところ知っておくべきなのは、<T> は Option 列挙型の Some バリアントが
任意の型のデータを1つ保持できること、そして T の代わりに使われる具体的な
型ごとに、全体の Option<T> 型も別の型になるということです。以下は、Option
値を使って数値型や char 型を保持する例です。
fn main() {
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
}
some_number の型は Option<i32> です。some_char の型は Option<char> で、
これは別の型です。Rust は、Some バリアントの中に値を指定しているため、これらの
型を推論できます。absent_number については、Rust では全体の Option 型を
注釈する必要があります。コンパイラは、None の値だけを見ても、それに対応する
Some バリアントがどの型を保持するのかを推論できません。ここでは、Rust に
absent_number が Option<i32> 型であることを伝えています。
Some の値があるとき、値が存在することがわかり、その値は Some の中に保持
されています。None の値があるとき、それはある意味では null と同じことを
意味します。つまり、有効な値を持っていないということです。では、Option<T> を
持つことは、null を持つことより、なぜよいのでしょうか。
要するに、Option<T> と T(ここで T は任意の型になり得ます)は異なる型で
あるため、コンパイラは Option<T> の値を、あたかもそれが確実に有効な値である
かのように使うことを許さないからです。たとえば、次のコードはコンパイルされ
ません。i8 と Option<i8> を足そうとしているからです。
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
}
このコードを実行すると、次のようなエラーメッセージが表示されます。
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
`&i8` implements `Add<i8>`
`&i8` implements `Add`
`i8` implements `Add<&i8>`
`i8` implements `Add`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error
強烈ですね! 実際のところ、このエラーメッセージが意味しているのは、Rust は
i8 と Option<i8> の足し方を理解していない、ということです。なぜなら、
それらは異なる型だからです。Rust で i8 のような型の値を持っているとき、
コンパイラはその値が常に有効であることを保証します。私たちは、その値を使う前に
null かどうかを確認しなくても、自信を持って先に進めます。Option<i8>(または
扱っている値の型が何であれ)を持っているときにだけ、値を持っていない可能性を
心配する必要があり、コンパイラは、その値を使う前にそのケースを確実に処理させます。
言い換えると、Option<T> を使って T の操作を実行する前に、Option<T> を T に変換する必要があります。一般に、これは null に関する最も一般的な問題の 1 つ、つまり実際には null であるのに null ではないと思い込んでしまうことを防ぐのに役立ちます。
null ではない値だと誤って思い込むリスクを取り除くことで、コードに対してより自信を持てるようになります。null である可能性のある値を持つには、その値の型を Option<T> にすることで、明示的にそうすることを選ばなければなりません。すると、その値を使うときには、その値が null である場合を明示的に処理することが求められます。値の型が Option<T> ではないあらゆる場所では、その値が null ではないと安全に仮定できます。これは、null が至る所に広がることを抑え、Rust コードの安全性を高めるための、Rust における意図的な設計判断でした。
では、Option<T> 型の値を持っているときに、その値を使えるようにするためには、Some バリアントからどのようにして T の値を取り出せばよいのでしょうか。Option<T> enum には、さまざまな状況で役立つ多数のメソッドがあります。詳しくは そのドキュメント を確認してください。Option<T> のメソッドに慣れ親しむことは、Rust を学んでいくうえで非常に役立ちます。
一般に、Option<T> の値を使うには、それぞれのバリアントを処理するコードを用意したいものです。Some(T) の値があるときにだけ実行されるコードが必要で、そのコードでは内部の T を使うことができます。None の値がある場合にだけ実行される別のコードも必要で、そのコードでは利用できる T の値はありません。match 式は、enum とともに使うとまさにこれを行う制御フロー構文です。enum のどのバリアントを持っているかに応じて異なるコードを実行し、そのコードは一致した値の内部にあるデータを使うことができます。
match 制御フロー構文
match 制御フロー構文
Rust には match という非常に強力な制御フロー構文があり、これを使うと値を一連のパターンと比較し、どのパターンに一致したかに基づいてコードを実行できます。パターンは、リテラル値、変数名、ワイルドカード、その他さまざまな要素で構成できます。第19章 では、あらゆる種類のパターンとその働きを扱います。match の強力さは、パターンの表現力と、考えられるすべてのケースが処理されていることをコンパイラが確認してくれる点にあります。
match 式は、コインを仕分ける機械のようなものだと考えてください。コインは大きさの異なる穴が並んだレールを滑り落ち、自分が入ることのできる最初の穴に落ちます。同じように、値は match の各パターンを順に通過し、値が「収まる」最初のパターンで、関連付けられたコードブロックに落ちて実行時に使われます。
コインの話が出たところで、match の例としてコインを使ってみましょう! 未知の米国硬貨を受け取り、仕分け機と同じようにその硬貨が何であるかを判定して、セント単位の値を返す関数を書くことができます。これをリスト6-3に示します。
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {}
value_in_cents 関数内の match を見ていきましょう。まず、match キーワードの後に式を書きます。この場合は値 coin です。これは if で使う条件式とよく似ているように見えますが、大きな違いがあります。if では条件は真偽値に評価される必要がありますが、ここではどんな型でもかまいません。この例での coin の型は、1行目で定義した Coin enum です。
次に match アームがあります。アームは、パターンと何らかのコードという2つの部分から成ります。ここでの最初のアームは、値 Coin::Penny というパターンと、それに続く、パターンと実行するコードを区切る => 演算子を持っています。この場合のコードは単に値 1 です。各アームはカンマで区切られます。
match 式が実行されると、その式の評価結果の値が各アームのパターンと順番に比較されます。あるパターンが値に一致した場合、そのパターンに関連付けられたコードが実行されます。そのパターンが値に一致しない場合、コイン仕分け機と同じように、実行は次のアームへ進みます。必要なだけアームを持つことができます。リスト6-3では、match は4つのアームを持っています。
各アームに関連付けられたコードは式であり、一致したアームの式の評価結果が match 式全体の返り値になります。
match アームのコードが短い場合は、通常、波かっこを使いません。リスト6-3では各アームが単に値を返すだけなので、そうなっています。match アームで複数行のコードを実行したい場合は、波かっこを使わなければなりません。その場合、アームの後ろのカンマは省略可能です。たとえば、次のコードは、このメソッドが Coin::Penny で呼び出されるたびに「Lucky penny!」と表示しますが、それでもブロックの最後の値である 1 を返します。
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {}
値に束縛するパターン
match アームのもう1つの便利な機能は、パターンに一致した値の一部を束縛できることです。これにより、enum バリアントから値を取り出せます。
例として、enum のバリアントの1つが内部にデータを保持するように変更してみましょう。1999年から2008年にかけて、米国は50州それぞれについて片面のデザインが異なるクォーター硬貨を鋳造しました。州ごとのデザインが施されたのはほかの硬貨にはなかったため、この追加の値を持つのはクォーター硬貨だけです。Quarter バリアントが内部に保持する UsState 値を含むように変更することで、この情報を enum に追加できます。リスト6-4ではそのようにしています。
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {}
友人が50州すべてのクォーター硬貨を集めようとしていると想像してみてください。ばらばらの小銭を硬貨の種類ごとに仕分けしながら、各クォーターに対応する州の名前も読み上げることにしましょう。そうすれば、それが友人のまだ持っていないものなら、そのコレクションに加えられます。
このコードの match 式では、Coin::Quarter バリアントの値に一致するパターンに state という変数を追加します。Coin::Quarter に一致すると、state 変数はそのクォーターの州の値に束縛されます。すると、そのアームのコードで state を次のように使えます。
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {state:?}!");
25
}
}
}
fn main() {
value_in_cents(Coin::Quarter(UsState::Alaska));
}
もし value_in_cents(Coin::Quarter(UsState::Alaska)) を呼び出すと、coin は Coin::Quarter(UsState::Alaska) になります。その値を各 match アームと比較していくと、Coin::Quarter(state) に到達するまでどれにも一致しません。その時点で、state への束縛は UsState::Alaska という値になります。そうして、その束縛を println! 式の中で使うことで、Coin enum の Quarter バリアントから内部の州の値を取り出せます。
Option<T> の match パターン
前の節では、Option<T> を使うときに Some の場合から内部の T 値を取り出したいと考えました。Coin enum で行ったのと同じように、Option<T> も match を使って扱えます! 硬貨を比較する代わりに Option<T> のバリアントを比較するだけで、match 式の動作のしかたは同じです。
Option<i32> を受け取り、内部に値があるならその値に 1 を足す関数を書きたいとしましょう。内部に値がない場合、その関数は None の値を返し、何らかの操作を実行しようとするべきではありません。
この関数は、match のおかげで非常に簡単に書けます。リスト6-5のようになります。
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
plus_one の最初の実行をもう少し詳しく見てみましょう。plus_one(five) を呼び出すと、plus_one 本体の変数 x は Some(5) という値を持ちます。次に、それを各 match アームと比較します。
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Some(5) という値は None パターンに一致しないので、次のアームに進みます。
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Some(5) は Some(i) にマッチするでしょうか。マッチします!同じバリアントだからです。i は Some に含まれている値に束縛されるので、i は値 5 を取ります。すると match アーム内のコードが実行され、i の値に 1 を足して、合計 6 を中に入れた新しい Some 値を作成します。
次に、リスト 6-5 にある plus_one の 2 回目の呼び出しを考えてみましょう。このとき x は None です。match に入り、最初のアームと比較します。
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
これもマッチします!加算する値はないので、プログラムは停止して => の右側にある None 値を返します。最初のアームがマッチしたので、ほかのアームは比較されません。
match と enum を組み合わせることは、多くの場面で役に立ちます。Rust のコードではこのパターンを頻繁に目にするでしょう。つまり、enum に対して match し、その内部のデータに変数を束縛し、それに基づいてコードを実行するというものです。最初は少しややこしいかもしれませんが、慣れてしまえば、すべての言語にこれがあればいいのにと思うようになるでしょう。これは一貫してユーザーに人気のある機能です。
match は網羅的である
match には、もう 1 つ説明しておくべき側面があります。アームのパターンは、すべての可能性を網羅していなければなりません。バグがあり、コンパイルできない次の plus_one 関数を考えてみましょう。
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
None のケースを処理していないため、このコードはバグを引き起こします。幸いなことに、これは Rust が検出できるバグです。このコードをコンパイルしようとすると、次のようなエラーが表示されます。
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
--> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597:5
|
= note: not covered
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error
Rust は、すべての可能なケースを網羅していないことを理解しており、しかも、どのパターンを忘れたのかまで把握しています!Rust の match は 網羅的 です。コードが有効であるためには、ありうるすべての可能性を余すところなく扱わなければなりません。特に Option<T> の場合、Rust が None のケースを明示的に処理し忘れないようにしてくれることで、実際には null かもしれないのに値があると思い込むことから私たちを守り、先に述べた 10 億ドルの過ちを不可能にしてくれます。
キャッチオールパターンと _ プレースホルダー
enum を使うと、いくつかの特定の値に対しては特別な動作をさせ、それ以外のすべての値に対しては 1 つのデフォルト動作をさせることもできます。たとえば、ゲームを実装していると想像してみてください。サイコロの出目が 3 なら、プレイヤーは移動せず、代わりにおしゃれな新しい帽子を手に入れます。7 が出たら、プレイヤーはおしゃれな帽子を 1 つ失います。それ以外の値なら、プレイヤーはゲーム盤の上をその数だけ進みます。次の match はそのロジックを実装したものです。サイコロの出目はランダムな値ではなくハードコードされており、他のロジックは、実際に実装することがこの例の範囲外であるため、本体を持たない関数で表されています。
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
}
最初の 2 つのアームでは、パターンはリテラル値 3 と 7 です。その他のすべての値をカバーする最後のアームでは、パターンは other という名前を付けた変数です。other アームで実行されるコードは、その変数を move_player 関数に渡して使います。
このコードは、u8 が取りうるすべての値を列挙していないにもかかわらず、コンパイルできます。なぜなら、最後のパターンが、個別に列挙されていないすべての値にマッチするからです。このキャッチオールパターンは、match が網羅的でなければならないという要件を満たしています。パターンは順番に評価されるため、キャッチオールアームは最後に置かなければならないことに注意してください。もしキャッチオールアームをもっと前に置いてしまうと、他のアームは決して実行されないため、キャッチオールの後にアームを追加すると Rust は警告します!
Rust には、キャッチオールは欲しいけれど、そのキャッチオールパターンの値を 使い たくないときに使えるパターンもあります。_ は任意の値にマッチし、その値に束縛しない特別なパターンです。これは Rust に、その値を使わないことを伝えるので、Rust は未使用変数について警告しません。
ゲームのルールを変更してみましょう。今度は、3 または 7 以外が出たら、もう一度振らなければなりません。キャッチオールの値を使う必要がなくなったので、other という名前の変数の代わりに _ を使うようにコードを変更できます。
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
}
この例も網羅性の要件を満たしています。最後のアームで他のすべての値を明示的に無視しているので、何も見落としていないからです。
最後に、ゲームのルールをもう一度変更して、3 または 7 以外が出た場合には、そのターンではそれ以外何も起こらないようにします。これは、_ アームに対応するコードとしてユニット値(「タプル型」 節で触れた空のタプル型)を使うことで表現できます。
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
}
ここでは、先行するアームのパターンにマッチしない他のどの値も使わず、この場合にはどんなコードも実行したくないということを Rust に明示的に伝えています。
パターンとマッチングについては、第19章 でさらに詳しく扱います。ここでは、match 式が少し冗長になる状況で役立つ if let 構文へ進みましょう。
if let と let...else による簡潔な制御フロー
if let と let...else による簡潔な制御フロー
if let 構文を使うと、if と let を組み合わせて、1つのパターンに一致する値を処理し、それ以外を無視する方法をより簡潔に書けます。リスト6-6のプログラムでは、config_max 変数内の Option<u8> 値に対してマッチしていますが、値が Some バリアントの場合にだけコードを実行したいと考えています。
fn main() {
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}
}
値が Some であれば、パターン中で値を変数 max に束縛することにより、Some バリアント内の値を出力します。None の値に対しては何もしたくありません。match 式の要件を満たすために、1つのバリアントを処理したあとに _ => () を追加しなければならず、これは追加するのが面倒なボイラープレートコードです。
代わりに、if let を使ってもっと短く書けます。次のコードはリスト6-6の match と同じように振る舞います。
fn main() {
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}
}
if let 構文は、等号で区切られたパターンと式を受け取ります。これは match と同じように動作し、式は match に渡され、パターンはその最初のアームになります。この場合、パターンは Some(max) であり、max は Some の内側の値に束縛されます。その後、対応する match アームで max を使ったのと同じように、if let ブロックの本体で max を使えます。if let ブロック内のコードは、値がそのパターンに一致した場合にのみ実行されます。
if let を使うと、書く量が少なくなり、インデントも減り、ボイラープレートコードも少なくなります。しかし、match が強制する、どのケースの処理も忘れていないことを保証する網羅性チェックは失われます。match と if let のどちらを選ぶかは、その状況で何をしているか、そして簡潔さを得ることが網羅性チェックを失うことに対して適切なトレードオフかどうかによって決まります。
言い換えると、if let は、値が1つのパターンに一致したときにコードを実行し、それ以外のすべての値は無視する match の糖衣構文だと考えられます。
if let には else を含めることもできます。else に対応するコードブロックは、if let と else に等価な match 式における _ ケースに対応するコードブロックと同じです。Quarter バリアントが UsState 値も保持していた、リスト6-4の Coin enum 定義を思い出してください。見つけた Quarter 以外の硬貨をすべて数えつつ、同時に25セント硬貨の州を知らせたいとすると、次のような match 式で実現できます。
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {
let coin = Coin::Penny;
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {state:?}!"),
_ => count += 1,
}
}
あるいは、次のように if let と else 式を使うこともできます。
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {
let coin = Coin::Penny;
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {state:?}!");
} else {
count += 1;
}
}
let...else で“ハッピーパス”を保つ
一般的なパターンは、値が存在するときに何らかの計算を行い、そうでなければデフォルト値を返すことです。UsState 値を持つコインの例を続けると、25セント硬貨の州がどれくらい古いかによって何か面白いことを言いたいなら、次のように州の年齢を確認するメソッドを UsState に導入するかもしれません。
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn describe_state_quarter(coin: Coin) -> Option<String> {
if let Coin::Quarter(state) = coin {
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
} else {
None
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
そして、リスト6-7のように、コインの種類に対してマッチさせるために if let を使い、条件の本体の中で state 変数を導入するかもしれません。
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn describe_state_quarter(coin: Coin) -> Option<String> {
if let Coin::Quarter(state) = coin {
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
} else {
None
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
これは目的は果たしますが、処理が if let 文の本体に押し込められており、行うべき処理がもっと複雑であれば、トップレベルの分岐がどのように関係しているのかを正確に追うのが難しくなるかもしれません。式は値を生成するという事実を利用して、if let から state を生成するか、あるいはリスト6-8のように早期リターンすることもできます。(match でも同様のことができます。)
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn describe_state_quarter(coin: Coin) -> Option<String> {
let state = if let Coin::Quarter(state) = coin {
state
} else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
とはいえ、これにも別の意味で少し追いにくさがあります! if let の一方の分岐は値を生成し、もう一方は関数全体からリターンします。
この一般的なパターンをよりうまく表現するために、Rust には let...else があります。let...else 構文は、左辺にパターン、右辺に式を取り、if let によく似ていますが、if 分岐はなく、else 分岐だけがあります。パターンが一致した場合、パターンの値は外側のスコープに束縛されます。パターンが一致しない場合、プログラムの流れは else アームに入り、そのアームは関数からリターンしなければなりません。
リスト6-9では、if let の代わりに let...else を使ったときに、リスト6-8がどのようになるかを見ることができます。
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn describe_state_quarter(coin: Coin) -> Option<String> {
let Coin::Quarter(state) = coin else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
この方法では、if let がそうであったように2つの分岐で大きく異なる制御フローを持つことなく、関数本体の主な処理が“ハッピーパス”に留まっていることに注目してください。
プログラムに、match を使うと冗長すぎて表現しにくいロジックがあるなら、if let と let...else も Rust の道具箱に入っていることを覚えておいてください。
まとめ
ここまでで、列挙された値の集合のいずれかになり得るカスタム型を作るために enum を使う方法を見てきました。標準ライブラリの Option<T> 型が、型システムを使ってエラーを防ぐのに役立つことも示しました。enum の値の内部にデータがある場合は、処理する必要のあるケースの数に応じて、match または if let を使ってその値を取り出して利用できます。
これで、あなたの Rust プログラムは、struct や enum を使ってドメイン内の概念を表現できるようになりました。API で使うカスタム型を作成することで型安全性が確保されます。つまり、各関数が期待する型の値だけをその関数が受け取ることをコンパイラが保証してくれます。
ユーザーに、よく整理され、使いやすく、しかもユーザーが必要とするものだけを正確に公開する API を提供するために、次は Rust のモジュールに目を向けましょう。
パッケージ、クレート、モジュール
大規模なプログラムを書くようになると、コードを整理することの重要性はますます高まります。関連する機能をグループ化し、異なる機能ごとにコードを分けることで、特定の機能を実装しているコードがどこにあるのか、またその機能の動作を変更するにはどこを見ればよいのかが明確になります。
これまでに書いてきたプログラムは、1つのファイル内の1つのモジュールに収まっていました。プロジェクトが大きくなるにつれて、コードを複数のモジュールに分け、さらに複数のファイルに分割して整理すべきです。パッケージには複数のバイナリクレートと、必要に応じて1つのライブラリクレートを含めることができます。パッケージが成長するにつれて、その一部を外部依存関係となる別個のクレートに切り出すことができます。この章では、これらすべての手法を扱います。相互に関連し、ともに進化していく一連のパッケージから構成される非常に大規模なプロジェクト向けに、Cargo はワークスペースを提供しています。これについては、第14章の「Cargo Workspaces」で扱います。
また、実装の詳細をカプセル化することについても説明します。これにより、より高いレベルでコードを再利用できるようになります。いったんある操作を実装すれば、他のコードは、その実装がどのように動作するかを知らなくても、その公開インターフェースを通じてあなたのコードを呼び出せます。コードの書き方によって、他のコードが利用するために公開される部分と、変更する権利を保持する非公開の実装詳細となる部分が定義されます。これも、頭の中で保持しておかなければならない詳細の量を減らすための別の方法です。
関連する概念にスコープがあります。コードが書かれる入れ子になったコンテキストには、「スコープ内」と定義される名前の集合があります。コードを読んだり、書いたり、コンパイルしたりするとき、プログラマとコンパイラは、ある場所にある特定の名前が、変数、関数、構造体、列挙型、モジュール、定数、その他の項目のどれを指しているのか、そしてその項目が何を意味するのかを知る必要があります。スコープを作成し、どの名前がスコープ内にあるか、あるいはスコープ外にあるかを変更できます。同じスコープ内に同じ名前の項目を2つ持つことはできません。名前の衝突を解決するためのツールも用意されています。
Rust には、どの詳細を公開し、どの詳細を非公開にし、プログラム内の各スコープにどの名前を含めるかを含め、コードの構成を管理するための多くの機能があります。これらの機能は、ときにまとめて モジュールシステム と呼ばれ、次のものを含みます。
- パッケージ: クレートをビルド、テスト、共有できる Cargo の機能
- クレート: ライブラリまたは実行可能ファイルを生成するモジュールツリー
- モジュールと use: パスの構成、スコープ、可視性を制御するためのもの
- パス: 構造体、関数、モジュールなどの項目に名前を付ける方法
この章では、これらの機能をすべて取り上げ、それらがどのように相互作用するのかを説明し、スコープを管理するためにそれらをどう使うかを解説します。読み終える頃には、モジュールシステムをしっかり理解し、スコープを使いこなせるようになっているはずです!
パッケージとクレート
パッケージとクレート
モジュールシステムで最初に取り上げる要素は、パッケージとクレートです。
クレート は、Rust コンパイラが一度に対象とするコードの最小単位です。
第1章の 「Rustプログラムの基本」 で行ったように、cargo ではなく rustc を実行して 1 つのソースコード
ファイルだけを渡した場合でも、コンパイラはそのファイルを 1 つのクレートと
見なします。クレートにはモジュールを含めることができ、そのモジュールは、
これからの節で見るように、クレートとともにコンパイルされる別のファイルで
定義されていることもあります。
クレートには 2 つの形式があります。バイナリクレートとライブラリクレートです。
バイナリクレート は、コンパイルして実行可能ファイルにできるプログラムです。
たとえば、コマンドラインプログラムやサーバーがこれに当たります。どの
バイナリクレートにも、実行可能ファイルが動いたときに何が起こるかを定義する
main という関数がなければなりません。これまで作成してきたクレートは、
すべてバイナリクレートでした。
ライブラリクレート には main 関数がなく、実行可能ファイルにも
コンパイルされません。その代わり、複数のプロジェクトで共有することを意図した
機能を定義します。たとえば、第2章で使った rand クレート
rand は、乱数を生成する機能を提供します。多くの場合、
Rustacean が「crate」と言うとき、それはライブラリクレートを意味しており、
「crate」を一般的なプログラミングの概念である「ライブラリ」とほぼ同じ意味で
使っています。
クレートルート は、Rust コンパイラが処理を開始するソースファイルであり、 クレートのルートモジュールを構成するものです(モジュールについては 「モジュールでスコープとプライバシーを制御する」 で詳しく説明します)。
パッケージ は、一連の機能を提供する 1 つ以上のクレートをまとめたものです。 パッケージには、それらのクレートをどのようにビルドするかを記述した Cargo.toml ファイルが含まれます。Cargo は実際には、これまでコードの ビルドに使ってきたコマンドラインツール用のバイナリクレートを含む パッケージです。Cargo パッケージには、バイナリクレートが依存する ライブラリクレートも含まれています。他のプロジェクトは Cargo の ライブラリクレートに依存することで、Cargo コマンドラインツールが使っている のと同じロジックを利用できます。
パッケージには、好きなだけ多くのバイナリクレートを含めることができますが、 ライブラリクレートは多くても 1 つだけです。パッケージには、ライブラリ クレートであれバイナリクレートであれ、少なくとも 1 つのクレートが 含まれていなければなりません。
では、パッケージを作成したときに何が起こるのかを見ていきましょう。まず、
cargo new my-project というコマンドを入力します。
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
cargo new my-project を実行したあと、Cargo が何を作成したかを見るために
ls を使います。my-project ディレクトリには、パッケージを構成する
Cargo.toml ファイルがあります。また、main.rs を含む src
ディレクトリもあります。テキストエディタで Cargo.toml を開くと、
そこには src/main.rs への記述がないことがわかります。Cargo は、
src/main.rs がパッケージと同じ名前を持つバイナリクレートの
クレートルートであるという慣習に従います。同様に、パッケージディレクトリに
src/lib.rs が含まれていれば、そのパッケージにはパッケージと同じ名前の
ライブラリクレートが含まれており、src/lib.rs がそのクレートルートで
あることを Cargo は認識します。Cargo は、ライブラリまたはバイナリを
ビルドするために、クレートルートファイルを rustc に渡します。
ここでは、src/main.rs だけを含むパッケージがあり、つまり
my-project という名前のバイナリクレートだけを含んでいます。パッケージに
src/main.rs と src/lib.rs の両方が含まれている場合、そのパッケージには
2 つのクレート、すなわちバイナリクレートとライブラリクレートがあり、
どちらもパッケージと同じ名前を持ちます。src/bin ディレクトリに
ファイルを置くことで、パッケージは複数のバイナリクレートを持てます。
各ファイルは個別のバイナリクレートになります。
モジュールでスコープと可視性を制御する
モジュールでスコープと公開範囲を制御する
この節では、モジュールと、モジュールシステムのそのほかの要素について説明します。具体的には、アイテムに名前を付けられるようにする パス、パスをスコープに導入する use キーワード、そしてアイテムを公開にする pub キーワードです。また、as キーワード、外部パッケージ、グロブ演算子についても説明します。
モジュールのチートシート
モジュールとパスの詳細に入る前に、ここでは、モジュール、パス、use キーワード、pub キーワードがコンパイラ内でどのように動作するのか、そしてほとんどの開発者がどのようにコードを整理しているのかについて、簡単なリファレンスを示します。この章では、これらの各規則の例をひとつずつ見ていきますが、モジュールの動作を思い出すための参照先としてここは最適です。
- クレートルートから始める: クレートをコンパイルするとき、コンパイラはまず、コンパイルするコードをクレートルートファイル(通常、ライブラリクレートでは src/lib.rs、バイナリクレートでは src/main.rs)で探します。
- モジュールを宣言する: クレートルートファイルでは、新しいモジュールを宣言できます。たとえば、
mod garden;で「garden」モジュールを宣言したとします。コンパイラは、そのモジュールのコードを次の場所で探します。- インライン。
mod gardenの後ろにあるセミコロンの代わりに波括弧で囲まれた部分 - ファイル src/garden.rs
- ファイル src/garden/mod.rs
- インライン。
- サブモジュールを宣言する: クレートルート以外のどのファイルでも、サブモジュールを宣言できます。たとえば、src/garden.rs で
mod vegetables;を宣言できます。コンパイラは、そのサブモジュールのコードを、親モジュールの名前が付いたディレクトリ内の次の場所で探します。- インライン。セミコロンの代わりに、
mod vegetablesの直後に続く波括弧内 - ファイル src/garden/vegetables.rs
- ファイル src/garden/vegetables/mod.rs
- インライン。セミコロンの代わりに、
- モジュール内のコードへのパス: いったんモジュールがクレートの一部になれば、公開範囲の規則が許す限り、同じクレート内のほかのどこからでも、そのコードへのパスを使ってそのモジュール内のコードを参照できます。たとえば、garden の vegetables モジュールにある
Asparagus型は、crate::garden::vegetables::Asparagusにあります。 - 非公開と公開: モジュール内のコードは、デフォルトでは親モジュールから非公開です。モジュールを公開にするには、
modではなくpub modで宣言します。公開モジュール内のアイテムも公開にするには、それらの宣言の前にpubを付けます。 useキーワード: スコープ内で、useキーワードはアイテムへのショートカットを作成し、長いパスの繰り返しを減らします。crate::garden::vegetables::Asparagusを参照できる任意のスコープでは、use crate::garden::vegetables::Asparagus;でショートカットを作成でき、その後はスコープ内でその型を使うためにAsparagusとだけ書けばよくなります。
ここでは、これらの規則を説明する backyard という名前のバイナリクレートを作成します。クレートのディレクトリも backyard という名前で、次のファイルとディレクトリが含まれています。
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden
│ └── vegetables.rs
├── garden.rs
└── main.rs
この場合のクレートルートファイルは src/main.rs で、次の内容が含まれています。
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {plant:?}!");
}
この pub mod garden; 行は、src/garden.rs にあるコードを取り込むようコンパイラに伝えています。内容は次のとおりです。
pub mod vegetables;
ここで、pub mod vegetables; は src/garden/vegetables.rs のコードも取り込まれることを意味します。そのコードは次のとおりです。
#[derive(Debug)]
pub struct Asparagus {}
では、これらの規則の詳細に入り、実際にそれらがどう動くかを見ていきましょう!
モジュールで関連するコードをグループ化する
モジュール を使うと、可読性と再利用のしやすさのために、クレート内のコードを整理できます。モジュールを使うと、アイテムの 公開範囲 も制御できます。というのも、モジュール内のコードはデフォルトで非公開だからです。非公開アイテムは、外部から利用できない内部実装の詳細です。モジュールやその内部のアイテムを公開にすることも選べます。そうすると、それらが公開され、外部のコードから利用したり依存したりできるようになります。
例として、レストランの機能を提供するライブラリクレートを書いてみましょう。コードの構成に集中するため、関数シグネチャは定義しますが、その本体は空のままにしておきます。
レストラン業界では、レストランの一部はホール、別の一部はバックヤードと呼ばれます。ホール は客がいる場所です。そこには、案内係が客を席へ案内する場所、給仕係が注文や会計を受ける場所、バーテンダーが飲み物を作る場所が含まれます。バックヤード は、シェフや調理師が厨房で働き、皿洗い係が片付けをし、マネージャーが事務作業を行う場所です。
クレートをこのように構造化するには、その機能を入れ子のモジュールに整理できます。cargo new restaurant --lib を実行して、restaurant という名前の新しいライブラリを作成してください。次に、いくつかのモジュールと関数シグネチャを定義するために、リスト7-1のコードを src/lib.rs に入力してください。このコードがホール側の部分です。
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
モジュールは、mod キーワードの後ろにモジュール名(この場合は front_of_house)を続けることで定義します。モジュール本体はその後、波括弧の中に入ります。モジュールの中には、今回の hosting モジュールや serving モジュールのように、さらに別のモジュールを置けます。モジュールには、構造体、列挙型、定数、トレイト、そしてリスト7-1のように関数など、ほかのアイテムの定義を置くこともできます。
モジュールを使うことで、関連する定義をひとまとめにし、なぜそれらが関連しているのかを名前で表せます。このコードを使うプログラマは、すべての定義を読み通す必要はなく、グループに基づいてコードをたどれるため、自分に関係のある定義を見つけやすくなります。このコードに新しい機能を追加するプログラマも、プログラムを整理された状態に保つために、コードをどこに置けばよいかがわかります。
先ほど、src/main.rs と src/lib.rs は クレートルート と呼ばれると述べました。そう呼ばれる理由は、これら2つのファイルのいずれかの内容が、クレートのモジュール構造の根にある crate という名前のモジュールを形成するからです。この構造は モジュールツリー として知られています。
リスト7-2は、リスト7-1の構造に対するモジュールツリーを示しています。
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
このツリーは、いくつかのモジュールが他のモジュールの内側に入れ子になっていることを示しています。たとえば、hosting は front_of_house の内側に入れ子になっています。また、このツリーは一部のモジュールが 兄弟 であることも示しています。つまり、同じモジュール内で定義されているということです。hosting と serving は、front_of_house 内で定義された兄弟です。モジュールAがモジュールBの内側に含まれている場合、モジュールAはモジュールBの 子 であり、モジュールBはモジュールAの 親 であると言います。モジュールツリー全体が、暗黙の crate という名前のモジュールの下を根としていることに注意してください。
このモジュールツリーは、コンピュータ上のファイルシステムのディレクトリツリーを思い出させるかもしれません。これはとても的確な比較です!ファイルシステム内のディレクトリと同じように、コードを整理するためにモジュールを使います。そして、ディレクトリ内のファイルと同じように、モジュールを見つける方法が必要です。
モジュールツリー内の要素を参照するためのパス
モジュールツリー内のアイテムを参照するためのパス
Rust に対してモジュールツリー内のどこにアイテムがあるかを示すには、 ファイルシステムを移動するときにパスを使うのと同じようにパスを使います。関数を呼び出すには、 そのパスを知っている必要があります。
パスには 2 つの形式があります。
- 絶対パス はクレートルートから始まる完全なパスです。外部クレートの
コードに対しては、絶対パスはクレート名から始まり、現在のクレートの
コードに対しては、リテラルの
crateから始まります。 - 相対パス は現在のモジュールから始まり、
self、super、または 現在のモジュール内の識別子を使います。
絶対パスと相対パスのどちらも、二重コロン (::) で区切られた 1 つ以上の識別子が続きます。
リスト 7-1 に戻り、add_to_waitlist 関数を呼び出したいとしましょう。
これはつまり、add_to_waitlist 関数のパスは何かと尋ねているのと同じです。
リスト 7-3 には、いくつかのモジュールと関数を削除したリスト 7-1 を示しています。
クレートルートで定義された新しい関数 eat_at_restaurant から
add_to_waitlist 関数を呼び出す 2 つの方法を示します。これらのパスは正しいのですが、
この例がこのままではコンパイルできない別の問題がまだ残っています。
その理由は少し後で説明します。
eat_at_restaurant 関数は、私たちのライブラリクレートの公開 API の一部なので、
pub キーワードを付けています。「pub キーワードでパスを公開する」
の節では、pub についてさらに詳しく説明します。
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
eat_at_restaurant で最初に add_to_waitlist 関数を呼び出すときには、
絶対パスを使います。add_to_waitlist 関数は eat_at_restaurant と同じ
クレートで定義されているので、絶対パスの先頭に crate キーワードを
使えます。その後、add_to_waitlist にたどり着くまで、順に各モジュールを含めていきます。
同じ構造のファイルシステムを想像してみてください。add_to_waitlist
プログラムを実行するには、/front_of_house/hosting/add_to_waitlist という
パスを指定するはずです。クレートルートから始めるためにクレート名を使うことは、
シェルでファイルシステムルートから始めるために / を使うのに似ています。
eat_at_restaurant で 2 回目に add_to_waitlist を呼び出すときには、
相対パスを使います。パスは front_of_house から始まります。これは
eat_at_restaurant と同じレベルのモジュールツリーで定義されているモジュール名です。
ファイルシステムでこれに相当するのは、
front_of_house/hosting/add_to_waitlist というパスを使うことです。モジュール名から
始めるということは、そのパスが相対パスであることを意味します。
相対パスと絶対パスのどちらを使うかは、プロジェクトに応じて決めることになります。
そしてそれは、アイテムを定義しているコードを、そのアイテムを使うコードとは
別に移動する可能性が高いか、それとも一緒に移動する可能性が高いかによって変わります。
たとえば、front_of_house モジュールと eat_at_restaurant 関数を
customer_experience という名前のモジュールに移動した場合、
add_to_waitlist への絶対パスは更新する必要がありますが、相対パスは
依然として有効です。一方で、eat_at_restaurant 関数だけを別個に
dining という名前のモジュールへ移動した場合、
add_to_waitlist 呼び出しへの絶対パスはそのままですが、相対パスは
更新する必要があります。一般に私たちは絶対パスを指定するほうを好みます。というのも、
コード定義とアイテム呼び出しは互いに独立して移動したくなる可能性のほうが高いからです。
リスト 7-3 をコンパイルして、なぜまだコンパイルできないのかを確認してみましょう。 得られるエラーをリスト 7-4 に示します。
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
error[E0603]: module `hosting` is private
--> src/lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
エラーメッセージは、モジュール hosting が非公開だと伝えています。言い換えると、
hosting モジュールと add_to_waitlist 関数へのパス自体は正しいものの、
Rust は非公開の部分にアクセスできないため、それらを使わせてくれません。Rust では、
すべてのアイテム(関数、メソッド、構造体、列挙型、モジュール、定数)は、
デフォルトでは親モジュールに対して非公開です。関数や構造体のようなアイテムを
非公開にしたいなら、それをモジュールに入れます。
親モジュール内のアイテムは子モジュール内の非公開アイテムを使えませんが、 子モジュール内のアイテムは祖先モジュール内のアイテムを使えます。これは、 子モジュールがその実装の詳細を包み隠す一方で、子モジュール自身は 定義されている文脈を見ることができるからです。この比喩を続けるなら、 プライバシールールはレストランのバックオフィスのようなものだと考えてください。 その中で起こっていることはレストランの客には非公開ですが、 事務所の管理者は自分が運営するレストランのすべてを見たり行ったりできます。
Rust がモジュールシステムをこのように機能させるよう選んだのは、
内部実装の詳細を隠すことがデフォルトになるようにするためです。そうすることで、
外側のコードを壊さずに内部コードのどの部分を変更できるかがわかります。
しかし Rust では、pub キーワードを使ってアイテムを公開にすることで、
子モジュールのコードの内部部分を外側の祖先モジュールに公開するという選択肢も用意されています。
pub キーワードでパスを公開する
hosting モジュールが非公開だと教えてくれたリスト 7-4 のエラーに戻りましょう。
親モジュール内の eat_at_restaurant 関数から、子モジュール内の
add_to_waitlist 関数にアクセスできるようにしたいので、リスト 7-5 に示すように
hosting モジュールに pub キーワードを付けます。
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
// -- snip --
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
残念ながら、リスト 7-5 のコードでも、リスト 7-6 に示すように コンパイラエラーが発生します。
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:10:37
|
10 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:13:30
|
13 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
何が起きたのでしょうか。mod hosting の前に pub キーワードを付けると、
そのモジュールは公開になります。この変更により、front_of_house にアクセスできれば、
hosting にもアクセスできます。しかし、hosting の 中身 は依然として
非公開です。モジュールを公開にしても、その中身まで公開になるわけではありません。
モジュールに付いた pub キーワードは、祖先モジュール内のコードがそのモジュールを
参照できるようにするだけで、その内部コードにアクセスできるようにするものではありません。
モジュールはコンテナなので、モジュールを公開にするだけでできることはあまりありません。
さらに進んで、そのモジュール内の 1 つ以上のアイテムも公開にする必要があります。
リスト 7-6 のエラーは、add_to_waitlist 関数が非公開だと言っています。
プライバシールールは、モジュールだけでなく、構造体、列挙型、関数、メソッドにも適用されます。
add_to_waitlist 関数も、リスト 7-7 のように、その定義の前に pub
キーワードを追加して公開にしましょう。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// -- snip --
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
これでコードはコンパイルできるようになりました! なぜ pub キーワードを
追加すると、可視性ルールに照らして eat_at_restaurant でこれらのパスを使える
ようになるのかを理解するために、絶対パスと相対パスを見ていきましょう。
絶対パスでは、クレートのモジュールツリーのルートである crate から始めます。
front_of_house モジュールはクレートのルートで定義されています。
front_of_house 自体は公開されていませんが、eat_at_restaurant 関数は
front_of_house と同じモジュールで定義されているため(つまり、
eat_at_restaurant と front_of_house は兄弟関係にあるため)、
eat_at_restaurant から front_of_house を参照できます。次に来るのは
pub が付いた hosting モジュールです。hosting の親モジュールには
アクセスできるので、hosting にアクセスできます。最後に、
add_to_waitlist 関数には pub が付いており、その親モジュールにも
アクセスできるので、この関数呼び出しは機能します!
相対パスでも、最初の一歩を除けばロジックは絶対パスと同じです。クレートの
ルートから始めるのではなく、パスは front_of_house から始まります。
front_of_house モジュールは eat_at_restaurant と同じモジュール内で
定義されているので、eat_at_restaurant が定義されているモジュールから
始まる相対パスは機能します。さらに、hosting と add_to_waitlist には
pub が付いているため、パスの残りの部分も機能し、この関数呼び出しは
有効です!
ライブラリクレートを共有して他のプロジェクトからコードを使えるようにする 予定があるなら、その公開APIはクレートの利用者との契約であり、利用者が あなたのコードとどのようにやり取りできるかを定めるものです。人々が あなたのクレートに依存しやすくなるように公開APIの変更を管理する際には、 考慮すべき点が数多くあります。これらの考慮事項はこの本の範囲を超える ので、この話題に興味があるなら、Rust API ガイドラインを 参照してください。
バイナリとライブラリを持つパッケージのベストプラクティス
既に説明したように、パッケージには src/main.rs のバイナリクレートの ルートと src/lib.rs のライブラリクレートのルートの両方を含めることが でき、どちらのクレートもデフォルトではパッケージ名を持ちます。通常、 ライブラリとバイナリクレートの両方を含むこのパターンのパッケージでは、 バイナリクレートには実行可能ファイルを起動し、ライブラリクレートで 定義されたコードを呼び出すのに必要な最小限のコードだけを置きます。 こうすることで、ライブラリクレートのコードを共有できるため、その パッケージが提供する機能の大部分を他のプロジェクトでも利用できる ようになります。
モジュールツリーは src/lib.rs で定義すべきです。そうすれば、公開された アイテムはすべて、パスをパッケージ名から始めることでバイナリクレート から使用できます。バイナリクレートは、完全に外部のクレートが ライブラリクレートを使うのと同じように、ライブラリクレートの利用者に なります。つまり、公開APIしか使えません。これは良いAPIを設計する助けに なります。というのも、あなたは作者であるだけでなく、クライアントでも あるからです!
第12章 では、この構成方法を、バイナリクレートと ライブラリクレートの両方を含むコマンドラインプログラムで実演します。
super で始まる相対パス
現在のモジュールやクレートルートではなく、親モジュールから始まる相対パスを
構築するには、パスの先頭に super を使います。これは、親ディレクトリへ
移動することを意味する .. 構文でファイルシステムのパスを始めるのに似て
います。super を使うことで、親モジュールにあると分かっているアイテムを
参照できるため、あるモジュールが親と密接に関係している一方で、その親が
将来モジュールツリー内の別の場所へ移されるかもしれない場合に、モジュール
ツリーの再編成が容易になることがあります。
Listing 7-8 のコードは、シェフが間違った注文を修正し、自ら客のところへ
運ぶ状況をモデル化したものです。back_of_house モジュールで定義された
fix_incorrect_order 関数は、super で始まる deliver_order への
パスを指定することで、親モジュールで定義された deliver_order 関数を
呼び出します。
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
fix_incorrect_order 関数は back_of_house モジュール内にあるので、
super を使って back_of_house の親モジュール、この場合はルートである
crate に移動できます。そこから deliver_order を探すと見つかります。
うまくいきます! back_of_house モジュールと deliver_order 関数は、
互いに対して同じ関係のまま保たれ、クレートのモジュールツリーを再編成する
ことになっても一緒に移動される可能性が高いと考えられます。そのため、この
コードが将来別のモジュールに移された場合でも更新すべき箇所が少なくて済む
ように、super を使いました。
構造体と列挙型を公開する
pub を使って構造体や列挙型を公開にすることもできますが、構造体や列挙型に
対する pub の使い方にはいくつか追加の細かい点があります。構造体定義の前に
pub を付けると、その構造体自体は公開になりますが、構造体のフィールドは
引き続き非公開のままです。各フィールドを公開にするかどうかは、
ケースバイケースで決められます。Listing 7-9 では、公開された toast
フィールドと非公開の seasonal_fruit フィールドを持つ、公開の
back_of_house::Breakfast 構造体を定義しています。これはレストランで、
客は食事に付くパンの種類は選べるが、どの果物を添えるかは旬かどうかや
在庫状況に基づいてシェフが決める、という状況を表しています。提供される
果物はすぐに変わるので、客は果物を選べず、どの果物が出てくるのかを
見ることさえできません。
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
// Order a breakfast in the summer with Rye toast.
let mut meal = back_of_house::Breakfast::summer("Rye");
// Change our mind about what bread we'd like.
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
// The next line won't compile if we uncomment it; we're not allowed
// to see or modify the seasonal fruit that comes with the meal.
// meal.seasonal_fruit = String::from("blueberries");
}
back_of_house::Breakfast 構造体の toast フィールドは公開されているため、
eat_at_restaurant ではドット記法を使って toast フィールドに書き込みも
読み取りもできます。seasonal_fruit は非公開なので、
eat_at_restaurant では seasonal_fruit フィールドを使えないことに注意して
ください。どのようなエラーが出るか確認するために、seasonal_fruit
フィールドの値を変更している行のコメントを外してみてください!
また、back_of_house::Breakfast には非公開フィールドがあるため、
Breakfast のインスタンスを構築する公開の関連関数を構造体側で提供する
必要があります(ここでは summer という名前にしています)。Breakfast に
そのような関数がなければ、eat_at_restaurant では Breakfast の
インスタンスを作成できません。なぜなら、eat_at_restaurant では非公開の
seasonal_fruit フィールドの値を設定できないからです。
対照的に、列挙型を公開にすると、そのすべてのバリアントも公開になります。
Listing 7-10 に示すように、必要なのは enum キーワードの前に pub を
付けることだけです。
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
Appetizer enum を公開にしたので、eat_at_restaurant では Soup と Salad
のバリアントを使えます。
バリアントが公開でなければ、enum はあまり有用ではありません。あらゆる場合に
すべての enum バリアントへ pub を付けなければならないのは煩雑なので、enum
バリアントはデフォルトで公開です。一方、struct はフィールドが公開でなくても
有用なことがよくあるため、struct のフィールドは、pub で注釈しない限り
すべてがデフォルトで非公開であるという一般的な規則に従います。
まだ扱っていない pub が関わる状況がもう 1 つあり、それがモジュール
システムにおける最後の機能である use キーワードです。まずは use 単体を
取り上げ、その後で pub と use を組み合わせる方法を示します。
use キーワードでパスをスコープに持ち込む
use キーワードでパスをスコープに導入する
関数を呼び出すたびにパスをすべて書き出さなければならないのは、不便で
繰り返しが多いと感じるかもしれません。リスト 7-7 では、add_to_waitlist
関数への絶対パスと相対パスのどちらを選んだとしても、add_to_waitlist
を呼び出すたびに front_of_house と hosting も指定しなければなりま
せんでした。幸い、この手順を単純化する方法があります。use
キーワードを使って一度パスへのショートカットを作成すれば、そのスコープ
の他の場所では短い名前を使えます。
リスト 7-11 では、crate::front_of_house::hosting モジュールを
eat_at_restaurant 関数のスコープに導入しているので、
hosting::add_to_waitlist とだけ指定すれば、eat_at_restaurant で
add_to_waitlist 関数を呼び出せます。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
スコープ内に use とパスを追加することは、ファイルシステムで
シンボリックリンクを作成することに似ています。クレートルートに
use crate::front_of_house::hosting を追加すると、あたかも hosting
モジュールがクレートルートで定義されていたかのように、そのスコープ内で
hosting は有効な名前になります。use でスコープに導入されたパスも、
他のパスと同じように可視性のチェックを受けます。
use は、その use が現れる特定のスコープに対してのみショートカットを
作成することに注意してください。リスト 7-12 では、eat_at_restaurant
関数を customer という新しい子モジュールに移動しています。この
子モジュールは use 文とは異なるスコープになるため、関数本体は
コンパイルできません。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}
コンパイラエラーは、このショートカットが
customer モジュール内ではもはや適用されないことを示しています。
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0433]: failed to resolve: use of unresolved module or unlinked crate `hosting`
--> src/lib.rs:11:9
|
11 | hosting::add_to_waitlist();
| ^^^^^^^ use of unresolved module or unlinked crate `hosting`
|
= help: if you wanted to use a crate named `hosting`, use `cargo add hosting` to add it to your `Cargo.toml`
help: consider importing this module through its public re-export
|
10 + use crate::hosting;
|
warning: unused import: `crate::front_of_house::hosting`
--> src/lib.rs:7:5
|
7 | use crate::front_of_house::hosting;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning emitted
さらに、その use がそのスコープ内でもはや使われていないという警告も
出ていることに注目してください。これを修正するには、customer
モジュールの中にも use を移動するか、子モジュール
customer の中で super::hosting を使って親モジュール内の
ショートカットを参照します。
慣用的な use パスを作成する
リスト 7-11 では、なぜ use crate::front_of_house::hosting と指定してから hosting::add_to_waitlist を
eat_at_restaurant の中で呼び出しているのか、同じ結果を得るために
リスト 7-13 のように use のパスを add_to_waitlist 関数まで指定
しないのはなぜか、と疑問に思ったかもしれません。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting::add_to_waitlist;
pub fn eat_at_restaurant() {
add_to_waitlist();
}
リスト 7-11 とリスト 7-13 はどちらも同じことを実現しますが、リスト
7-11 のほうが関数を use でスコープに導入する慣用的な方法です。use
で関数の親モジュールをスコープに導入すると、関数を呼び出すときに
親モジュールを指定する必要があります。関数呼び出し時に親モジュールを
指定すると、その関数がローカルで定義されているわけではないことが明確に
なる一方で、完全なパスの繰り返しは最小限に抑えられます。リスト 7-13 の
コードでは、add_to_waitlist がどこで定義されているのかが不明確です。
一方で、構造体、列挙型、その他の項目を use で導入する場合は、
完全なパスを指定するのが慣用的です。リスト 7-14 は、標準ライブラリの
HashMap 構造体をバイナリクレートのスコープに導入する慣用的な方法
を示しています。
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
この慣習に強い理由があるわけではありません。単にそうした慣例が 生まれ、人々がこの書き方の Rust コードを読み書きすることに慣れた だけです。
この慣習の例外は、use 文で同じ名前を持つ 2 つの項目を
スコープに導入する場合です。Rust ではそれが許されないためです。リスト 7-15
は、同じ名前を持ちながら親モジュールが異なる 2 つの Result 型を
スコープに導入する方法と、それらを参照する方法を示しています。
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
Ok(())
}
fn function2() -> io::Result<()> {
// --snip--
Ok(())
}
見てのとおり、親モジュールを使うことで 2 つの Result 型を区別できます。
これに対して、use std::fmt::Result と use std::io::Result を指定すると、
同じスコープに 2 つの Result 型が存在することになり、Result を
使ったときに Rust はどちらを意味しているのか判断できません。
as キーワードで新しい名前を与える
同じ名前の 2 つの型を
use で同じスコープに導入する問題には、別の解決策があります。パスの後に as と新しい
ローカル名、つまり エイリアス を指定して、その型に別名を与えることが
できます。リスト 7-16 は、2 つの Result 型のうち一方の名前を as を使って変更することで、
リスト 7-15 のコードを別の形で書く方法を示しています。
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
Ok(())
}
2 番目の use 文では、std::io::Result 型に対して新しい名前 IoResult を
選んでおり、これなら同じくスコープに導入した std::fmt
の Result と衝突しません。リスト 7-15 とリスト 7-16 は
どちらも慣用的とみなされるので、どちらを選ぶかはあなた次第です。
pub use で名前を再エクスポートする
use キーワードで名前をスコープに導入すると、その名前は導入先の
スコープに対してプライベートになります。そのスコープの外のコードから
も、その名前があたかもそのスコープで定義されているかのように参照
できるようにするには、pub と use を組み合わせます。この手法は、
項目をスコープに導入すると同時に、その項目を他のコードも自分の
スコープに導入できるようにしているため、再エクスポート と呼ばれます。
リスト 7-17 は、ルートモジュール内の use を
pub use に変更したリスト 7-11 のコードを示しています。
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
この変更前は、外部コードは add_to_waitlist
関数を
restaurant::front_of_house::hosting::add_to_waitlist() というパスで呼び出す必要があり、
そのためには front_of_house モジュールも pub としてマーク
されていなければなりませんでした。この pub use によって hosting モジュールがルートモジュールから再エクスポートされたので、外部コード
は代わりに restaurant::hosting::add_to_waitlist() というパスを使えます。
再エクスポートは、コードの内部構造が、そのコードを呼び出すプログラマがそのドメインをどう捉えるかと異なる場合に役立ちます。たとえば、このレストランの比喩では、レストランを運営する人たちは「front of house」と「back of house」という観点で考えます。しかし、レストランを訪れる客は、おそらくそのような言い方でレストランの各部分を捉えないでしょう。pub use を使うと、コードはある構造で書きつつ、外部には別の構造を公開できます。こうすることで、ライブラリを開発するプログラマにとっても、そのライブラリを呼び出すプログラマにとっても、よく整理されたライブラリになります。pub use の別の例と、それがクレートのドキュメントにどう影響するかについては、第 14 章の 「便利な公開 API をエクスポートする」 で見ていきます。
外部パッケージを使う
第 2 章では、乱数を取得するために rand という外部パッケージを使う推測ゲームのプロジェクトを作成しました。プロジェクトで rand を使うために、Cargo.toml に次の行を追加しました。
rand = "0.8.5"
Cargo.toml に依存関係として rand を追加すると、Cargo は crates.io から rand パッケージとその依存関係をダウンロードし、rand を私たちのプロジェクトで利用できるようにします。
次に、rand の定義を私たちのパッケージのスコープに導入するために、クレート名である rand から始まる use 行を追加し、スコープに導入したい要素を列挙しました。思い出してください。第 2 章の 「乱数を生成する」 では、Rng トレイトをスコープに導入し、rand::thread_rng 関数を呼び出しました。
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
Rust コミュニティのメンバーは、crates.io で多くのパッケージを公開しており、そのどれを自分のパッケージに取り込む場合でも、手順は同じです。つまり、それらを自分のパッケージの Cargo.toml ファイルに列挙し、use を使ってそれらのクレートから要素をスコープに導入します。
標準の std ライブラリも、私たちのパッケージの外部にあるクレートであることに注意してください。標準ライブラリは Rust 言語と一緒に配布されるため、std を含めるために Cargo.toml を変更する必要はありません。しかし、そこから要素を私たちのパッケージのスコープに導入するには、use で参照する必要があります。たとえば、HashMap の場合は次の行を使います。
#![allow(unused)]
fn main() {
use std::collections::HashMap;
}
これは、標準ライブラリのクレート名である std から始まる絶対パスです。
use リストを整理するためにネストしたパスを使う
同じクレートや同じモジュールで定義された複数の要素を使う場合、それぞれの要素を別々の行に書くと、ファイル内でかなりの縦方向のスペースを使ってしまうことがあります。たとえば、リスト 2-4 の推測ゲームにあった次の 2 つの use 文は、std から要素をスコープに導入しています。
use rand::Rng;
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
その代わりに、ネストしたパスを使って、同じ要素を 1 行でスコープに導入できます。これは、パスの共通部分を指定し、その後に 2 つのコロンを書き、さらに異なる部分の一覧を波かっこで囲むことで行います。リスト 7-18 に示すとおりです。
use rand::Rng;
// --snip--
use std::{cmp::Ordering, io};
// --snip--
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
より大きなプログラムでは、同じクレートやモジュールから多くの要素をネストしたパスでスコープに導入することで、必要な use 文の数を大幅に減らせます。
ネストしたパスは、パスのどのレベルでも使えます。これは、サブパスを共有する 2 つの use 文をまとめるときに便利です。たとえば、リスト 7-19 には 2 つの use 文があります。1 つは std::io をスコープに導入し、もう 1 つは std::io::Write をスコープに導入しています。
use std::io;
use std::io::Write;
この 2 つのパスの共通部分は std::io であり、それが 1 つ目の完全なパスです。この 2 つのパスを 1 つの use 文にまとめるには、リスト 7-20 に示すように、ネストしたパスの中で self を使えます。
use std::io::{self, Write};
この行は、std::io と std::io::Write をスコープに導入します。
グロブ演算子で要素をインポートする
あるパスで定義されている すべての 公開要素をスコープに導入したい場合は、そのパスの後ろに * グロブ演算子を付けて指定できます。
#![allow(unused)]
fn main() {
use std::collections::*;
}
この use 文は、std::collections で定義されているすべての公開要素を現在のスコープに導入します。グロブ演算子を使うときは注意してください。Glob を使うと、どの名前がスコープに入っているのか、またプログラム内で使っている名前がどこで定義されたのかが分かりにくくなります。さらに、依存関係側の定義が変わると、インポートされる内容も変わります。そのため、たとえば依存関係が、同じスコープ内にあるあなた自身の定義と同名の定義を追加した場合、依存関係をアップグレードしたときにコンパイラエラーにつながる可能性があります。
グロブ演算子は、テスト時にテスト対象のすべてを tests モジュールに導入するためによく使われます。これについては、第 11 章の 「テストの書き方」 で説明します。グロブ演算子は、prelude パターンの一部として使われることもあります。そのパターンについて詳しくは、標準ライブラリのドキュメント を参照してください。
モジュールを別々のファイルに分割する
モジュールを別々のファイルに分ける
ここまで、この章のすべての例では、1つのファイルの中で複数のモジュールを定義してきました。 モジュールが大きくなってくると、コードをたどりやすくするために、それらの定義を別の ファイルに移したくなることがあります。
たとえば、複数の restaurant モジュールがあったリスト 7-17 のコードから始めましょう。
すべてのモジュールをクレートルートファイルで定義するのではなく、モジュールをファイルに
切り出します。この場合、クレートルートファイルは src/lib.rs ですが、この手順は
クレートルートファイルが src/main.rs であるバイナリクレートでも機能します。
まず、front_of_house モジュールを専用のファイルに切り出します。front_of_house
モジュールの波かっこ内のコードを削除し、mod front_of_house; 宣言だけを残して、
src/lib.rs がリスト 7-21 に示すコードを含むようにします。なお、リスト 7-22 の
src/front_of_house.rs ファイルを作成するまでは、これはコンパイルされません。
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
次に、波かっこ内にあったコードを、リスト 7-22 に示すように
src/front_of_house.rs という新しいファイルに置きます。コンパイラは、この
ファイルを探すべきことを知っています。というのも、クレートルートで
front_of_house という名前のモジュール宣言に出会っているからです。
pub mod hosting {
pub fn add_to_waitlist() {}
}
モジュールツリーの中で、ファイルを mod 宣言によって読み込む必要があるのは 一度だけ であることに注意してください。いったんコンパイラがそのファイルがプロジェクトの一部であることを知り(さらに、mod 文を置いた場所によって、そのコードがモジュールツリーのどこに属するのかも把握し)、プロジェクト内のほかのファイルは、「モジュールツリー内の項目を参照するためのパス」 節で説明したように、そのコードが宣言された場所へのパスを使って、読み込まれたファイルのコードを参照すべきです。言い換えると、mod は、ほかのプログラミング言語で見たことがあるかもしれない “include” 操作 ではありません。
次に、hosting モジュールを専用のファイルに切り出します。hosting はルート
モジュールではなく front_of_house の子モジュールなので、手順は少し異なります。
hosting 用のファイルは、モジュールツリー内のその祖先にちなんだ名前の新しい
ディレクトリに置きます。この場合は src/front_of_house です。
hosting の移動を始めるために、src/front_of_house.rs を、hosting
モジュールの宣言だけを含むように変更します。
pub mod hosting;
次に、src/front_of_house ディレクトリと hosting.rs ファイルを作成し、
hosting モジュール内で行われていた定義を含めます。
pub fn add_to_waitlist() {}
もし代わりに hosting.rs を src ディレクトリに置いた場合、コンパイラは
hosting.rs のコードがクレートルートで宣言された hosting モジュールに属すると
期待し、front_of_house モジュールの子として宣言されたものとは見なしません。
どのモジュールのコードについてどのファイルを調べるかというコンパイラの規則により、
ディレクトリとファイルはモジュールツリーにより近い形で対応することになります。
代替のファイルパス
ここまでは、Rust コンパイラが使うもっとも慣用的なファイルパスを扱ってきましたが、 Rust は古いスタイルのファイルパスもサポートしています。クレートルートで宣言された
front_of_houseという名前のモジュールについて、コンパイラはそのモジュールの コードを次の場所で探します。
- src/front_of_house.rs(ここまでで扱ったもの)
- src/front_of_house/mod.rs(古いスタイルですが、現在もサポートされているパス)
front_of_houseのサブモジュールであるhostingという名前のモジュールについて、 コンパイラはそのモジュールのコードを次の場所で探します。
- src/front_of_house/hosting.rs(ここまでで扱ったもの)
- src/front_of_house/hosting/mod.rs(古いスタイルですが、現在もサポートされているパス)
同じモジュールに対して両方のスタイルを使うと、コンパイラエラーになります。 同じプロジェクト内で異なるモジュールごとに両方のスタイルを混在させることは 可能ですが、プロジェクトをたどる人にとってはわかりにくくなるかもしれません。
mod.rs という名前のファイルを使うスタイルの主な欠点は、プロジェクト内に mod.rs という名前のファイルがたくさんできてしまい、エディタで同時に開いていると 混乱しやすいことです。
各モジュールのコードを別々のファイルに移動しましたが、モジュールツリーは同じままです。
定義が別のファイルに存在していても、eat_at_restaurant 内の関数呼び出しは何も変更せずに
動作します。この手法により、モジュールの規模が大きくなるにつれて、新しいファイルへ
モジュールを移動できます。
また、src/lib.rs にある pub use crate::front_of_house::hosting 文も変更されておらず、
use も、クレートの一部としてどのファイルがコンパイルされるかには影響しないことに
注意してください。mod キーワードはモジュールを宣言し、Rust は、そのモジュールに
入るコードを、モジュールと同じ名前のファイルに探しに行きます。
まとめ
Rust では、1つのパッケージを複数のクレートに分割し、さらに1つのクレートをモジュールに
分割できるため、あるモジュールで定義された項目を別のモジュールから参照できます。
これを行うには、絶対パスまたは相対パスを指定します。これらのパスは use 文によって
スコープに持ち込めるので、そのスコープ内でその項目を何度も使うときに、より短いパスを
使えます。モジュールのコードはデフォルトでは非公開ですが、pub キーワードを追加する
ことで定義を公開できます。
次の章では、きちんと整理されたコードの中で使える、標準ライブラリのいくつかの コレクションデータ構造を見ていきます。
一般的なコレクション
Rustの標準ライブラリには、_コレクション_と呼ばれる非常に便利なデータ構造が いくつも含まれています。他のほとんどのデータ型は1つの特定の値を表しますが、 コレクションは複数の値を保持できます。組み込みの配列型やタプル型とは異なり、 これらのコレクションが指すデータはヒープに格納されます。つまり、データ量は コンパイル時にわかっている必要がなく、プログラムの実行中に増減できます。コ レクションの種類ごとに異なる機能とコストがあり、現在の状況に適したものを選 ぶことは、時間をかけて身に付けていくスキルです。この章では、Rustのプログラ ムで非常によく使われる次の3つのコレクションについて説明します。
- _ベクタ_を使うと、可変個の値を隣り合わせに格納できます。
- _文字列_は文字のコレクションです。これまでにも
String型については 触れてきましたが、この章ではこれを詳しく扱います。 - _ハッシュマップ_を使うと、値を特定のキーに関連付けることができます。これは より一般的なデータ構造である マップ の特定の実装です。
標準ライブラリが提供するその他の種類のコレクションについて学ぶには、 ドキュメントを参照してください。
ベクタ、文字列、ハッシュマップの作成方法と更新方法、そしてそれぞれを特別な ものにしている点について見ていきます。
ベクタで値のリストを保持する
ベクターで値のリストを格納する
最初に見るコレクション型は、ベクターとも呼ばれる Vec<T> です。
ベクターを使うと、複数の値を 1 つのデータ構造に格納でき、それらの値は
メモリ上で互いに隣り合って配置されます。ベクターに格納できるのは、
同じ型の値だけです。ファイル内のテキストの行やショッピングカート内の
商品の価格のように、項目のリストを扱うときに便利です。
新しいベクターを作成する
新しい空のベクターを作成するには、リスト 8-1 に示すように
Vec::new 関数を呼び出します。
fn main() {
let v: Vec<i32> = Vec::new();
}
ここでは型注釈を追加していることに注目してください。このベクターには
まだ何の値も挿入していないため、Rust はどの種類の要素を格納したいのかを
知ることができません。これは重要な点です。ベクターはジェネリクスを使って
実装されています。独自の型でジェネリクスを使う方法については、第 10 章で
扱います。今のところは、標準ライブラリが提供する Vec<T> 型は任意の型を
保持できるということを知っておいてください。特定の型を保持するベクターを
作成する場合、その型を山かっこ内に指定できます。リスト 8-1 では、v の
Vec<T> が i32 型の要素を保持することを Rust に伝えています。
より一般的には、初期値付きで Vec<T> を作成し、Rust が格納したい値の型を
推論してくれるため、この型注釈が必要になることはあまりありません。Rust は
便利な vec! マクロも提供しており、渡した値を保持する新しいベクターを
作成できます。リスト 8-2 では、1、2、3 を保持する新しい
Vec<i32> を作成しています。整数型が i32 なのは、第 3 章の
「データ型」 節で説明したとおり、それが
デフォルトの整数型だからです。
fn main() {
let v = vec![1, 2, 3];
}
初期値として i32 の値を与えているので、Rust は v の型が Vec<i32> で
あると推論でき、型注釈は不要です。次に、ベクターを変更する方法を見ていきます。
ベクターを更新する
ベクターを作成してから要素を追加するには、リスト 8-3 に示すように
push メソッドを使います。
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
}
どの変数でもそうであるように、その値を変更できるようにしたい場合は、
第 3 章で説明した mut キーワードを使って可変にする必要があります。
中に入れる数値はすべて i32 型であり、Rust はデータからこれを推論するため、
Vec<i32> の注釈は必要ありません。
ベクターの要素を読み取る
ベクターに格納された値を参照する方法は 2 つあります。インデックスを使う方法と、
get メソッドを使う方法です。次の例では、分かりやすさのために、これらの
関数から返される値の型に注釈を付けています。
リスト 8-4 は、インデックス構文と get メソッドを使って、ベクター内の値に
アクセスする両方の方法を示しています。
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
}
ここでいくつかの詳細に注意してください。2 というインデックス値を使って
3 番目の要素を取得していますが、これはベクターが 0 から始まる番号で
インデックス付けされるためです。& と [] を使うと、インデックス位置の
要素への参照が得られます。インデックスを引数として get メソッドを使うと、
match と組み合わせて使える Option<&T> が得られます。
Rust が要素を参照する方法をこの 2 つ提供しているのは、存在する要素の範囲外の インデックスを使おうとしたときに、プログラムをどのように振る舞わせたいかを 選べるようにするためです。例として、5 つの要素を持つベクターがあり、その後で それぞれの手法を使ってインデックス 100 の要素にアクセスしようとすると何が 起こるかを、リスト 8-5 で見てみましょう。
fn main() {
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
}
このコードを実行すると、最初の [] による方法では、存在しない要素を
参照しているため、プログラムがパニックを起こします。この方法は、ベクターの
末尾を超える要素にアクセスしようとする試みがあった場合に、プログラムを
クラッシュさせたいときに最適です。
get メソッドにベクターの範囲外のインデックスを渡すと、パニックを起こさずに
None を返します。このメソッドは、通常の状況でもベクターの範囲を超えた
要素へのアクセスが時折起こり得る場合に使います。その場合、コードには
第 6 章で説明したように、Some(&element) と None のどちらにも対応する
ロジックが必要になります。たとえば、インデックスが人が入力した数値から
来ることがあります。もし誤って大きすぎる数値を入力して、プログラムが
None を受け取ったなら、現在のベクターにいくつ項目があるのかをユーザーに
伝え、有効な値をもう一度入力する機会を与えられます。それは、タイプミスが
原因でプログラムをクラッシュさせるよりも、ユーザーフレンドリーです。
プログラムが有効な参照を持っている場合、借用チェッカーは所有権と借用の ルール(第 4 章で扱いました)を適用し、この参照およびベクターの内容に対する ほかの参照が有効なままであることを保証します。同じスコープ内では可変参照と 不変参照を同時に持てない、というルールを思い出してください。そのルールは リスト 8-6 にも適用されます。ここでは、ベクターの最初の要素への不変参照を 保持したまま、末尾に要素を追加しようとしています。このプログラムは、後で 関数内でその要素を参照しようとすると動作しません。
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
}
このコードをコンパイルすると、次のエラーになります。
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("The first element is: {first}");
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error
リスト 8-6 のコードは、一見すると動作してよさそうに見えるかもしれません。 なぜ最初の要素への参照が、ベクターの末尾での変更を気にするのでしょうか。 このエラーは、ベクターの動作の仕組みによるものです。ベクターは値をメモリ上で 互いに隣り合わせに配置するため、ベクターの末尾に新しい要素を追加するとき、 現在ベクターが格納されている場所にすべての要素を隣り合わせで置くのに十分な 空きがない場合には、新しいメモリを確保して古い要素を新しい領域へコピーする 必要が生じることがあります。その場合、最初の要素への参照は解放済みの メモリを指すことになります。借用のルールは、プログラムがそのような状態に 陥るのを防ぎます。
注:
Vec<T>型の実装の詳細については、「The Rustonomicon」 を参照してください。
ベクター内の値を反復処理する
ベクタの各要素に順番にアクセスするには、インデックスを使って1つずつ
アクセスするのではなく、すべての要素を反復処理します。リスト8-7は、
i32 値のベクタの各要素への不変参照を for ループで取得し、それらを
出力する方法を示しています。
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
}
可変ベクタの各要素への可変参照を反復処理して、すべての要素に変更を
加えることもできます。リスト8-8の for ループは、各要素に 50 を
加えます。
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
}
可変参照が参照している値を変更するには、+= 演算子を使う前に *
参照外し演算子を使って i の中の値にたどり着かなければなりません。
参照外し演算子については、第15章の
「参照が指す値をたどる」 節で詳しく説明します。
不変でも可変でも、ベクタの反復処理は借用チェッカーの規則のおかげで
安全です。もしリスト8-7やリスト8-8の for ループ本体で要素を挿入したり
削除したりしようとすると、リスト8-6のコードで得たものと同様の
コンパイルエラーになります。for ループが保持しているベクタへの参照が、
ベクタ全体の同時変更を防いでいるのです。
複数の型を格納するために enum を使う
ベクタには同じ型の値しか格納できません。これは不便なことがあります。 異なる型の項目のリストを格納したいユースケースは確かに存在します。 幸い、enum のバリアントは同じ enum 型のもとで定義されるので、異なる型の 要素を1つの型で表現する必要があるときは、enum を定義して使えます!
たとえば、ある行のいくつかの列には整数、いくつかの列には浮動小数点数、 そしていくつかの列には文字列が入っているスプレッドシートの1行から値を 取得したいとします。異なる値型を保持するバリアントを持つ enum を定義 すれば、すべての enum バリアントは同じ型、すなわちその enum の型だと みなされます。すると、その enum を保持するベクタを作成でき、結果として 異なる型を格納できます。これをリスト8-9で示しています。
fn main() {
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}
Rust は、各要素を格納するのにヒープ上でどれだけのメモリが必要になるかを
正確に把握するため、コンパイル時にベクタ内にどの型が入るのかを知って
いる必要があります。また、このベクタでどの型を許可するのかを明示しなけ
ればなりません。もし Rust がベクタにあらゆる型を保持できるようにして
いたら、そのうちの1つ以上の型が、ベクタの要素に対して行う操作でエラーを
引き起こす可能性があります。enum と match 式を組み合わせて使えば、
第6章で説明したように、考えられるすべてのケースが処理されていることを
Rust がコンパイル時に保証してくれます。
実行時にプログラムがベクタに格納するために受け取る型の網羅的な集合が 分からない場合、enum の手法は使えません。代わりに、トレイトオブジェクトを 使えます。これについては第18章で扱います。
ベクタの最も一般的な使い方のいくつかを見てきたので、標準ライブラリが
Vec<T> に定義している数多くの便利なメソッドについては、
API ドキュメント をぜひ確認してください。
たとえば、push に加えて、pop メソッドは最後の要素を取り除いて返します。
ベクタをドロップするとその要素もドロップされる
ほかのあらゆる struct と同様に、ベクタはスコープを抜けると解放されます。
これはリスト8-10で注釈付きで示しています。
fn main() {
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
}
ベクタがドロップされると、その中身もすべてドロップされるため、保持して いる整数もクリーンアップされます。借用チェッカーは、ベクタの中身への 参照が、ベクタ自体が有効な間だけ使われることを保証します。
次は、次のコレクション型である String に進みましょう!
文字列でUTF-8エンコードされたテキストを保持する
文字列で UTF-8 エンコードされたテキストを格納する
第4章で文字列について触れましたが、ここではさらに深く見ていきます。 Rust を学び始めた人が文字列でつまずくことはよくありますが、その理由は主に3つ あります。Rust には起こりうるエラーを表面化させる傾向があること、文字列が多くの プログラマが思っている以上に複雑なデータ構造であること、そして UTF-8 です。 他のプログラミング言語から来ると、これらの要因が組み合わさって難しく感じられる ことがあります。
文字列をコレクションの文脈で扱うのは、文字列がバイトのコレクションとして実装
されており、それに加えてそのバイト列をテキストとして解釈したときに有用な機能を
提供するいくつかのメソッドを備えているからです。この節では、作成、更新、読み取り
といった、すべてのコレクション型が持つ String の操作について話します。また、
String が他のコレクションと異なる点、すなわち、人間とコンピューターで String
データの解釈が異なるために String へのインデックスアクセスが複雑になることに
ついても説明します。
文字列を定義する
まず、string という用語で何を意味するのかを定義します。Rust のコア言語には
文字列型が1つしかなく、それは通常借用された形である &str として見かける
文字列スライス str です。第4章では、文字列スライスについて説明しました。これは、
別の場所に格納されている UTF-8 エンコードされた文字列データへの参照です。たとえば、
文字列リテラルはプログラムのバイナリに格納されるため、文字列スライスです。
コア言語に組み込まれているのではなく Rust の標準ライブラリによって提供される
String 型は、拡張可能で、可変で、所有権を持つ、UTF-8 エンコードされた
文字列型です。Rustacean が Rust における「文字列」について言うとき、それは
String 型または文字列スライス &str 型のどちらかを指している可能性があり、
そのどちらか一方だけを指しているわけではありません。この節の大部分は String
についてですが、Rust の標準ライブラリでは両方の型が多用されており、String も
文字列スライスも UTF-8 エンコードされています。
新しい文字列を作成する
String は実際には、追加の保証、制約、機能を備えたバイトベクタのラッパーとして
実装されているため、Vec<T> で使えるものと同じ操作の多くは String でも使えます。
Vec<T> と String の両方で同じように動作する関数の一例が、インスタンスを
生成する new 関数で、リスト8-11に示しています。
fn main() {
let mut s = String::new();
}
この行は s という新しい空の文字列を作成し、そこに後からデータを入れられます。
多くの場合、文字列を初期化するときに最初から入れておきたいデータがあります。その
ために、文字列リテラルのように Display トレイトを実装している任意の型で利用
できる to_string メソッドを使います。リスト8-12はその2つの例を示しています。
fn main() {
let data = "initial contents";
let s = data.to_string();
// The method also works on a literal directly:
let s = "initial contents".to_string();
}
このコードは initial contents を含む文字列を作成します。
文字列リテラルから String を作成するには、関数 String::from も使えます。
リスト8-13のコードは、to_string を使うリスト8-12のコードと等価です。
fn main() {
let s = String::from("initial contents");
}
文字列は非常に多くの用途で使われるため、文字列に対して多くの異なる汎用 API を
使うことができ、その分多くの選択肢があります。冗長に見えるものもありますが、
どれにもそれぞれの役割があります! この場合、String::from と to_string は
同じことを行うので、どちらを選ぶかはスタイルと可読性の問題です。
文字列は UTF-8 でエンコードされていることを思い出してください。したがって、 リスト8-14に示すように、適切にエンコードされた任意のデータを含めることが できます。
fn main() {
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שלום");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
}
これらはすべて有効な String 値です。
文字列を更新する
String は、さらにデータをプッシュすれば、Vec<T> の内容と同じようにサイズを
大きくでき、内容も変更できます。さらに、String 値を連結するために +
演算子や format! マクロを便利に使えます。
push_str または push による追加
push_str メソッドを使って文字列スライスを追加することで、String を大きく
できます。これはリスト8-15に示しています。
fn main() {
let mut s = String::from("foo");
s.push_str("bar");
}
この2行の後、s には foobar が入ります。push_str メソッドが文字列スライスを
受け取るのは、必ずしも引数の所有権を取りたいわけではないからです。たとえば、
リスト8-16のコードでは、s2 の内容を s1 に追加したあとでも s2 を使える
ようにしたいのです。
fn main() {
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("s2 is {s2}");
}
push_str メソッドが s2 の所有権を受け取っていたなら、最後の行でその値を出力
することはできないでしょう。しかし、このコードは期待どおりに動きます!
push メソッドは単一の文字を引数に取り、それを String に追加します。
リスト8-17では、push メソッドを使って文字 l を String に追加しています。
fn main() {
let mut s = String::from("lo");
s.push('l');
}
その結果、s には lol が入ります。
+ または format! による連結
既存の2つの文字列を結合したいことはよくあります。その1つの方法は、リスト8-18に
示すように + 演算子を使うことです。
fn main() {
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
}
文字列 s3 には Hello, world! が入ります。s1 が加算後に無効になる理由と、
s2 への参照を使った理由は、+ 演算子を使ったときに呼び出されるメソッドの
シグネチャにあります。+ 演算子は add メソッドを使っており、そのシグネチャは
次のようなものです。
fn add(self, s: &str) -> String {
標準ライブラリでは、add がジェネリクスと関連型を使って定義されているのが
わかります。ここでは具体的な型を当てはめていますが、これはこのメソッドを
String 値で呼び出したときに起こることです。ジェネリクスについては第10章で説明します。
このシグネチャは、+ 演算子のわかりにくい部分を理解するために必要な手がかりを与えてくれます。
まず、s2 には & が付いています。これは、2番目の文字列への参照を
1番目の文字列に追加していることを意味します。これは add 関数の s パラメータが
あるためです。String に追加できるのは文字列スライスだけであり、2つの
String 値をそのまま一緒に加算することはできません。ですが、待ってください。add の
2番目のパラメータで指定されている型は &str なのに、&s2 の型は &String です。
では、なぜリスト8-18はコンパイルできるのでしょうか。
add の呼び出しで &s2 を使える理由は、コンパイラが
&String 引数を &str に型強制できるからです。add メソッドを呼び出すと、
Rust は deref coercion を使い、ここでは &s2 を &s2[..] に変えます。deref
coercion については第15章でさらに詳しく説明します。add は s パラメータの所有権を受け取らないため、
この操作の後でも s2 は引き続き有効な String です。
次に、シグネチャを見ると、self には & が 付いていない ため、add は self
の所有権を受け取ることがわかります。これは、リスト8-18の s1 が add
呼び出しにムーブされ、その後はもう有効ではなくなることを意味します。したがって、
let s3 = s1 + &s2; は両方の文字列をコピーして新しい文字列を作るように見えますが、
実際にはこの文は s1 の所有権を受け取り、s2 の内容のコピーを末尾に追加し、
その後で結果の所有権を返します。つまり、多くのコピーを行っているように見えても、
実際にはそうではなく、この実装は単純にコピーするよりも効率的です。
複数の文字列を連結する必要がある場合、+ 演算子の振る舞いは
扱いにくくなります。
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
}
この時点で、s は tic-tac-toe になります。+ と " の文字がたくさんあるため、
何が起きているのかを見通しにくくなっています。より複雑な方法で文字列を
結合するには、代わりに format! マクロを使えます。
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
}
このコードでも、s は tic-tac-toe になります。format! マクロは
println! と同様に動作しますが、出力を画面に表示する代わりに、その内容を持つ
String を返します。format! を使うこの版のコードははるかに読みやすく、
また format! マクロが生成するコードは参照を使うため、
この呼び出しはそのどのパラメータの所有権も受け取りません。
文字列へのインデックスアクセス
他の多くのプログラミング言語では、文字列内の個々の文字に
インデックスで参照してアクセスすることは、有効で一般的な操作です。しかし、
Rust でインデックス構文を使って String の一部にアクセスしようとすると、
エラーになります。リスト8-19の無効なコードを見てください。
fn main() {
let s1 = String::from("hi");
let h = s1[0];
}
このコードは次のエラーになります:
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `str` cannot be indexed by `{integer}`
--> src/main.rs:3:16
|
3 | let h = s1[0];
| ^ string indices are ranges of `usize`
|
= help: the trait `SliceIndex<str>` is not implemented for `{integer}`
= note: you can use `.chars().nth()` or `.bytes().nth()`
for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>
= help: the following other types implement trait `SliceIndex<T>`:
`usize` implements `SliceIndex<ByteStr>`
`usize` implements `SliceIndex<[T]>`
= note: required for `String` to implement `Index<{integer}>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` (bin "collections") due to 1 previous error
このエラーが物語っています。Rust の文字列はインデックスアクセスをサポートしていません。ですが、なぜでしょうか。 その疑問に答えるには、Rust が文字列をメモリにどのように格納しているかを説明する必要があります。
内部表現
String は Vec<u8> のラッパーです。リスト8-14にある、適切に
UTF-8 でエンコードされた文字列の例をいくつか見てみましょう。まずはこれです:
fn main() {
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שלום");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
}
この場合、len は 4 になります。これは、文字列
"Hola" を格納しているベクタの長さが 4 バイトであることを意味します。これらの文字は
UTF-8 でエンコードすると、それぞれ 1 バイトを占めます。しかし、次の行は
驚くかもしれません(この文字列は数字の 3 ではなく、キリル文字の大文字 Ze で始まることに注意してください):
fn main() {
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שלום");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
}
この文字列の長さはいくつかと聞かれたら、12 と答えるかもしれません。実際には、Rust の 答えは 24 です。これは、“Здравствуйте” を UTF-8 でエンコードするのに必要な バイト数です。なぜなら、その文字列内の各 Unicode スカラ値は 2 バイトの 領域を使うからです。したがって、文字列のバイト列へのインデックスは、常に有効な Unicode スカラ値に対応するとは限りません。これを示すために、次の無効な Rust コードを考えてみましょう:
let hello = "Здравствуйте";
let answer = &hello[0];
すでにわかっているとおり、answer は最初の文字である З にはなりません。UTF-8 で
エンコードすると、З の 1 バイト目は 208、2 バイト目は 151 なので、
answer は実際には 208 になるようにも思えます。しかし、208 はそれ単独では
有効な文字ではありません。この文字列の最初の文字を求めたときに 208 が返るのは、
おそらくユーザーの望む動作ではないでしょう。しかし、それが Rust が
バイトインデックス 0 に持っている唯一のデータです。文字列がラテン文字だけを含む場合でも、
一般にユーザーはバイト値が返されることを望みません。&"hi"[0] がバイト値を返す
有効なコードだとしたら、返るのは h ではなく 104 です。
したがって、その答えは、予期しない値を返して すぐには見つからないかもしれないバグを引き起こすことを避けるために、Rust はこのコードを そもそもコンパイルせず、開発プロセスの早い段階で誤解を防いでいるということです。
バイト、スカラ値、書記素クラスタ
UTF-8 に関するもうひとつのポイントは、Rust の観点から文字 列を見る方法には、実は重要なものが 3 つあるということです。すなわち、バイト列、 スカラ値、そして書記素クラスタ(私たちがいうところの 文字 に最も近いもの)です。
デーヴァナーガリー文字で書かれたヒンディー語の “नमस्ते” を見ると、それは
次のような u8 値のベクタとして格納されています:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
これは 18 バイトであり、コンピュータは最終的にこのようにデータを格納します。これらを
Unicode スカラ値として見ると、つまり Rust の char 型が表すものとして見ると、
そのバイト列は次のようになります:
['न', 'म', 'स', '्', 'त', 'े']
ここには 6 つの char 値がありますが、4 番目と 6 番目は文字ではありません。
それらは単独では意味をなさない発音区別符号です。最後に、これらを
書記素クラスタとして見ると、人がヒンディー語のその単語を構成する 4 つの文字と
呼ぶであろうものが得られます:
["न", "म", "स्", "ते"]
Rust は、コンピュータが格納する生の文字列データを解釈するためのさまざまな方法を提供しており、
そのデータがどの人間の言語で書かれているかにかかわらず、各プログラムが
必要とする解釈を選べるようになっています。
Rust で String にインデックスを使って文字を取得できない最後の理由は、
インデックス操作には常に定数時間
(O(1))が期待されるからです。しかし、String ではその性能を保証できません。
というのも、Rust は有効な文字がいくつあるかを判定するために、先頭から
指定されたインデックスまで内容をたどる必要があるからです。
文字列のスライス
文字列へのインデックス指定は、文字列インデックス操作の戻り値の型が何であるべきか、 つまりバイト値なのか、文字なのか、書記素クラスタなのか、それとも文字列スライスなのかが 明確ではないため、多くの場合あまりよい考えではありません。したがって、文字列スライスを 作るためにどうしてもインデックスを使う必要がある場合、Rust はより具体的に指定することを 求めます。
単一の数値で [] を使ってインデックス指定する代わりに、範囲を指定して [] を使うことで、
特定のバイトを含む文字列スライスを作成できます。
#![allow(unused)]
fn main() {
let hello = "Здравствуйте";
let s = &hello[0..4];
}
ここで、s は文字列の先頭 4 バイトを含む &str になります。
前に述べたように、これらの文字はそれぞれ 2 バイトなので、
s は Зд になります。
&hello[0..1] のように、文字を構成するバイトの一部だけをスライスしようとすると、
Rust は、ベクタで無効なインデックスにアクセスした場合と同じように、実行時に
パニックします。
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/collections`
thread 'main' panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
そのため、範囲を使って文字列スライスを作成するときは注意が必要です。 そうすると、プログラムがクラッシュする可能性があるからです。
文字列の反復処理
文字列の一部分を操作する最善の方法は、文字が欲しいのかバイトが欲しいのかを
明示することです。個々の Unicode スカラー値については、chars メソッドを使います。
“Зд” に対して chars を呼び出すと、型 char の 2 つの値に分けて返し、
結果を反復処理して各要素にアクセスできます。
#![allow(unused)]
fn main() {
for c in "Зд".chars() {
println!("{c}");
}
}
このコードは次のように出力します。
З
д
一方、bytes メソッドは各生バイトを返します。これは、ドメインによっては
適切かもしれません。
#![allow(unused)]
fn main() {
for b in "Зд".bytes() {
println!("{b}");
}
}
このコードは、この文字列を構成する 4 バイトを出力します。
208
151
208
180
ただし、有効な Unicode スカラー値は 1 バイトより多くのバイトで構成されることがある、 という点を忘れないでください。
デーヴァナーガリー文字のように、文字列から書記素クラスタを取得するのは複雑なので、 この機能は標準ライブラリでは提供されていません。この機能が必要な場合は、 crates.io で利用できるクレートがあります。
文字列の複雑さへの対処
要約すると、文字列は複雑です。異なるプログラミング言語は、この複雑さを
プログラマにどう見せるかについて異なる選択をしています。Rust は、String
データを正しく扱うことをすべての Rust プログラムのデフォルトの動作にする、
という選択をしました。つまり、プログラマは UTF-8 データの扱いについて
あらかじめより多く考える必要があります。このトレードオフによって、文字列の
複雑さが他のプログラミング言語よりも表に出てきますが、その一方で、開発
ライフサイクルの後半で非 ASCII 文字に関するエラーに対処しなくて済むように
なります。
よい知らせは、標準ライブラリが、こうした複雑な状況を正しく扱うのに役立つ、
String 型と &str 型を基盤とした多くの機能を提供していることです。
文字列内を検索する contains や、文字列の一部を別の文字列で置き換える
replace のような便利なメソッドについて、ぜひドキュメントを確認してください。
では、もう少し複雑でないもの、ハッシュマップに移りましょう!
ハッシュマップでキーに関連付けられた値を保持する
ハッシュマップでキーに関連付けられた値を格納する
一般的なコレクションの最後の 1 つがハッシュマップです。型 HashMap<K, V> は、ハッシュ関数 を使って、型 K のキーから型 V の値への対応付けを格納します。この関数は、これらのキーと値をメモリ内のどこに配置するかを決定します。多くのプログラミング言語はこの種のデータ構造をサポートしていますが、hash、map、object、hash table、dictionary、associative array など、さまざまな名前で呼ばれることがよくあります。
ハッシュマップは、ベクタのようにインデックスを使ってではなく、任意の型になりうるキーを使ってデータを検索したいときに便利です。たとえばゲームでは、各チームのスコアをハッシュマップで管理できます。この場合、各キーはチーム名で、各値はそのチームのスコアです。チーム名が与えられれば、そのスコアを取り出せます。
このセクションではハッシュマップの基本的な API を見ていきますが、標準ライブラリが HashMap<K, V> に対して定義している便利な機能はほかにもたくさんあります。いつものように、より詳しい情報については標準ライブラリのドキュメントを確認してください。
新しいハッシュマップを作成する
空のハッシュマップを作成する 1 つの方法は、new を使い、insert で要素を追加することです。リスト 8-20 では、名前が Blue と Yellow の 2 チームのスコアを追跡しています。Blue チームは 10 点から始まり、Yellow チームは 50 点から始まります。
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
}
まず、標準ライブラリの collections 部分から HashMap を use する必要があることに注意してください。3 つの一般的なコレクションのうち、これは最も使用頻度が低いため、prelude によって自動的にスコープに導入される機能には含まれていません。また、ハッシュマップは標準ライブラリからのサポートもやや少なく、たとえばそれらを構築するための組み込みマクロはありません。
ベクタと同様に、ハッシュマップはデータをヒープに格納します。この HashMap は、キーの型が String、値の型が i32 です。ベクタと同様に、ハッシュマップも同種です。つまり、すべてのキーは同じ型でなければならず、すべての値も同じ型でなければなりません。
ハッシュマップ内の値にアクセスする
ハッシュマップから値を取り出すには、リスト 8-21 に示すように、そのキーを get メソッドに渡します。
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
}
ここで、score には Blue チームに関連付けられた値が入り、結果は 10 になります。get メソッドは Option<&V> を返します。ハッシュマップ内にそのキーに対応する値がない場合、get は None を返します。このプログラムでは、copied を呼び出して Option<&i32> ではなく Option<i32> を取得し、その後 unwrap_or を使って、scores にそのキーのエントリがない場合は score を 0 に設定することで、この Option を処理しています。
ハッシュマップ内の各キーと値のペアは、ベクタと同様に for ループを使って走査できます。
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{key}: {value}");
}
}
このコードは、各ペアを任意の順序で出力します。
Yellow: 50
Blue: 10
ハッシュマップで所有権を管理する
i32 のように Copy トレイトを実装している型では、値はハッシュマップにコピーされます。String のような所有権を持つ値では、値はムーブされ、ハッシュマップがそれらの値の所有者になります。これはリスト 8-22 で示しています。
fn main() {
use std::collections::HashMap;
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name and field_value are invalid at this point, try using them and
// see what compiler error you get!
}
field_name と field_value は insert の呼び出しによってハッシュマップにムーブされているため、その後はこれらの変数を使うことはできません。
値への参照をハッシュマップに挿入する場合、値自体はハッシュマップにムーブされません。参照が指す値は、少なくともハッシュマップが有効であるのと同じだけ長く有効でなければなりません。これらの問題については、第 10 章の 「ライフタイムで参照を検証する」 で詳しく説明します。
ハッシュマップを更新する
キーと値のペアの数は増やせますが、各一意なキーに同時に関連付けられる値は 1 つだけです(ただし逆は成り立ちません。たとえば Blue チームと Yellow チームの両方が、scores ハッシュマップで値 10 を持つことはできます)。
ハッシュマップ内のデータを変更したいときは、あるキーにすでに値が割り当てられている場合をどう扱うか決める必要があります。古い値を完全に無視して新しい値で置き換えることもできます。古い値を保持して新しい値を無視し、そのキーにまだ値が ない 場合にだけ新しい値を追加することもできます。あるいは、古い値と新しい値を組み合わせることもできます。それぞれの方法を見ていきましょう。
値を上書きする
キーと値をハッシュマップに挿入し、その後同じキーに対して別の値を挿入すると、そのキーに関連付けられた値は置き換えられます。リスト 8-23 のコードでは insert を 2 回呼び出していますが、Blue チームのキーに対する値を両方とも挿入しているため、ハッシュマップに含まれるキーと値のペアは 1 つだけになります。
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{scores:?}");
}
このコードは {"Blue": 25} を出力します。元の値 10 は上書きされています。
キーが存在しない場合にのみキーと値を追加する
特定のキーがすでに値とともにハッシュマップに存在するかどうかを確認し、その後に次のような動作を行うのは一般的です。キーがハッシュマップ内に存在する場合は、既存の値はそのままにしておきます。キーが存在しない場合は、そのキーとそれに対応する値を挿入します。
ハッシュマップには、このための特別な API として entry があり、確認したいキーを引数として取ります。entry メソッドの戻り値は Entry という enum で、存在するかもしれないし存在しないかもしれない値を表します。たとえば、Yellow チームのキーに値が関連付けられているかどうかを確認したいとします。もし関連付けられていなければ、値 50 を挿入したいとし、Blue チームについても同様です。entry API を使うと、コードはリスト 8-24 のようになります。
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{scores:?}");
}
リスト 8-24 のコードを実行すると、{"Yellow": 50, "Blue": 10} が出力されます。最初の entry 呼び出しでは、Yellow チームにはまだ値がないため、値 50 とともに Yellow チームのキーが挿入されます。2 回目の entry 呼び出しでは、Blue チームにはすでに値 10 があるため、ハッシュマップは変更されません。
古い値に基づいて値を更新する
ハッシュマップのもう 1 つの一般的な使用例は、キーの値を調べてから、その古い値に基づいて更新することです。たとえば、リスト 8-25 は、あるテキスト内で各単語が何回現れるかを数えるコードを示しています。単語をキーとするハッシュマップを使い、その単語を見かけた回数を追跡するために値を増やしています。ある単語を初めて見たときは、まず値 0 を挿入します。
fn main() {
use std::collections::HashMap;
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{map:?}");
}
このコードは {"world": 2, "hello": 1, "wonderful": 1} を出力します。同じキーと値のペアが別の順序で出力されることもあります。「ハッシュマップ内の値にアクセスする」 で述べたように、ハッシュマップの反復は任意の順序で行われることを思い出してください。
split_whitespace メソッドは、text の値に含まれる、空白で区切られた部分スライスに対するイテレータを返します。or_insert メソッドは、指定されたキーに対応する値への可変参照 (&mut V) を返します。ここでは、その可変参照を count 変数に格納しているので、その値に代入するには、まずアスタリスク (*) を使って count をデリファレンスしなければなりません。可変参照は for ループの終わりでスコープを抜けるため、これらの変更はすべて安全であり、借用規則によって許可されます。
ハッシュ関数
デフォルトでは、HashMap は SipHash と呼ばれるハッシュ関数を使用しており、ハッシュテーブルを利用したサービス拒否 (DoS) 攻撃への耐性を提供できます1。これは利用可能な中で最速のハッシュアルゴリズムではありませんが、性能低下に伴う、より高いセキュリティとのトレードオフには十分な価値があります。コードをプロファイルして、デフォルトのハッシュ関数が用途に対して遅すぎるとわかった場合は、別の hasher を指定することで別の関数に切り替えることができます。hasher とは、BuildHasher トレイトを実装する型です。トレイトとその実装方法については、第 10 章 で説明します。必ずしも自分で hasher をゼロから実装する必要はありません。crates.io には、一般的なハッシュアルゴリズムを実装した hasher を提供する、ほかの Rust ユーザーによって共有されたライブラリがあります。
まとめ
ベクタ、文字列、ハッシュマップは、データを保存、アクセス、変更する必要があるときに、プログラムで必要となる多くの機能を提供してくれます。これで、次のような練習問題を解けるようになっているはずです。
- 整数のリストが与えられたとき、ベクタを使って中央値(ソートしたときの中央の位置にある値)と最頻値(最も多く現れる値。ここではハッシュマップが役に立ちます)を返してください。
- 文字列を Pig Latin に変換してください。各単語の最初の子音を単語の末尾に移動し、ay を追加します。たとえば first は irst-fay になります。母音で始まる単語には、代わりに末尾に hay を追加します(apple は apple-hay になります)。UTF-8 エンコーディングに関する詳細も忘れないでください。
- ハッシュマップとベクタを使って、ユーザーが会社内の部署に従業員名を追加できるテキストインターフェースを作成してください。たとえば、「Add Sally to Engineering」や「Add Amir to Sales」のようにします。続いて、ユーザーが部署ごとの全従業員の一覧、または会社全体の全従業員を部署別にアルファベット順で取得できるようにしてください。
標準ライブラリの API ドキュメントには、これらの練習問題に役立つ、ベクタ、文字列、ハッシュマップが持つメソッドが説明されています。
処理が失敗する可能性のある、より複雑なプログラムに入っていくので、エラーハンドリングについて議論するには絶好のタイミングです。次はそれを見ていきましょう!
エラー処理
エラーはソフトウェアにおいて避けられないものなので、Rust には 何か問題が起きた状況を扱うためのさまざまな機能があります。多くの場合、 Rust はコードをコンパイルできるようになる前に、エラーが起こる可能性を 認識し、何らかの対処を行うことを要求します。この要件により、コードを 本番環境にデプロイする前にエラーを発見して適切に処理できるようになるため、 プログラムの堅牢性が高まります。
Rust では、エラーを大きく 2 つのカテゴリに分類します。回復可能なエラーと、 回復不可能なエラーです。たとえば、ファイルが見つからない といった 回復可能なエラー の場合、たいていはその問題をユーザーに報告して、 操作を再試行したいだけでしょう。回復不可能なエラー は、配列の末尾を 超えた位置にアクセスしようとするような、常にバグの兆候であるため、 プログラムをただちに停止させたいと考えます。
ほとんどの言語では、この 2 種類のエラーを区別せず、例外のような仕組みを
使って両方を同じ方法で扱います。Rust には例外がありません。その代わりに、
回復可能なエラーには型 Result<T, E> があり、プログラムが回復不可能な
エラーに遭遇したときに実行を停止する panic! マクロがあります。この章では、
まず panic! の呼び出しについて取り上げ、その後で Result<T, E> の値を
返す方法について説明します。さらに、エラーからの回復を試みるべきか、
それとも実行を停止すべきかを判断する際の考慮事項についても見ていきます。
panic! による回復不能なエラー
panic! による回復不能なエラー
コードの中では、ときどき良くないことが起こり、それに対して何もできない場合があります。こうしたケースのために、Rust には panic! マクロがあります。実際にパニックを発生させる方法は 2 つあります。コードがパニックするような操作を行うこと(たとえば、配列の末尾を超えてアクセスすること)と、panic! マクロを明示的に呼び出すことです。どちらの場合でも、プログラム内でパニックが発生します。デフォルトでは、これらのパニックは失敗メッセージを表示し、アンワインドし、スタックをクリーンアップして終了します。環境変数を使うことで、パニックが発生したときに Rust にコールスタックを表示させ、パニックの原因を追跡しやすくすることもできます。
パニックに応じたスタックのアンワインドまたはアボート
デフォルトでは、パニックが発生すると、プログラムは アンワインド を開始します。これは、Rust がスタックをさかのぼりながら、通過する各関数のデータをクリーンアップすることを意味します。しかし、さかのぼってクリーンアップするのはかなりの作業です。そのため Rust では、即座に アボート するという代替手段も選べます。これは、クリーンアップを行わずにプログラムを終了する方法です。
その場合、プログラムが使用していたメモリはオペレーティングシステムによってクリーンアップされる必要があります。プロジェクトで生成されるバイナリをできるだけ小さくしたい場合は、Cargo.toml ファイルの適切な
[profile]セクションにpanic = 'abort'を追加することで、パニック時の動作をアンワインドからアボートに切り替えられます。たとえば、リリースモードでパニック時にアボートしたい場合は、次のように追加します。[profile.release] panic = 'abort'
単純なプログラムで panic! を呼び出してみましょう。
fn main() {
panic!("crash and burn");
}
プログラムを実行すると、次のような出力が表示されます。
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
panic! の呼び出しによって、最後の 2 行に含まれるエラーメッセージが発生します。最初の行には、パニックメッセージと、パニックが発生したソースコード内の位置が表示されています。src/main.rs:2:5 は、src/main.rs ファイルの 2 行目 5 文字目であることを示しています。
この場合、示されている行は私たちのコードの一部であり、その行に移動すると panic! マクロの呼び出しが見つかります。別のケースでは、panic! の呼び出しが、私たちのコードが呼び出したコードの中にあることもあります。その場合、エラーメッセージで報告されるファイル名と行番号は、最終的に panic! 呼び出しに至った私たちのコードの行ではなく、panic! マクロが呼び出されている誰か別のコードの位置になります。
panic! 呼び出しの発生元となった関数のバックトレースを使うと、問題を引き起こしているコードの箇所を特定できます。panic! のバックトレースの使い方を理解するために、別の例を見てみましょう。今度は、コードが直接マクロを呼び出したのではなく、コード内のバグのためにライブラリから panic! が呼び出される場合です。リスト 9-1 には、有効なインデックス範囲を超えてベクタのインデックスにアクセスしようとするコードがあります。
fn main() {
let v = vec![1, 2, 3];
v[99];
}
ここでは、ベクタの 100 番目の要素にアクセスしようとしています(インデックスは 0 から始まるため、位置としてはインデックス 99 です)が、そのベクタには要素が 3 つしかありません。この状況では、Rust はパニックします。[] は要素を返すことが想定されていますが、無効なインデックスを渡した場合、Rust がここで返せる正しい要素は存在しません。
C では、データ構造の末尾を超えて読み取ろうとすることは未定義動作です。そのデータ構造に属していないにもかかわらず、その要素に対応するメモリ位置にたまたまあるものを取得してしまうかもしれません。これは buffer overread と呼ばれ、攻撃者がインデックスを操作して、本来は許可されるべきではない、そのデータ構造の後ろに格納されているデータを読み取れる場合には、セキュリティ脆弱性につながる可能性があります。
この種の脆弱性からプログラムを保護するために、存在しないインデックスの要素を読み取ろうとすると、Rust は実行を停止し、続行を拒否します。試して確認してみましょう。
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
このエラーは、v のベクタのインデックス 99 にアクセスしようとしている main.rs の 4 行目を指しています。
note: 行は、RUST_BACKTRACE 環境変数を設定することで、何が起きてエラーに至ったのかを正確に示すバックトレースを取得できることを教えてくれます。バックトレース とは、この地点に到達するまでに呼び出されたすべての関数の一覧です。Rust のバックトレースは他の言語と同様に機能します。バックトレースを読む際の要点は、先頭から読み始めて、自分が書いたファイルが現れるまで進むことです。そこが問題の発生源です。その箇所より上の行は、あなたのコードが呼び出したコードです。下の行は、あなたのコードを呼び出したコードです。こうした前後の行には、Rust のコアコード、標準ライブラリのコード、あるいは使用しているクレートが含まれることがあります。RUST_BACKTRACE 環境変数を 0 以外の任意の値に設定して、バックトレースを取得してみましょう。リスト 9-2 に、表示されるものと似た出力を示します。
```console
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: rust_begin_unwind
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5
1: core::panicking::panic_fmt
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14
2: core::panicking::panic_bounds_check
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9
6: panic::main
at ./src/main.rs:4:6
7: core::ops::function::FnOnce::call_once
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
すごい量の出力ですね! 実際に表示される出力は、使用している
オペレーティングシステムやRustのバージョンによって異なる場合があります。この
情報を含むバックトレースを取得するには、デバッグシンボルが有効になっている必要があります。デバッグシンボルは、
ここで行っているように、cargo build や cargo run を --release フラグなしで使用すると、
デフォルトで有効になります。
リスト9-2の出力では、バックトレースの6行目が、問題を引き起こしている プロジェクト内の行、つまり src/main.rs の4行目を指しています。もし プログラムをパニックさせたくないなら、私たちが書いたファイルに言及している最初の行が 指し示す場所から調査を始めるべきです。意図的に パニックするコードを書いたリスト9-1では、パニックを修正する方法は、 ベクターのインデックス範囲を超える要素を要求しないことです。将来、あなたのコードが パニックしたときには、そのパニックを引き起こすためにコードがどの値で どのような操作を行っているのか、そして代わりにコードが何をするべきかを 突き止める必要があります。
この章の後半にある
「panic! するべきか、しないべきか」 節で、
panic! と、エラー条件を処理するために panic! を使うべき場合と使うべきでない場合について
再び取り上げます。次に、Result を使ってエラーから回復する方法を見ていきます。
Result による回復可能なエラー
Result による回復可能なエラー
ほとんどのエラーは、プログラム全体を完全に停止させなければならないほど深刻ではありません。関数が失敗したとしても、その理由を簡単に解釈して対応できる場合があります。たとえば、ファイルを開こうとしてその操作が失敗した原因がファイルの不存在であれば、プロセスを終了する代わりにそのファイルを作成したいかもしれません。
第2章の 「Result で起こりうる失敗を処理する」 で見たように、Result enum は次のように Ok と Err の2つのバリアントを持つよう定義されています。
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T),
Err(E),
}
}
この T と E はジェネリック型パラメータです。ジェネリクスについては第10章でさらに詳しく説明します。ここで今知っておく必要があるのは、T は Ok バリアント内で成功時に返される値の型を表し、E は Err バリアント内で失敗時に返されるエラーの型を表すということです。Result はこのようなジェネリック型パラメータを持っているため、返したい成功値やエラー値が異なるさまざまな状況で、Result 型とそれに対して定義された関数を使うことができます。
関数が失敗する可能性があるために Result 値を返す関数を呼び出してみましょう。リスト9-3では、ファイルを開こうとしています。
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
}
File::open の戻り値の型は Result<T, E> です。ジェネリックパラメータ T には、File::open の実装によって成功値の型である std::fs::File が当てられています。これはファイルハンドルです。エラー値に使われる E の型は std::io::Error です。この戻り値の型が意味するのは、File::open の呼び出しが成功して、読み取りや書き込みに使えるファイルハンドルを返すかもしれないということです。また、この関数呼び出しは失敗するかもしれません。たとえば、ファイルが存在しないかもしれませんし、そのファイルにアクセスする権限がないかもしれません。File::open 関数は、成功したか失敗したかを私たちに伝える方法を持つ必要があり、同時にファイルハンドルまたはエラー情報のどちらかを渡せなければなりません。この情報こそが、まさに Result enum が表現しているものです。
File::open が成功した場合、変数 greeting_file_result の値はファイルハンドルを含む Ok のインスタンスになります。失敗した場合、greeting_file_result の値は発生したエラーの種類についての詳細情報を含む Err のインスタンスになります。
File::open が返す値に応じて異なる動作をするように、リスト9-3のコードに処理を追加する必要があります。リスト9-4は、Result を、第6章で説明した基本的な道具である match 式を使って処理する一つの方法を示しています。
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
};
}
Option enum と同様に、Result enum とそのバリアントは prelude によってスコープに導入されているため、match アーム内の Ok と Err バリアントの前に Result:: を指定する必要はないことに注意してください。
結果が Ok のとき、このコードは Ok バリアントから内部の file 値を返し、そのファイルハンドル値を変数 greeting_file に代入します。match の後では、そのファイルハンドルを読み取りや書き込みに使えます。
match のもう一方のアームは、File::open から Err 値を受け取った場合を処理します。この例では、panic! マクロを呼び出すことにしています。現在のディレクトリに hello.txt という名前のファイルが存在しない状態でこのコードを実行すると、panic! マクロから次のような出力が表示されます。
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
いつものように、この出力は何がうまくいかなかったのかを正確に教えてくれます。
異なるエラーに対するマッチング
リスト9-4のコードは、File::open が失敗した理由にかかわらず panic! します。しかし、失敗理由に応じて異なるアクションを取りたい場合があります。File::open がファイルの不存在によって失敗したのであれば、そのファイルを作成して新しいファイルのハンドルを返したいところです。File::open がそれ以外の理由で失敗した場合、たとえばファイルを開く権限がなかった場合には、リスト9-4と同じようにコードを panic! させたいままです。そのために、リスト9-5に示すように、内側の match 式を追加します。
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
_ => {
panic!("Problem opening the file: {error:?}");
}
},
};
}
Err バリアントの内側で File::open が返す値の型は io::Error で、これは標準ライブラリが提供する構造体です。この構造体には kind というメソッドがあり、これを呼び出すことで io::ErrorKind の値を取得できます。enum io::ErrorKind も標準ライブラリが提供しており、io 操作の結果として起こりうるさまざまな種類のエラーを表すバリアントを持っています。ここで使いたいバリアントは ErrorKind::NotFound で、これは開こうとしているファイルがまだ存在しないことを示します。したがって、greeting_file_result に対してマッチさせるだけでなく、error.kind() に対しても内側でマッチさせています。
内側の match で確認したい条件は、error.kind() が返す値が ErrorKind enum の NotFound バリアントかどうかです。もしそうなら、File::create でそのファイルを作成しようとします。しかし、File::create も失敗する可能性があるため、内側の match 式には2つ目のアームも必要です。ファイルを作成できない場合は、別のエラーメッセージが表示されます。外側の match の2つ目のアームは同じままなので、プログラムは不足しているファイルのエラー以外のどのエラーでも panic します。
Result<T, E>でmatchを使う代わりの方法
matchがたくさん出てきました!match式は非常に便利ですが、同時に かなり基本的なものでもあります。第13章ではクロージャについて学びます。これはResult<T, E>に定義されている多くのメソッドとともに使われます。こうしたメソッドは、 コード内でResult<T, E>の値を扱う際に、matchを使うよりも 簡潔に書ける場合があります。たとえば、以下はリスト9-5で示したものと同じロジックを記述する別の方法で、 今回はクロージャと
unwrap_or_elseメソッドを使っています:use std::fs::File; use std::io::ErrorKind; fn main() { let greeting_file = File::open("hello.txt").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|error| { panic!("Problem creating the file: {error:?}"); }) } else { panic!("Problem opening the file: {error:?}"); } }); }このコードはリスト9-5と同じ振る舞いをしますが、
match式を まったく含んでおらず、よりすっきりしていて読みやすくなっています。この例には、 第13章を読んだあとに戻ってきて、標準ライブラリのドキュメントでunwrap_or_elseメソッドを調べてみてください。エラーを扱うときには、 こうしたメソッドがさらに多くあり、大きく入れ子になったmatch式を きれいにできます。
エラー時に panic するためのショートカット
match は十分うまく機能しますが、少し冗長になることがあり、意図が常に
うまく伝わるとは限りません。Result<T, E> 型には、さまざまな、より具体的な処理を
行うためのヘルパーメソッドが多数定義されています。unwrap メソッドは、
リスト9-4で書いた match 式とまったく同じように実装されたショートカットメソッドです。
Result の値が Ok バリアントであれば、unwrap は Ok の中の値を返します。
Result が Err バリアントであれば、unwrap は代わりに panic! マクロを
呼び出します。以下は unwrap を実際に使っている例です:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
このコードを hello.txt ファイルがない状態で実行すると、unwrap メソッドが
行う panic! 呼び出しによるエラーメッセージが表示されます:
thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
同様に、expect メソッドでは panic! のエラーメッセージも自分で選べます。
unwrap の代わりに expect を使い、適切なエラーメッセージを与えることで、
意図を伝えやすくなり、パニックの原因を追跡しやすくなります。expect の
構文は次のようになります:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
expect は unwrap と同じように使います。つまり、ファイルハンドルを返すか、
panic! マクロを呼び出します。expect が panic! を呼び出すときに使う
エラーメッセージは、unwrap が使うデフォルトの panic! メッセージではなく、
expect に渡した引数になります。次のようになります:
thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }
本番品質のコードでは、ほとんどの Rustacean は unwrap よりも expect を選び、
その操作が常に成功すると見込んでいる理由について、より多くの文脈を与えます。
そうすれば、もしその前提が誤りだと判明した場合でも、デバッグに使える情報が
より多く得られます。
エラーを伝播させる
関数の実装が失敗する可能性のある何かを呼び出すとき、関数自身の中で エラーを処理する代わりに、エラーを呼び出し元のコードに返して、どうするかを そこで決められるようにできます。これはエラーを 伝播させる こととして知られており、 呼び出し元のコードにより大きな制御を与えます。そこには、あなたのコードの文脈で 利用できるものよりも、エラーをどう処理すべきかを決めるための追加の情報や ロジックがあるかもしれないからです。
たとえば、リスト9-6はファイルからユーザー名を読み取る関数を示しています。もし ファイルが存在しない、または読み取れない場合、この関数はそれらのエラーを その関数を呼び出したコードに返します。
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
}
この関数はもっと短く書けますが、エラー処理を探るために、まずはその多くを
手作業で行うところから始めます。最後に、より短い書き方を示します。まず、
この関数の戻り値の型を見てみましょう: Result<String, io::Error> です。これは、
この関数が Result<T, E> 型の値を返していることを意味します。ここで、
ジェネリックパラメータ T には具体的な型 String が入り、ジェネリック
パラメータ E には具体的な型 io::Error が入っています。
この関数が何の問題もなく成功した場合、この関数を呼び出すコードは String を保持した
Ok 値を受け取ります。つまり、この関数がファイルから読み取った username です。
この関数で何らかの問題が発生した場合、呼び出し元のコードは、どのような問題だったかに
ついての追加情報を含む io::Error のインスタンスを保持した Err 値を受け取ります。
この関数の戻り値の型として io::Error を選んだのは、関数本体の中で呼び出している、
失敗する可能性のある2つの操作、つまり File::open 関数と read_to_string
メソッドの両方が返すエラー値の型が、たまたま io::Error だからです。
この関数本体は、まず File::open 関数を呼び出すところから始まります。次に、
リスト9-4の match と似た match で Result 値を処理します。File::open が
成功した場合、パターン変数 file に入っているファイルハンドルが可変変数
username_file の値になり、関数は続行します。Err の場合は、panic! を
呼び出す代わりに、return キーワードを使って関数全体から早期に戻り、
現在はパターン変数 e に入っている File::open からのエラー値を、この関数の
エラー値として呼び出し元のコードに返します。
つまり、username_file にファイルハンドルがある場合、この関数は次に変数 username に新しい String を作成し、username_file のファイルハンドルに対して read_to_string メソッドを呼び出して、ファイルの内容を username に読み込みます。read_to_string メソッドも Result を返します。File::open が成功していても、この処理は失敗する可能性があるためです。したがって、その Result を処理するために、さらにもう1つの match が必要になります。read_to_string が成功した場合、この関数も成功したことになり、いま username に入っているファイル内のユーザー名を Ok で包んで返します。read_to_string が失敗した場合は、File::open の戻り値を処理した match でエラー値を返したのと同じ方法で、そのエラー値を返します。ただし、これは関数内の最後の式なので、明示的に return と書く必要はありません。
このコードを呼び出す側は、その後、ユーザー名を含む Ok 値か、io::Error を含む Err 値のいずれかを受け取ることになります。それらの値をどう扱うかは呼び出し元コードに委ねられます。呼び出し元コードが Err 値を受け取った場合、たとえば panic! を呼び出してプログラムをクラッシュさせることもできますし、デフォルトのユーザー名を使うこともできますし、ファイル以外のどこかからユーザー名を取得することもできます。しかし、呼び出し元コードが実際に何をしようとしているのかについて、私たちには十分な情報がありません。そのため、適切に処理できるよう、成功またはエラーに関するすべての情報を上位へ伝播させます。
このようなエラー伝播のパターンは Rust では非常によくあるため、Rust にはこれを簡単にするための疑問符演算子 ? が用意されています。
? 演算子によるショートカット
リスト9-7は、リスト9-6と同じ機能を持つ read_username_from_file の実装を示していますが、この実装では ? 演算子を使用しています。
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
}
Result 値の後ろに置かれた ? は、リスト9-6で Result 値を処理するために定義した match 式と、ほぼ同じように動作するよう定義されています。Result の値が Ok であれば、その Ok の中の値がこの式から返され、プログラムは継続します。値が Err であれば、return キーワードを使ったかのように、その Err が関数全体から返され、エラー値が呼び出し元コードへ伝播されます。
リスト9-6の match 式が行うことと、? 演算子が行うことの間には、1つ違いがあります。? 演算子が適用されたエラー値は、標準ライブラリの From トレイトで定義されている from 関数を通過します。この関数は、ある型の値を別の型へ変換するために使われます。? 演算子が from 関数を呼び出すと、受け取ったエラー型は、現在の関数の戻り値の型で定義されているエラー型へ変換されます。これは、関数の一部がさまざまな理由で失敗しうる場合でも、関数が失敗しうるすべての方法を表すために1つのエラー型を返すときに役立ちます。
たとえば、リスト9-7の read_username_from_file 関数を、私たちが定義する OurError という名前のカスタムエラー型を返すように変更できます。さらに、io::Error から OurError のインスタンスを構築する impl From<io::Error> for OurError も定義すれば、read_username_from_file の本体にある ? 演算子の呼び出しは from を呼び出し、関数にこれ以上コードを追加しなくてもエラー型を変換してくれます。
リスト9-7の文脈では、File::open 呼び出しの末尾にある ? は、Ok の中の値を変数 username_file に返します。エラーが発生した場合、? 演算子は関数全体から早期に戻り、任意の Err 値を呼び出し元コードに渡します。同じことが read_to_string 呼び出しの末尾にある ? にも当てはまります。
? 演算子は多くの定型コードを取り除き、この関数の実装をよりシンプルにしてくれます。さらに、リスト9-8に示すように、? の直後にメソッド呼び出しを連結することで、このコードをさらに短くすることもできます。
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
}
新しい String を username に作成する処理は、関数の先頭へ移動しました。この部分は変わっていません。変数 username_file を作成する代わりに、File::open("hello.txt")? の結果に対して read_to_string の呼び出しを直接連結しています。read_to_string 呼び出しの末尾にも引き続き ? があり、File::open と read_to_string の両方が成功したときには、エラーを返すのではなく、username を含む Ok 値を返します。この機能は、やはりリスト9-6およびリスト9-7と同じです。これは、単に別の、より書きやすい書き方にすぎません。
リスト9-9は、fs::read_to_string を使ってこれをさらに短くする方法を示しています。
#![allow(unused)]
fn main() {
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
}
ファイルを文字列に読み込むのはかなり一般的な操作なので、標準ライブラリには便利な fs::read_to_string 関数が用意されています。この関数はファイルを開き、新しい String を作成し、ファイルの内容を読み込み、その内容をその String に入れ、それを返します。もちろん、fs::read_to_string を使うと、すべてのエラー処理を説明する機会は得られないので、最初はより長い方法で説明しました。
? 演算子を使える場所
? 演算子は、その ? が使われる値と互換性のある戻り値型を持つ関数でのみ使用できます。これは、? 演算子が、リスト9-6で定義した match 式と同じように、関数から値を早期リターンするよう定義されているためです。リスト9-6では、match は Result 値を使っており、早期リターンするアームは Err(e) 値を返していました。この return と互換性を持たせるために、関数の戻り値型は Result でなければなりません。
リスト 9-10 では、? 演算子を、? を適用する値の型と互換性のない戻り値型を持つ
main 関数で使った場合に発生するエラーを見てみましょう。
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
このコードはファイルを開きますが、これは失敗する可能性があります。? 演算子は
File::open が返す Result 値に従いますが、この main 関数の戻り値型は
Result ではなく () です。このコードをコンパイルすると、次のエラーメッセージが
表示されます。
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:48
|
3 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
4 | let greeting_file = File::open("hello.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
help: consider adding return type
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 | let greeting_file = File::open("hello.txt")?;
5 + Ok(())
|
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error
このエラーは、? 演算子を使えるのは、Result、Option、または
FromResidual を実装している別の型を返す関数の中だけであることを示しています。
このエラーを修正するには、2 つの選択肢があります。1 つは、そうすることを妨げる制約が
ないのであれば、関数の戻り値型を、? 演算子を適用している値と互換性のあるものに
変更することです。もう 1 つは、match または Result<T, E> のメソッドのいずれかを
使って、Result<T, E> を適切な方法で処理することです。
エラーメッセージでは、? を Option<T> の値と一緒に使えることにも触れていました。
Result に対して ? を使う場合と同様に、Option に対して ? を使えるのは、
Option を返す関数の中だけです。Option<T> に対して呼び出されたときの
? 演算子の振る舞いは、Result<T, E> に対して呼び出されたときの振る舞いと
似ています。値が None なら、その時点で関数から早期に None が返されます。
値が Some なら、Some の中の値がその式の結果値となり、関数の実行は継続します。
リスト 9-11 には、与えられたテキストの最初の行の最後の文字を見つける関数の例が
あります。
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
fn main() {
assert_eq!(
last_char_of_first_line("Hello, world\nHow are you today?"),
Some('d')
);
assert_eq!(last_char_of_first_line(""), None);
assert_eq!(last_char_of_first_line("\nhi"), None);
}
この関数は Option<char> を返します。というのも、そこに文字がある可能性もありますが、
ない可能性もあるからです。このコードは text という文字列スライス引数を受け取り、
それに対して lines メソッドを呼び出します。これは文字列内の各行に対する
イテレータを返します。この関数は最初の行を調べたいので、イテレータに対して next
を呼び出して最初の値を取得します。text が空文字列なら、この next の呼び出しは
None を返します。その場合、? を使って処理を止め、last_char_of_first_line
から None を返します。text が空文字列でなければ、next は Some 値を返し、
その中には text の最初の行の文字列スライスが入っています。
? はその文字列スライスを取り出し、その文字列スライスに対して chars を呼び出して、
その文字たちのイテレータを取得できます。私たちが関心を持っているのはこの最初の行の
最後の文字なので、イテレータの最後の要素を返すために last を呼び出します。
これは Option です。というのも、最初の行が空文字列である可能性があるからです。
たとえば、text が "\nhi" のように空行で始まっていて、他の行には文字がある場合が
それにあたります。しかし、最初の行に最後の文字が存在するなら、それは Some
バリアントの中で返されます。途中にある ? 演算子によって、このロジックを簡潔に
表現でき、関数を 1 行で実装できます。Option に対して ? 演算子を使えなければ、
もっと多くのメソッド呼び出しか match 式を使ってこのロジックを実装しなければ
ならないでしょう。
Result を返す関数では Result に対して ? 演算子を使えますし、Option を返す
関数では Option に対して ? 演算子を使えますが、これらを混在させて使うことは
できません。? 演算子は Result を Option に、あるいはその逆に自動変換しては
くれません。そのような場合には、Result の ok メソッドや Option の ok_or
メソッドのようなものを使って、明示的に変換できます。
これまでに使ってきた main 関数は、すべて () を返していました。main 関数は、
実行可能プログラムのエントリポイントであり終了ポイントでもあるため特別であり、
プログラムが期待どおりに振る舞うためには、その戻り値型に制約があります。
ありがたいことに、main は Result<(), E> を返すこともできます。リスト 9-12 には、
リスト 9-10 のコードがありますが、main の戻り値型を Result<(), Box<dyn Error>>
に変更し、末尾に戻り値 Ok(()) を追加しています。このコードは今度は
コンパイルされます。
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
Box<dyn Error> 型はトレイトオブジェクトです。これについては第 18 章の
「共有の振る舞いを抽象化するためにトレイトオブジェクトを使う」
で説明します。今のところは、Box<dyn Error> を「あらゆる種類のエラー」を意味するものと
考えてください。エラー型が Box<dyn Error> である main 関数の中で Result
値に対して ? を使えるのは、あらゆる Err 値を早期に返せるからです。この main
関数の本体が実際に返すエラーは std::io::Error 型だけですが、Box<dyn Error> を
指定しておけば、main の本体に他の種類のエラーを返すコードがさらに追加されても、
このシグネチャは引き続き正しいままです。
main 関数が Result<(), E> を返す場合、main が Ok(()) を返せば実行ファイルは
値 0 で終了し、Err 値を返せば 0 以外の値で終了します。C で書かれた実行ファイルは、
終了時に整数を返します。正常に終了するプログラムは整数 0 を返し、エラーで終了する
プログラムは 0 以外の整数を返します。Rust もこの慣習との互換性を保つために、
実行ファイルから整数を返します。
main 関数は、std::process::Termination トレイト
を実装する任意の型を返すことができます。このトレイトには ExitCode を返す report
という関数が含まれています。独自の型に対して Termination トレイトを実装する方法に
ついては、標準ライブラリのドキュメントを参照してください。
ここまでで panic! を呼び出すことと Result を返すことの詳細を説明したので、
どのような場合にどちらを使うのが適切か、という話題に戻りましょう。
panic! すべきか、すべきでないか
panic! すべきか、すべきでないか
では、いつ panic! を呼び出し、いつ Result を返すべきかは、どのように判断すればよいのでしょうか? コードがパニックすると、そこから回復する方法はありません。回復可能かどうかにかかわらず、どんなエラー状況でも panic! を呼び出すことはできますが、そうすると、その状況は回復不能であるという判断を、呼び出し側のコードに代わって下すことになります。一方で Result 値を返すことを選べば、呼び出し側のコードに選択肢を与えることになります。呼び出し側のコードは、その状況に適した方法で回復を試みることもできますし、この場合の Err 値は回復不能だと判断して panic! を呼び出し、あなたの回復可能なエラーを回復不能なものに変えることもできます。したがって、失敗する可能性のある関数を定義するときのデフォルトとしては、Result を返すのがよい選択です。
しかし、サンプル、プロトタイプコード、テストのような状況では、Result を返すよりも、パニックするコードを書くほうが適切です。その理由を見ていき、その後で、コンパイラには失敗が不可能だとわからなくても、人間であるあなたにはわかる状況について議論しましょう。この章の最後では、ライブラリコードでパニックすべきかどうかを判断するための一般的な指針をいくつか示します。
サンプル、プロトタイプコード、テスト
ある概念を説明するためのサンプルを書くときに、堅牢なエラーハンドリングのコードまで含めると、サンプルがかえってわかりにくくなることがあります。サンプルでは、パニックする可能性のある unwrap のようなメソッド呼び出しは、アプリケーションでエラーをどのように処理したいかを後で埋めるためのプレースホルダーであると理解されます。その方法は、残りのコードが何をしているかによって異なりえます。
同様に、プロトタイピング中で、まだエラーをどう処理するか決める準備ができていないときには、unwrap メソッドと expect メソッドは非常に便利です。これらは、あとでプログラムをより堅牢にしようとするときのための明確な目印をコード内に残してくれます。
テスト内でメソッド呼び出しが失敗したなら、そのメソッド自体がテスト対象の機能でなくても、テスト全体が失敗してほしいはずです。そして、panic! はテストを失敗としてマークする方法なので、unwrap や expect を呼び出すのはまさに起こるべきことです。
コンパイラより多くの情報を持っている場合
また、Result が Ok 値を持つことを保証する別のロジックがあるものの、そのロジックをコンパイラが理解できない場合には、expect を呼び出すのも適切です。それでも、扱わなければならない Result 値は残ります。呼び出している操作は、あなたの特定の状況では論理的に失敗しえないとしても、一般論としては依然として失敗する可能性があるからです。コードを目で確認することで Err バリアントが決して発生しないと保証できるなら、expect を呼び出し、なぜ Err バリアントが起こらないと考えるのかを引数のテキストに記しておくのは、まったく問題ありません。例を見てみましょう。
fn main() {
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1"
.parse()
.expect("Hardcoded IP address should be valid");
}
ここでは、ハードコードされた文字列をパースして IpAddr インスタンスを作成しています。127.0.0.1 が有効な IP アドレスであることはわかるので、ここで expect を使うのは問題ありません。しかし、有効な文字列をハードコードしているからといって、parse メソッドの戻り値の型が変わるわけではありません。私たちは依然として Result 値を受け取り、また、この文字列が常に有効な IP アドレスであることをコンパイラは見抜けるほど賢くないため、コンパイラは Err バリアントの可能性があるものとして Result を処理することを依然として要求します。もし IP アドレス文字列がプログラムにハードコードされているのではなくユーザーから渡されるものであり、そのため実際に失敗する可能性が ある のなら、Result はもっと堅牢な方法で処理したいはずです。この IP アドレスはハードコードされている、という前提を書いておけば、将来 IP アドレスを別のソースから取得する必要が出てきたときに、expect をより適切なエラーハンドリングコードへ変更するきっかけになります。
エラーハンドリングの指針
コードが不正な状態に陥る可能性がある場合には、コードをパニックさせるようにするのが望ましいです。この文脈でいう 不正な状態 とは、何らかの前提、保証、契約、または不変条件が破られている状態のことです。たとえば、無効な値、矛盾した値、欠けている値がコードに渡された場合などで、さらに次のうち 1 つ以上に当てはまる場合です。
- その不正な状態は、ユーザーが誤った形式でデータを入力する、といった時折起こりうるものではなく、想定外のものである。
- その時点以降のコードは、各段階で問題を確認し続けるのではなく、その不正な状態ではないことを前提にする必要がある。
- 使用している型の中にこの情報をうまくエンコードするよい方法がない。これがどういう意味かについては、第 18 章の 「状態と振る舞いを型としてエンコードする」 で例を通して見ていきます。
誰かがあなたのコードを呼び出して意味をなさない値を渡してきた場合には、可能であればエラーを返すのが最善です。そうすれば、ライブラリの利用者がその場合にどうしたいかを判断できます。しかし、処理を続けることが安全でなかったり有害であったりする場合には、panic! を呼び出して、あなたのライブラリを使っている人にそのコード内のバグを知らせ、開発中に修正してもらうのが最善の選択かもしれません。同様に、制御できない外部コードを呼び出していて、そのコードが修正しようのない不正な状態を返してきた場合にも、panic! はしばしば適切です。
しかし、失敗が予想される場合には、panic! を呼び出すよりも Result を返すほうが適切です。たとえば、パーサーに不正な形式のデータが渡される場合や、HTTP リクエストがレート制限に達したことを示すステータスを返す場合などです。このような場合に Result を返すことは、失敗が予期される可能性であり、それをどう扱うかは呼び出し側のコードが決めなければならないことを示します。
コードが、無効な値で呼び出されたときにユーザーを危険にさらすおそれのある操作を行う場合、そのコードはまず値が有効であることを検証し、値が無効ならパニックすべきです。これは主に安全性のためです。無効なデータに対して操作を試みると、コードが脆弱性にさらされる可能性があります。これが、範囲外のメモリアクセスを試みたときに標準ライブラリが panic! を呼び出す主な理由です。現在のデータ構造に属していないメモリへアクセスしようとすることは、一般的なセキュリティ上の問題だからです。関数にはしばしば 契約 があります。つまり、その振る舞いは、入力が特定の要件を満たしている場合にのみ保証されます。契約が破られたときにパニックするのは理にかなっています。なぜなら、契約違反は常に呼び出し側のバグを示しており、呼び出し側のコードに明示的な処理を強いたい種類のエラーではないからです。実際、呼び出し側のコードに回復のための妥当な方法はありません。コードを修正する必要があるのは、呼び出し側の プログラマ です。関数の契約、特に違反するとパニックを引き起こすような契約については、その関数の API ドキュメントで説明すべきです。
しかし、すべての関数に大量のエラーチェックを書くのは冗長で面倒です。幸いなことに、Rust の型システム(したがってコンパイラが行う型チェック)を使えば、多くのチェックを任せることができます。関数が特定の型を引数として受け取るなら、コンパイラがすでに有効な値であることを保証していると分かったうえで、そのコードのロジックを進められます。たとえば、Option ではなくある型を持っているなら、プログラムは 何か があることを期待しており、何もない ことを期待しているのではありません。そうすると、コードは Some バリアントと None バリアントの 2 つのケースを処理する必要がなくなります。確実に値がある 1 つのケースだけを扱えばよいのです。関数に何も渡そうとするコードは、そもそもコンパイルすら通りません。したがって、関数はそのケースを実行時にチェックする必要がありません。別の例として、u32 のような符号なし整数型を使えば、その引数が決して負にならないことが保証されます。
バリデーションのためのカスタム型
有効な値があることを Rust の型システムで保証するという考えを、もう一歩進めて、バリデーションのためのカスタム型を作成する方法を見てみましょう。第 2 章の数当てゲームを思い出してください。このコードでは、ユーザーに 1 から 100 までの数を予想させました。秘密の数と比較する前に、ユーザーの予想がその範囲内にあるかどうかは一度も検証しておらず、予想が正であることだけを検証していました。この場合、その結果はそれほど深刻ではありませんでした。出力が「大きすぎます」や「小さすぎます」であっても、依然として正しいからです。しかし、ユーザーを有効な予想へ導き、範囲外の数を予想した場合と、たとえば文字を入力した場合とで挙動を変えられるようにするのは、有用な改善になるでしょう。
これを行う 1 つの方法は、潜在的に負の数も許容するために、予想を単に u32 としてではなく i32 としてパースし、その後で数値が範囲内にあるかどうかのチェックを追加することです。次のようになります。
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
// --snip--
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
この if 式は、値が範囲外かどうかをチェックし、問題をユーザーに伝え、continue を呼び出してループの次の反復を開始し、別の予想を求めます。if 式の後では、guess が 1 から 100 の間にあると分かったうえで、guess と秘密の数の比較を進められます。
しかし、これは理想的な解決策ではありません。プログラムが 1 から 100 までの値に対してのみ動作することが絶対的に重要で、しかもこの要件を持つ関数が多数あるなら、すべての関数でこのようなチェックを行うのは面倒ですし、性能にも影響するかもしれません。
代わりに、専用のモジュールで新しい型を作り、その型のインスタンスを生成する関数の中にバリデーションを入れることで、あらゆる場所で同じバリデーションを繰り返さずに済みます。そうすれば、関数はシグネチャで新しい型を安全に使え、受け取った値を安心して利用できます。リスト 9-13 は、new 関数が 1 から 100 までの値を受け取った場合にのみ Guess のインスタンスを生成する Guess 型の定義方法の一例を示しています。
#![allow(unused)]
fn main() {
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
}
なお、src/guessing_game.rs のこのコードは、ここでは示していない src/lib.rs へのモジュール宣言 mod guessing_game; の追加に依存しています。この新しいモジュールのファイル内では、i32 を保持する value という名前のフィールドを持つ Guess という名前の構造体を定義しています。数値はここに格納されます。
次に、Guess に対して Guess のインスタンスを生成する new という関連関数を実装します。new 関数は、i32 型の value という名前の引数を 1 つ受け取り、Guess を返すように定義されています。new 関数本体のコードは、value が 1 から 100 の間にあることを確認するために value を検査します。もし value がこの検査を通らなければ、panic! を呼び出します。これにより、呼び出し側のコードを書いているプログラマに、修正すべきバグがあることが伝わります。というのも、この範囲外の value で Guess を作成することは、Guess::new が前提としている契約に違反するからです。Guess::new が panic! する可能性のある条件は、その公開 API ドキュメントで説明されるべきです。panic! の可能性を API ドキュメントで示すための文書化の慣習については、第 14 章で扱います。value が検査を通った場合は、value フィールドを value 引数に設定した新しい Guess を作成し、その Guess を返します。
次に、self を借用し、ほかの引数を持たず、i32 を返す value という名前のメソッドを実装します。この種のメソッドは、フィールドからデータを取り出して返すことが目的であるため、ゲッター と呼ばれることがあります。この公開メソッドが必要なのは、Guess 構造体の value フィールドがプライベートだからです。value フィールドがプライベートであることは重要です。そうすることで、Guess 構造体を使うコードは value を直接設定できません。guessing_game モジュールの外側のコードは、Guess のインスタンスを作成するために 必ず Guess::new 関数を使わなければならず、その結果、Guess::new 関数の条件でチェックされていない value を Guess が持つ方法は存在しないことが保証されます。
その後、引数として受け取る、または返す値が 1 から 100 までの数だけである関数は、シグネチャで i32 ではなく Guess を受け取る、あるいは返すと宣言でき、関数本体で追加のチェックを行う必要がなくなります。
まとめ
Rust のエラーハンドリング機能は、より堅牢なコードを書けるように設計されています。panic! マクロは、プログラムが対処できない状態にあることを示し、無効または不正な値で処理を続けようとする代わりに、プロセスを停止するよう指示できるようにします。Result enum は Rust の型システムを使って、操作が失敗する可能性はあるが、その失敗はコードが回復できる種類のものであることを示します。Result を使えば、あなたのコードを呼び出すコードに対して、起こりうる成功または失敗を処理する必要があることを伝えられます。適切な状況で panic! と Result を使うことで、避けられない問題に直面しても、コードの信頼性を高められます。
標準ライブラリが Option と Result enum でジェネリクスをどのように有用に使っているかを見たので、次はジェネリクスがどのように機能するのか、そしてそれを自分のコードでどう使えるのかについて説明します。
ジェネリック型、トレイト、ライフタイム
どのプログラミング言語にも、概念の重複を効果的に扱うための手段があります。Rust では、そのような手段のひとつが ジェネリクス です。これは、具体的な型やその他の性質の代わりとなる抽象的な代役です。ジェネリクスの振る舞いや、ほかのジェネリクスとどのように関係するかを、コードのコンパイル時や実行時に何がその場所に入るのかを知らなくても表現できます。
関数は、i32 や String のような具体的な型ではなく、何らかのジェネリック型のパラメータを受け取ることができます。これは、複数の具体的な値に対して同じコードを実行するために、未知の値を持つパラメータを受け取れるのと同じです。実際、すでに第6章では Option<T>、第8章では Vec<T> と HashMap<K, V>、第9章では Result<T, E> でジェネリクスを使っています。この章では、ジェネリクスを使って独自の型、関数、メソッドを定義する方法を見ていきます。
まず、コードの重複を減らすために関数を抽出する方法を復習します。次に、パラメータの型だけが異なる2つの関数から、同じ手法を使ってジェネリックな関数を作ります。さらに、struct と enum の定義でジェネリック型を使う方法も説明します。
その後、トレイトを使って振る舞いをジェネリックに定義する方法を学びます。トレイトとジェネリック型を組み合わせることで、ジェネリック型が受け入れる型を、単なる任意の型ではなく、特定の振る舞いを持つ型だけに制約できます。
最後に、ライフタイム について説明します。これは、参照どうしがどのように関係しているかについてコンパイラに情報を与える、ジェネリクスの一種です。ライフタイムによって、借用された値に関する十分な情報をコンパイラに与えられるため、私たちの助けがない場合よりも多くの状況で参照が有効であることを保証できるようになります。
関数を抽出して重複を取り除く
ジェネリクスを使うと、複数の型を表すプレースホルダーで特定の型を置き換えることで、コードの重複を取り除けます。ジェネリクスの構文に入る前に、まずは、複数の値を表すプレースホルダーで特定の値を置き換える関数を抽出することで、ジェネリック型を使わずに重複を取り除く方法を見てみましょう。その後、同じ手法を適用してジェネリックな関数を抽出します。関数として抽出できる重複コードの見分け方を見ることで、ジェネリクスを使える重複コードも見分けられるようになっていきます。
まずは、リスト内の最大の数値を見つける、リスト10-1の短いプログラムから始めます。
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
assert_eq!(*largest, 100);
}
整数のリストを変数 number_list に格納し、そのリストの最初の数値への参照を largest という名前の変数に入れます。次に、リスト内のすべての数値を反復処理し、現在の数値が largest に格納されている数値より大きければ、その変数内の参照を置き換えます。一方、現在の数値がここまでに見た最大の数値以下であれば、変数は変わらず、コードはリスト内の次の数値へ進みます。リスト内のすべての数値を調べ終えたあと、largest は最大の数値を参照しているはずで、この場合は 100 です。
次に、2つの異なる数値のリストの中から最大の数値を見つけるよう求められたとします。そのためには、リスト10-1のコードを複製し、同じロジックをプログラム内の2か所で使うことができます。これはリスト10-2に示されています。
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
}
このコードは動作しますが、コードを複製するのは面倒で、間違いの原因にもなります。また、変更したいときには複数箇所のコードを更新する必要があることも忘れてはいけません。
この重複をなくすために、パラメータとして渡された任意の整数リストに対して動作する関数を定義し、抽象化を行います。この解決策により、コードはより明確になり、リスト内の最大の数値を見つけるという概念を抽象的に表現できるようになります。
リスト10-3では、最大の数値を見つけるコードを largest という名前の関数に抽出します。次に、その関数を呼び出して、リスト10-2の2つのリストで最大の数値を見つけます。将来、ほかの i32 値のリストがあれば、そのリストに対してもこの関数を使えます。
fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {result}");
assert_eq!(*result, 100);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("The largest number is {result}");
assert_eq!(*result, 6000);
}
largest 関数には list という名前のパラメータがあり、これはその関数に渡す可能性のある任意の i32 値のスライスを表します。その結果、この関数を呼び出すと、コードは私たちが渡した具体的な値に対して実行されます。
まとめると、リスト10-2のコードをリスト10-3に変更するために行った手順は次のとおりです。
- 重複しているコードを特定する。
- 重複しているコードを関数本体に抽出し、そのコードの入力と戻り値を関数シグネチャで指定する。
- 重複していた2か所のコードを、代わりにその関数を呼び出すよう更新する。
次に、同じ手順をジェネリクスに対して使って、コードの重複を減らします。関数本体が特定の値ではなく抽象的な list に対して操作できるのと同じように、ジェネリクスを使うとコードは抽象的な型に対して操作できます。
たとえば、2つの関数があるとします。ひとつは i32 値のスライス内で最大の要素を見つける関数、もうひとつは char 値のスライス内で最大の要素を見つける関数です。この重複をどのように取り除けばよいでしょうか。見ていきましょう!
ジェネリックなデータ型
ジェネリックなデータ型
関数シグネチャや構造体のような項目の定義を作成し、それをさまざまな具体的なデータ型で使えるようにするために、ジェネリクスを使用します。まず、ジェネリクスを使って関数、構造体、列挙型、メソッドを定義する方法を見ていきます。次に、ジェネリクスがコードのパフォーマンスにどのような影響を与えるかを説明します。
関数定義において
ジェネリクスを使う関数を定義するときは、通常であれば引数や戻り値のデータ型を指定する関数のシグネチャに、ジェネリクスを配置します。そうすることで、コードがより柔軟になり、コードの重複を防ぎつつ、関数の呼び出し側により多くの機能を提供できます。
先ほどの largest 関数を続けて見ていきましょう。リスト10-4は、どちらもスライス内の最大の値を見つける2つの関数を示しています。続いて、これらをジェネリクスを使った1つの関数にまとめます。
fn largest_i32(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: &[char]) -> &char {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest_i32(&number_list);
println!("The largest number is {result}");
assert_eq!(*result, 100);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {result}");
assert_eq!(*result, 'y');
}
largest_i32 関数は、リスト10-3で切り出した、スライス内の最大の i32 を見つける関数です。largest_char 関数は、スライス内の最大の char を見つけます。関数本体のコードは同じなので、1つの関数にジェネリック型パラメータを導入することで重複をなくしましょう。
新しい1つの関数で型をパラメーター化するには、関数の値パラメータと同じように、型パラメータにも名前を付ける必要があります。型パラメータ名には任意の識別子を使えます。しかし、ここでは T を使います。というのも、Rust では慣例として、型パラメータ名は短く、しばしば1文字だけであり、Rust の型命名規則は UpperCamelCase だからです。型 を表す T は、ほとんどの Rust プログラマにとって定番の選択です。
関数本体でパラメータを使うときは、コンパイラがその名前の意味を理解できるように、シグネチャ内でそのパラメータ名を宣言しなければなりません。同様に、関数シグネチャ内で型パラメータ名を使うときも、それを使う前に型パラメータ名を宣言しなければなりません。ジェネリックな largest 関数を定義するには、次のように、関数名と引数リストの間にある山括弧 <> の中に型名の宣言を置きます。
fn largest<T>(list: &[T]) -> &T {
この定義は、「関数 largest は何らかの型 T に対してジェネリックである」と読みます。この関数には list という名前の引数が1つあり、これは型 T の値からなるスライスです。largest 関数は、同じ型 T の値への参照を返します。
リスト10-5は、シグネチャでジェネリックなデータ型を使った、統合後の largest 関数の定義を示しています。このリストでは、i32 値のスライスと char 値のスライスのどちらでもこの関数を呼び出せることも示しています。なお、このコードはまだコンパイルできません。
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {result}");
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {result}");
}
このコードを今すぐコンパイルすると、次のエラーが出ます。
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T` with trait `PartialOrd`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
| ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
ヘルプテキストでは std::cmp::PartialOrd に触れています。これはトレイトであり、次の節でトレイトについて説明します。ここではひとまず、このエラーは largest の本体が T に取りうるすべての型に対しては動作しないことを示している、と理解してください。本体で型 T の値を比較したいので、使えるのは値を順序付けできる型だけです。比較を可能にするために、標準ライブラリには型に実装できる std::cmp::PartialOrd トレイトがあります(このトレイトの詳細は付録Cを参照してください)。リスト10-5を修正するには、ヘルプテキストの提案に従って、T に有効な型を PartialOrd を実装するものだけに制限できます。そうすればこのリストはコンパイルできるようになります。標準ライブラリは i32 と char の両方に PartialOrd を実装しているからです。
構造体定義において
<> 構文を使って、1つ以上のフィールドでジェネリック型パラメータを使用する構造体を定義することもできます。リスト10-6では、任意の型の x 座標値と y 座標値を保持する Point<T> 構造体を定義しています。
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
構造体定義でジェネリクスを使う構文は、関数定義で使ったものと似ています。まず、構造体名の直後の山括弧の中に型パラメータ名を宣言します。次に、通常であれば具体的なデータ型を指定する場所で、そのジェネリック型を構造体定義内に使用します。
Point<T> を定義するためにジェネリック型を1つしか使っていないため、この定義は Point<T> 構造体が何らかの型 T に対してジェネリックであり、フィールド x と y は どちらも その同じ型であることを意味します。リスト10-7のように、異なる型の値を持つ Point<T> のインスタンスを作成すると、コードはコンパイルされません。
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
この例では、整数値 5 を x に代入した時点で、この Point<T> のインスタンスにおいてジェネリック型 T は整数になることをコンパイラに伝えています。その後、y に 4.0 を指定すると、y は x と同じ型になるよう定義されているため、次のような型不一致エラーが発生します。
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number
For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
x と y がどちらもジェネリクスでありながら異なる型を取れる Point 構造体を定義するには、複数のジェネリック型パラメータを使えます。たとえばリスト10-8では、Point の定義を型 T と U に対してジェネリックなものに変更し、x は型 T、y は型 U としています。
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}
これで、ここに示した Point のインスタンスはすべて許可されます! 定義では必要なだけ多くのジェネリック型パラメータを使えますが、数が多すぎるとコードが読みにくくなります。コード内で多数のジェネリック型が必要になっているなら、それはコードをより小さな部品へ再構成する必要があることを示しているのかもしれません。
列挙型定義において
構造体で行ったのと同様に、列挙型でも、そのバリアント内にジェネリックなデータ型を保持するよう定義できます。第6章で使った、標準ライブラリが提供する Option<T> 列挙型をもう一度見てみましょう。
#![allow(unused)]
fn main() {
enum Option<T> {
Some(T),
None,
}
}
これで、この定義がより理解しやすくなったはずです。見てのとおり、
Option<T> 列挙型は型 T に対してジェネリックで、2つのバリアントを持っています。Some は型 T の値を1つ保持し、None バリアントは何の値も保持しません。
Option<T> 列挙型を使うことで、オプショナルな値という抽象的な概念を表現できます。また、Option<T> はジェネリックなので、オプショナルな値の型が何であってもこの抽象化を使えます。
列挙型は複数のジェネリック型も使えます。第9章で使った Result
列挙型の定義は、その一例です。
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T),
Err(E),
}
}
Result 列挙型は2つの型 T と E に対してジェネリックで、2つのバリアントを持ちます。
Ok は型 T の値を保持し、Err は型
E の値を保持します。この定義により、成功する可能性がある操作(何らかの型 T の値を返す)や失敗する可能性がある操作(何らかの型 E のエラーを返す)があるあらゆる場所で、Result 列挙型を便利に使えます。実際、これはリスト9-3でファイルを開くときに使ったもので、ファイルを正常に開けた場合には T に std::fs::File 型が入り、ファイルを開く際に問題があった場合には E に
std::io::Error 型が入っていました。
コードの中で、保持する値の型だけが異なる複数の構造体や列挙型の定義がある状況に気づいたら、ジェネリック型を使うことで重複を避けられます。
メソッド定義において
構造体や列挙型に対してメソッドを実装でき(第5章で行ったとおりです)、その定義の中でもジェネリック型を使えます。リスト10-9は、リスト10-6で定義した
Point<T> 構造体に、x という名前のメソッドを実装したものを示しています。
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
ここでは、Point<T> に対して x という名前のメソッドを定義しており、
フィールド x のデータへの参照を返します。
Point<T> 型に対してメソッドを実装していることを示すのに T を使えるよう、impl の直後で T を宣言しなければならないことに注意してください。
impl の後で T をジェネリック型として宣言することで、Rust は Point
の山かっこ内の型が具体的な型ではなくジェネリック型であると識別できます。
このジェネリックパラメータには、構造体定義で宣言したジェネリックパラメータとは別の名前を選ぶこともできましたが、同じ名前を使うのが慣例です。ジェネリック型を宣言した impl の中にメソッドを書くと、そのメソッドは、最終的にそのジェネリック型にどの具体的な型が代入されるかにかかわらず、その型のあらゆるインスタンスに対して定義されます。
型に対してメソッドを定義するときに、ジェネリック型に制約を設けることもできます。たとえば、どんなジェネリック型でもよい Point<T> のインスタンスではなく、
Point<f32> のインスタンスに対してだけメソッドを実装できます。リスト10-10では、具体的な型 f32 を使っているため、impl の後に型を何も宣言していません。
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
このコードは、Point<f32> 型には distance_from_origin
メソッドがある一方、T が f32 型ではない Point<T> の他のインスタンスには、このメソッドが定義されないことを意味します。このメソッドは、座標 (0.0, 0.0) の点から自分たちの点までがどれだけ離れているかを測り、浮動小数点型でしか使えない数学的演算を利用します。
構造体定義のジェネリック型パラメータは、同じ構造体のメソッドシグネチャで使うものと必ずしも同じではありません。リスト10-11では、例をより明確にするために、Point 構造体にはジェネリック型 X1 と Y1 を使い、mixup メソッドシグネチャには X2 と Y2 を使っています。このメソッドは、self
の Point(型は X1)から x の値を取り、渡された Point(型は Y2)からy の値を取って、新しい Point インスタンスを作ります。
struct Point<X1, Y1> {
x: X1,
y: Y1,
}
impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
main では、x に i32(値は 5)を持ち、y に f64
(値は 10.4)を持つ Point を定義しています。p2 変数は、
x に文字列スライス(値は "Hello")を持ち、y に char
(値は c)を持つ Point 構造体です。p1 に対して引数 p2 で mixup
を呼び出すと p3 が得られます。x は p1 から来るので、p3 の x は i32
になります。y は p2 から来るので、p3 の y は char
になります。println! マクロ呼び出しは p3.x = 5, p3.y = c を出力します。
この例の目的は、一部のジェネリックパラメータが impl
で宣言され、一部がメソッド定義で宣言される状況を示すことです。ここでは、ジェネリックパラメータ X1 と Y1 は構造体定義に対応するので impl
の後で宣言されます。ジェネリックパラメータ X2 と Y2 はメソッドにしか関係しないので、fn mixup の後で宣言されます。
ジェネリクスを使うコードのパフォーマンス
ジェネリック型パラメータを使うと実行時コストがかかるのではないか、と疑問に思うかもしれません。うれしいことに、ジェネリック型を使っても、具体的な型を使った場合よりプログラムの実行が遅くなることはありません。
Rust は、コンパイル時にジェネリクスを使ったコードに対して単相化を行うことでこれを実現しています。単相化 とは、コンパイル時に使われる具体的な型を埋め込むことによって、ジェネリックなコードを具体的なコードに変換するプロセスです。このプロセスで、コンパイラはリスト10-5でジェネリック関数を作るために行った手順とは逆のことをします。コンパイラは、ジェネリックなコードが呼び出されているすべての箇所を見て、そのジェネリックなコードが呼び出されている具体的な型向けのコードを生成します。
これがどのように動くのか、標準ライブラリのジェネリックな
Option<T> 列挙型を使って見てみましょう。
#![allow(unused)]
fn main() {
let integer = Some(5);
let float = Some(5.0);
}
Rust がこのコードをコンパイルすると、単相化が行われます。その過程で、コンパイラは Option<T>
のインスタンスで使われている値を読み取り、2種類の Option<T> を特定します。1つは i32 で、もう1つは
f64 です。そのため、ジェネリックな Option<T> の定義は、i32 用と f64
用に特化した2つの定義へと展開され、ジェネリックな定義がそれら具体的な定義に置き換えられます。
単相化されたコードのバージョンは、次のようなものに似ています(説明のため、ここではコンパイラが実際とは異なる名前を使っています)。
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
ジェネリックな Option<T> は、コンパイラによって作成された具体的な定義に
置き換えられます。Rust はジェネリックなコードを、各インスタンスで型が指定された
コードにコンパイルするため、ジェネリクスを使用しても実行時コストは発生しません。コードが
実行されるとき、その動作は、各定義を手作業で複製していた場合とまったく同じです。
単相化のプロセスにより、Rust のジェネリクスは実行時に非常に効率的になります。
トレイトで共有の振る舞いを定義する
トレイトを使って共有の振る舞いを定義する
トレイト は、ある特定の型が持ち、他の型と共有できる機能を定義します。トレイトを使うと、共有される振る舞いを抽象的な方法で定義できます。トレイト境界 を使うと、ジェネリック型が特定の振る舞いを持つ任意の型になりうることを指定できます。
注: トレイトは、他の言語でしばしば インターフェース と呼ばれる機能に似ていますが、いくつか違いがあります。
トレイトを定義する
型の振る舞いは、その型に対して呼び出せるメソッドで構成されます。同じメソッドをそれらすべての型に対して呼び出せるなら、異なる型は同じ振る舞いを共有していることになります。トレイト定義は、ある目的を達成するために必要な一連の振る舞いを定義するため、メソッドシグネチャをひとまとめにする方法です。
たとえば、さまざまな種類と量のテキストを保持する複数の構造体があるとしましょう。ある特定の場所で報告されたニュース記事を保持する NewsArticle 構造体と、最大 280 文字までのテキストに加えて、それが新規投稿、再投稿、または別の投稿への返信であるかを示すメタデータを持てる SocialPost です。
NewsArticle や SocialPost のインスタンスに格納されているかもしれないデータの要約を表示できる、aggregator という名前のメディアアグリゲータのライブラリクレートを作りたいとします。そのためには、それぞれの型から要約を取得する必要があり、その要約はインスタンスに対して summarize メソッドを呼び出すことで要求します。リスト 10-12 は、この振る舞いを表現する公開 Summary トレイトの定義を示しています。
pub trait Summary {
fn summarize(&self) -> String;
}
ここでは、trait キーワードに続けて、この場合は Summary であるトレイト名を書いてトレイトを宣言しています。また、このトレイトを pub として宣言しているので、このクレートに依存する他のクレートもこのトレイトを利用できます。これについては、後のいくつかの例で確認します。波かっこの内側では、このトレイトを実装する型の振る舞いを記述するメソッドシグネチャを宣言します。この場合は fn summarize(&self) -> String です。
メソッドシグネチャの後には、波かっこ内に実装を書く代わりに、セミコロンを使います。このトレイトを実装する各型は、メソッド本体に対して独自の振る舞いを提供しなければなりません。コンパイラは、Summary トレイトを持つあらゆる型に、このシグネチャどおりの summarize メソッドが定義されていることを強制します。
トレイトは本体に複数のメソッドを持つことができます。メソッドシグネチャは 1 行に 1 つずつ並び、各行はセミコロンで終わります。
型にトレイトを実装する
Summary トレイトのメソッドに望まれるシグネチャを定義したので、次はそれをメディアアグリゲータ内の型に実装できます。リスト 10-13 は、見出し、著者、場所を使って summarize の戻り値を作る、NewsArticle 構造体に対する Summary トレイトの実装を示しています。SocialPost 構造体については、投稿内容がすでに 280 文字に制限されていると仮定して、ユーザー名の後に投稿全文を続けるものとして summarize を定義します。
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
型にトレイトを実装することは、通常のメソッドを実装するのと似ています。違いは、impl の後に実装したいトレイト名を書き、それから for キーワードを使い、その後にそのトレイトを実装したい型の名前を指定することです。impl ブロックの中には、トレイト定義で定義されたメソッドシグネチャを書きます。各シグネチャの後にセミコロンを付ける代わりに、波かっこを使い、その特定の型に対してトレイトのメソッドに持たせたい具体的な振る舞いをメソッド本体に記述します。
ライブラリが NewsArticle と SocialPost に対して Summary トレイトを実装したので、クレートの利用者は、通常のメソッドを呼び出すのと同じ方法で、NewsArticle と SocialPost のインスタンスに対してトレイトメソッドを呼び出せます。唯一の違いは、利用者が型だけでなくトレイトもスコープに導入しなければならないことです。以下は、バイナリクレートが私たちの aggregator ライブラリクレートを利用する例です。
use aggregator::{SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
このコードは 1 new post: horse_ebooks: of course, as you probably already know, people を出力します。
aggregator クレートに依存する他のクレートも、自分たちの型に Summary を実装するために Summary トレイトをスコープに導入できます。注意すべき制約が 1 つあります。それは、トレイトか型のいずれか一方、またはその両方が自分たちのクレートにローカルである場合にのみ、ある型に対してトレイトを実装できるということです。たとえば、SocialPost 型は私たちの aggregator クレートにローカルなので、aggregator クレートの機能の一部として、SocialPost のようなカスタム型に標準ライブラリの Display トレイトを実装できます。また、Summary トレイトは私たちの aggregator クレートにローカルなので、aggregator クレート内で Vec<T> に対して Summary を実装することもできます。
しかし、外部の型に対して外部のトレイトを実装することはできません。たとえば、Display と Vec<T> はどちらも標準ライブラリで定義されており、私たちの aggregator クレートにはローカルではないため、aggregator クレート内で Vec<T> に Display トレイトを実装することはできません。この制約は コヒーレンス と呼ばれる性質の一部であり、より具体的には 孤児ルール と呼ばれます。これは親となる型が存在しないことに由来する名前です。このルールにより、他人のコードがあなたのコードを壊したり、その逆が起きたりしないことが保証されます。このルールがなければ、2 つのクレートが同じ型に同じトレイトを実装できてしまい、どの実装を使うべきかを Rust は判断できなくなります。
デフォルト実装を使う
あるトレイト内の一部またはすべてのメソッドについて、すべての型で実装を必須にするのではなく、デフォルトの振る舞いを持たせることが便利な場合があります。そうすれば、そのトレイトを特定の型に実装するときに、各メソッドのデフォルトの振る舞いをそのまま使うことも、オーバーライドすることもできます。
リスト 10-14 では、リスト 10-12 のように Summary トレイトでメソッドシグネチャだけを定義するのではなく、summarize メソッドに対してデフォルトの文字列を指定しています。
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
NewsArticle のインスタンスを要約するためにデフォルト実装を使うには、impl Summary for NewsArticle {} のように空の impl ブロックを指定します。
NewsArticle に対して summarize メソッドをもはや
直接定義していないにもかかわらず、デフォルト実装を提供し、さらに
NewsArticle が Summary トレイトを実装していることを指定しています。その結果、
次のように NewsArticle のインスタンスで summarize メソッドを呼び出せます:
use aggregator::{self, NewsArticle, Summary};
fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};
println!("New article available! {}", article.summarize());
}
このコードは New article available! (Read more...) を出力します。
デフォルト実装を作成しても、リスト 10-13 の SocialPost に対する
Summary の実装については、何も変更する必要はありません。その理由は、
デフォルト実装をオーバーライドするための構文が、デフォルト実装を持たない
トレイトメソッドを実装するための構文と同じだからです。
デフォルト実装は、同じトレイト内のほかのメソッドを呼び出すこともできます。たとえそれらの
ほかのメソッドにデフォルト実装がなくても構いません。このようにして、トレイトは
多くの有用な機能を提供しつつ、実装者にはそのうちの
ごく一部だけを指定させることができます。たとえば、Summary トレイトに
実装が必須の summarize_author メソッドを定義し、そのうえで
summarize_author メソッドを呼び出すデフォルト実装を持つ summarize
メソッドを定義できます:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
このバージョンの Summary を使うには、summarize_author を定義するだけで十分です。
それは、ある型に対してこのトレイトを実装するときです:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
summary_author を定義した後は、SocialPost 構造体のインスタンスに対して
summarize を呼び出せます。すると、summarize のデフォルト実装が
私たちの用意した summarize_author の定義を呼び出します。私たちが
summarize_author を実装しているので、Summary トレイトは
これ以上コードを書かなくても summarize メソッドの
振る舞いを与えてくれます。次のようになります:
use aggregator::{self, SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
このコードは 1 new post: (Read more from @horse_ebooks...) を出力します。
なお、同じメソッドのオーバーライド実装から、そのメソッドの デフォルト実装を呼び出すことはできません。
パラメータとしてトレイトを使う
トレイトの定義と実装の方法がわかったので、次は
トレイトを使ってさまざまな型を受け取る関数を定義する方法を見ていきましょう。ここでは、
リスト 10-13 で NewsArticle 型と SocialPost 型に実装した
Summary トレイトを使って、item パラメータに対して summarize メソッドを
呼び出す notify 関数を定義します。この item は Summary
トレイトを実装する何らかの型です。これを行うには、次のように impl Trait 構文を使います:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
item パラメータに具体的な型を指定する代わりに、impl
キーワードとトレイト名を指定します。このパラメータは、指定された
トレイトを実装する任意の型を受け取ります。notify の本体では、item に対して
Summary トレイト由来の任意のメソッド、たとえば summarize を
呼び出せます。notify を呼び出して NewsArticle や SocialPost の任意の
インスタンスを渡すことができます。String や i32 のような
ほかの型でこの関数を呼び出そうとするコードはコンパイルされません。
なぜなら、それらの型は Summary を実装していないからです。
トレイト境界の構文
impl Trait 構文は単純なケースでは機能しますが、実際には
トレイト境界 として知られる、より長い形式の糖衣構文です。次のようになります:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
この長い形式は前の節の例と等価ですが、より冗長です。 トレイト境界は、コロンの後、山かっこの内側で、ジェネリック型 パラメータの宣言とともに配置します。
impl Trait 構文は便利で、単純なケースではより簡潔なコードになりますが、
より完全なトレイト境界構文は、別のケースでより複雑なことを表現できます。
たとえば、Summary を実装する 2 つのパラメータを持たせることができます。これを
impl Trait 構文で書くと、次のようになります:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
impl Trait を使うのは、この関数で item1 と
item2 に異なる型を許可したい場合に適しています(両方の型が Summary を実装している限り)。
しかし、両方のパラメータを同じ型にしたいのであれば、次のように
トレイト境界を使わなければなりません:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
item1 と item2 の型として指定されたジェネリック型 T
は、この関数に制約を与えます。すなわち、
item1 と item2 に引数として渡される値の具体的な型は
同じでなければなりません。
+ 構文による複数のトレイト境界
複数のトレイト境界を指定することもできます。たとえば、notify で
item に対して summarize だけでなく表示フォーマットも使いたいとします。その場合、notify
の定義で item は Display と Summary の両方を実装しなければならないと指定します。これは
+ 構文を使って行えます:
pub fn notify(item: &(impl Summary + Display)) {
+ 構文は、ジェネリック型に対するトレイト境界でも有効です:
pub fn notify<T: Summary + Display>(item: &T) {
2 つのトレイト境界を指定したので、notify の本体では summarize を呼び出し、
{} を使って item をフォーマットできます。
where 句でトレイト境界をより明確にする
トレイト境界をあまりに多く使うことには欠点があります。各ジェネリックはそれぞれ独自のトレイト
境界を持つため、複数のジェネリック型パラメータを持つ関数では、関数名と
パラメータリストの間に大量のトレイト境界情報が入ることがあり、
関数シグネチャが読みにくくなります。このため、Rust には、関数シグネチャの後ろにある
where 句の中でトレイト境界を指定する別構文があります。したがって、次のように書く代わりに:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
次のように where 句を使えます:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
unimplemented!()
}
この関数のシグネチャは、より雑然としていません。関数名、パラメータリスト、 戻り値の型が互いに近くにあり、多くのトレイト境界を持たない関数に近い形です。
トレイトを実装する型を返す
戻り値の位置でも impl Trait 構文を使って、トレイトを実装する
何らかの型の値を返すことができます。次のようになります:
```rust,ignore
# pub trait Summary {
# fn summarize(&self) -> String;
# }
#
# pub struct NewsArticle {
# pub headline: String,
# pub location: String,
# pub author: String,
# pub content: String,
# }
#
# impl Summary for NewsArticle {
# fn summarize(&self) -> String {
# format!("{}, by {} ({})", self.headline, self.author, self.location)
# }
# }
#
# pub struct SocialPost {
# pub username: String,
# pub content: String,
# pub reply: bool,
# pub repost: bool,
# }
#
# impl Summary for SocialPost {
# fn summarize(&self) -> String {
# format!("{}: {}", self.username, self.content)
# }
# }
#
fn returns_summarizable() -> impl Summary {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
戻り値の型に impl Summary を使うことで、returns_summarizable 関数が、具体的な型名を挙げることなく、Summary トレイトを実装している何らかの型を返すことを指定できます。この場合、returns_summarizable は SocialPost を返しますが、この関数を呼び出すコードはそれを知る必要はありません。
戻り値の型を、それが実装しているトレイトだけで指定できることは、13章で扱うクロージャとイテレータの文脈で特に有用です。クロージャとイテレータは、コンパイラだけが知っている型や、指定するには非常に長い型を生み出します。impl Trait 構文を使うと、非常に長い型を書き出さなくても、関数が Iterator トレイトを実装する何らかの型を返すことを簡潔に指定できます。
しかし、impl Trait を使えるのは、単一の型を返す場合だけです。たとえば、戻り値の型を impl Summary として、NewsArticle または SocialPost のいずれかを返す次のコードは動作しません。
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"Penguins win the Stanley Cup Championship!",
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
} else {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
}
NewsArticle または SocialPost のいずれかを返すことが許されないのは、コンパイラにおける impl Trait 構文の実装方法に関する制約があるためです。このような振る舞いを持つ関数の書き方については、18章の「トレイトオブジェクトを使って共有された振る舞いを抽象化する」節で扱います。
トレイト境界を使って条件付きでメソッドを実装する
ジェネリック型パラメータを使う impl ブロックにトレイト境界を用いることで、指定したトレイトを実装している型に対してだけ、条件付きでメソッドを実装できます。たとえば、リスト10-15の型 Pair<T> は、常に new 関数を実装しており、新しい Pair<T> のインスタンスを返します(Self は impl ブロックの型に対する型エイリアスであり、この場合は Pair<T> であることを、5章の「メソッド記法」節で思い出してください)。しかし、次の impl ブロックでは、Pair<T> の内部の型 T が、比較を可能にする PartialOrd トレイト かつ 表示を可能にする Display トレイトを実装している場合にのみ、Pair<T> は cmp_display メソッドを実装します。
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
また、別のトレイトを実装している任意の型に対して、トレイトを条件付きで実装することもできます。トレイト境界を満たすすべての型に対するトレイトの実装は、ブランケット実装 と呼ばれ、Rust標準ライブラリで広く使われています。たとえば、標準ライブラリは Display トレイトを実装している任意の型に対して ToString トレイトを実装しています。標準ライブラリの impl ブロックは、次のコードに似ています。
impl<T: Display> ToString for T {
// --中略--
}
標準ライブラリにはこのブランケット実装があるため、Display トレイトを実装している任意の型に対して、ToString トレイトで定義された to_string メソッドを呼び出せます。たとえば、整数は Display を実装しているので、次のように整数を対応する String 値に変換できます。
#![allow(unused)]
fn main() {
let s = 3.to_string();
}
ブランケット実装は、そのトレイトのドキュメントの「Implementors」セクションに表示されます。
トレイトとトレイト境界を使うと、ジェネリック型パラメータを用いて重複を減らすコードを書けるだけでなく、そのジェネリック型に特定の振る舞いを持たせたいことをコンパイラに指定できます。するとコンパイラは、トレイト境界の情報を使って、私たちのコードとともに使われるすべての具体的な型が正しい振る舞いを提供しているかを検査できます。動的型付け言語では、ある型にそのメソッドが定義されていないのにそのメソッドを呼び出すと、実行時にエラーになります。しかし Rust では、この種のエラーはコンパイル時に移されるため、コードを実行できるようになる前に問題を修正することが強制されます。さらに、振る舞いを実行時にチェックするコードを書く必要はありません。なぜなら、すでにコンパイル時にチェックしているからです。これにより、ジェネリクスの柔軟性を犠牲にすることなく、性能が向上します。
ライフタイムで参照を検証する
ライフタイムで参照を検証する
ライフタイムは、私たちがすでに使ってきたもう 1 種類のジェネリクスです。 型が望んだ振る舞いを持っていることを保証するのではなく、ライフタイムは 必要なあいだ参照が有効であることを保証します。
第 4 章の 「参照と借用」 節で 触れなかった詳細の 1 つは、Rust におけるすべての参照にはライフタイムがあり、 その参照が有効であるスコープを表すということです。ほとんどの場合、 ライフタイムは暗黙的で、型推論と同じように推論されます。複数の型が あり得る場合にだけ型注釈が必要になるのと同様に、参照のライフタイムが いくつかの異なる形で関係し得る場合には、ライフタイム注釈を付けなければ なりません。Rust は、実行時に実際に使われる参照が確実に有効になるよう、 ジェネリックなライフタイムパラメータを使ってその関係を注釈することを 要求します。
ライフタイムに注釈を付けるという考え方は、ほかの多くのプログラミング言語 にはまったくないものなので、見慣れないと感じるでしょう。この章では ライフタイムのすべてを扱うわけではありませんが、この概念に慣れられるよう、 ライフタイム構文に出会う典型的な場面について説明します。
ダングリング参照
ライフタイムの主な目的は、ダングリング参照を防ぐことです。もしそれが 存在できてしまうと、プログラムは本来参照すべきデータとは別のデータを 参照してしまうことになります。外側のスコープと内側のスコープを持つ、 リスト 10-16 のプログラムを考えてみましょう。
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
注: リスト 10-16、10-17、10-23 の例では、変数に初期値を与えずに 宣言しているため、変数名は外側のスコープに存在します。一見すると、 これは Rust に null 値がないことと矛盾しているように見えるかもしれません。 しかし、値を与える前に変数を使おうとするとコンパイル時エラーになり、 そのことから実際に Rust は null 値を許していないことが分かります。
外側のスコープでは、初期値を持たない r という変数を宣言し、内側の
スコープでは、初期値 5 を持つ x という変数を宣言します。内側の
スコープの中で、r の値を x への参照にしようとします。その後、
内側のスコープが終わり、r に入っている値を出力しようとします。この
コードはコンパイルできません。r が参照している値が、使おうとする前に
スコープを抜けてしまっているからです。以下がエラーメッセージです。
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
5 | let x = 5;
| - binding `x` declared here
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
8 |
9 | println!("r: {r}");
| - borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
エラーメッセージは、変数 x が「十分長く生存しない」と述べています。
理由は、7 行目で内側のスコープが終わるときに x がスコープ外になるから
です。しかし r はまだ外側のスコープで有効です。そのスコープのほうが
大きいため、「より長く生きる」と言います。もし Rust がこのコードを
動作させることを許したなら、r は x がスコープを抜けたときに解放
されたメモリを参照することになり、r で何をしようとしても正しく動作
しなくなります。では、Rust はどのようにしてこのコードが不正だと判断
するのでしょうか。借用チェッカーを使います。
借用チェッカー
Rust コンパイラには、すべての借用が有効かどうかを判断するためにスコープ を比較する 借用チェッカー があります。リスト 10-17 はリスト 10-16 と 同じコードですが、変数のライフタイムを示す注釈が付いています。
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
ここでは、r のライフタイムに 'a、x のライフタイムに 'b という
注釈を付けています。ご覧のとおり、内側の 'b ブロックは、外側の 'a
ライフタイムブロックよりずっと小さくなっています。コンパイル時に、
Rust は 2 つのライフタイムの大きさを比較し、r は 'a のライフタイム
を持つ一方で、'b のライフタイムを持つメモリを参照していることを
確認します。プログラムは却下されます。なぜなら 'b は 'a より短い
からです。つまり、参照先は参照そのものと同じだけ長く生きないのです。
リスト 10-18 では、ダングリング参照が生じないようにコードを修正しており、 エラーなくコンパイルできます。
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {r}"); // | |
// --+ |
} // ----------+
ここで、x はライフタイム 'b を持ち、この場合 'a より長くなって
います。つまり r は x を参照できます。なぜなら、x が有効である
あいだ、r に入っている参照も常に有効だと Rust が分かっているからです。
これで、参照のライフタイムがどこにあり、Rust がライフタイムをどのように 解析して参照が常に有効であることを保証しているかが分かったので、関数の 引数と戻り値におけるジェネリックなライフタイムを見ていきましょう。
関数におけるジェネリックなライフタイム
2 つの文字列スライスのうち長いほうを返す関数を書いてみましょう。この
関数は 2 つの文字列スライスを受け取り、1 つの文字列スライスを返します。
longest 関数を実装したあと、リスト 10-19 のコードは
The longest string is abcd と出力するはずです。
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
この関数には文字列そのものではなく、参照である文字列スライスを受け取ら
せたいことに注意してください。longest 関数に引数の所有権を奪わせたく
ないからです。リスト 10-19 で使っている引数が望ましいものである理由に
ついては、第 4 章の 「引数としての文字列スライス」
を参照してください。
リスト 10-20 のように longest 関数を実装しようとすると、コンパイル
できません。
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
代わりに、ライフタイムに関する以下のエラーが出ます。
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
ヘルプテキストを見ると、戻り値の型にはジェネリックなライフタイム
パラメータが必要だと分かります。Rust には、返される参照が x を指す
のか y を指すのか判断できないからです。実際、私たちにも分かりません。
この関数本体の if ブロックは x への参照を返し、else ブロックは
y への参照を返すからです。
この関数を定義している時点では、この関数に渡される具体的な値がわからないため、if のケースと else のケースのどちらが実行されるかはわかりません。また、渡される参照の具体的なライフタイムもわからないため、リスト10-17と10-18で行ったようにスコープを見て、返す参照が常に有効かどうかを判断することもできません。借用チェッカーにもこれを判断することはできません。なぜなら、x と y のライフタイムが戻り値のライフタイムとどのような関係にあるのかを知らないからです。このエラーを修正するために、参照同士の関係を定義するジェネリックなライフタイムパラメータを追加し、借用チェッカーが解析を実行できるようにします。
ライフタイム注釈の構文
ライフタイム注釈は、どの参照がどれだけ長く生きるかを変更するものではありません。そうではなく、ライフタイムに影響を与えることなく、複数の参照のライフタイム同士の関係を記述するものです。関数がシグネチャでジェネリックな型パラメータを指定することで任意の型を受け取れるのと同じように、関数はジェネリックなライフタイムパラメータを指定することで、任意のライフタイムを持つ参照を受け取れます。
ライフタイム注釈の構文は少し独特です。ライフタイムパラメータの名前はアポストロフィ (') で始めなければならず、通常はすべて小文字で、ジェネリック型のように非常に短い名前になります。ほとんどの人は、最初のライフタイム注釈に 'a という名前を使います。ライフタイムパラメータの注釈は参照の & の後ろに置き、注釈と参照先の型は空白で区切ります。
以下にいくつか例を示します。ライフタイムパラメータのない i32 への参照、'a というライフタイムパラメータを持つ i32 への参照、そして同じくライフタイム 'a を持つ i32 への可変参照です。
&i32 // 参照
&'a i32 // 明示的なライフタイムを持つ参照
&'a mut i32 // 明示的なライフタイムを持つ可変参照
ライフタイム注釈は1つだけではあまり意味を持ちません。というのも、注釈は複数の参照のジェネリックなライフタイムパラメータ同士がどのような関係にあるのかを Rust に伝えるためのものだからです。longest 関数の文脈で、ライフタイム注釈同士がどのように関係するのかを見ていきましょう。
関数シグネチャ内で
関数シグネチャでライフタイム注釈を使うには、ジェネリックな型パラメータのときと同じように、関数名とパラメータリストの間の山かっこの中でジェネリックなライフタイムパラメータを宣言する必要があります。
このシグネチャでは、次の制約を表したいと考えています。返される参照は、2つのパラメータの両方が有効である限り有効である、ということです。これが、パラメータのライフタイムと戻り値のライフタイムの関係です。ライフタイムに 'a という名前を付け、それを各参照に追加します。リスト10-21に示すとおりです。
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
このコードはコンパイルでき、リスト10-19の main 関数と一緒に使うと、期待どおりの結果を生成するはずです。
この関数シグネチャは Rust に対して、あるライフタイム 'a について、この関数が2つのパラメータを受け取り、そのどちらも少なくともライフタイム 'a の間は有効な文字列スライスであることを伝えています。また、この関数シグネチャは、関数から返される文字列スライスも少なくともライフタイム 'a の間は有効であることを Rust に伝えています。実際には、これは longest 関数が返す参照のライフタイムが、関数引数が参照している値のライフタイムのうち短いほうと同じであることを意味します。これらの関係こそが、このコードを解析するときに Rust に使ってほしいものです。
覚えておいてください。この関数シグネチャでライフタイムパラメータを指定しても、渡される値や返される値のライフタイム自体を変更しているわけではありません。そうではなく、借用チェッカーはこれらの制約に従わない値を拒否すべきだと指定しているのです。longest 関数は、x と y が正確にどれだけ生きるかを知る必要はなく、このシグネチャを満たす何らかのスコープを 'a に当てはめられることだけが必要です。
関数でライフタイムを注釈する場合、注釈は関数本体ではなく関数シグネチャに記述します。ライフタイム注釈は、シグネチャ内の型とよく似た形で、関数の契約の一部になります。関数シグネチャにライフタイムの契約を含めることで、Rust コンパイラが行う解析はより単純になります。関数の注釈のしかたや呼び出しかたに問題がある場合、コンパイラエラーはコードのどの部分とどの制約に問題があるのかを、より正確に指し示せます。逆に、Rust コンパイラがライフタイム同士の関係について私たちの意図をもっと推論していたなら、コンパイラは問題の原因から何段階も離れたコードの使用箇所しか指し示せないかもしれません。
longest に具体的な参照を渡すとき、'a に代入される具体的なライフタイムは、x のスコープと y のスコープが重なっている部分になります。言い換えると、ジェネリックなライフタイム 'a には、x と y のライフタイムのうち短いほうと等しい具体的なライフタイムが入ります。返される参照にも同じライフタイムパラメータ 'a を注釈しているため、返される参照も x と y のライフタイムのうち短いほうの長さだけ有効になります。
ライフタイム注釈が longest 関数をどのように制限するのかを、具体的なライフタイムが異なる参照を渡して見てみましょう。リスト10-22はわかりやすい例です。
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {result}");
}
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
この例では、string1 は外側のスコープの終わりまで有効で、string2 は内側のスコープの終わりまで有効で、result は内側のスコープの終わりまで有効な何かを参照しています。このコードを実行すると、借用チェッカーがこれを許可することがわかります。コンパイルされ、The longest string is long string is long と出力されます。
次に、result 内の参照のライフタイムが2つの引数のうち短いほうのライフタイムでなければならないことを示す例を試してみましょう。result 変数の宣言を内側のスコープの外に移動しますが、result 変数への値の代入は string2 があるスコープの内側に残します。次に、result を使う println! を内側のスコープの外、つまりそのスコープが終わった後に移動します。リスト10-23のコードはコンパイルされません。
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {result}");
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
このコードをコンパイルしようとすると、次のエラーが表示されます。
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
5 | let string2 = String::from("xyz");
| ------- binding `string2` declared here
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("The longest string is {result}");
| ------ borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
このエラーは、result が println! 文に対して有効であるためには、
string2 が外側のスコープの終わりまで有効である必要があることを示して
います。Rust がこれを知っているのは、同じライフタイムパラメータ 'a を
使って関数パラメータと戻り値のライフタイムを注釈したからです。
人間である私たちは、このコードを見て、string1 が string2 より長く、
したがって result には string1 への参照が入ることが分かります。
string1 はまだスコープを抜けていないため、string1 への参照は
println! 文でも依然として有効です。しかし、この場合にその参照が有効で
あることをコンパイラは見抜けません。私たちは、longest 関数によって返される
参照のライフタイムが、渡された参照のライフタイムのうち短い方と同じであると
Rust に伝えています。そのため、borrow checker はリスト 10-23 のコードを、
無効な参照を持つ可能性があるものとして許可しません。
longest 関数に渡す参照の値やライフタイム、および返された参照の使い方を
変えた、さらに多くの実験を設計してみてください。コンパイルする前に、その
実験が borrow checker を通過するかどうかについて仮説を立ててみましょう。
その後、実際に確かめて、自分の予想が正しかったか確認してください。
関係
どのようにライフタイムパラメータを指定する必要があるかは、関数が何をして
いるかによって異なります。たとえば、longest 関数の実装を、常に最長の
文字列スライスではなく最初のパラメータを返すように変更したとすると、y
パラメータにライフタイムを指定する必要はありません。次のコードはコンパイル
されます。
fn main() {
let string1 = String::from("abcd");
let string2 = "efghijklmnopqrstuvwxyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
x パラメータと戻り値の型にはライフタイムパラメータ 'a を指定しましたが、
y パラメータには指定していません。これは、y のライフタイムが x の
ライフタイムや戻り値と何の関係もないからです。
関数から参照を返す場合、戻り値の型のライフタイムパラメータは、いずれかの
パラメータのライフタイムパラメータと一致している必要があります。返される
参照がパラメータのいずれかを指していない場合、それはこの関数内で作成された
値を指していなければなりません。しかし、この場合、それはダングリング参照に
なってしまいます。なぜなら、その値は関数の終わりでスコープを抜けるからです。
コンパイルできない longest 関数の次の実装を考えてみましょう。
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
ここでは、戻り値の型にライフタイムパラメータ 'a を指定しているにも
かかわらず、この実装はコンパイルに失敗します。というのも、戻り値の
ライフタイムがパラメータのライフタイムとまったく関係していないからです。
以下は実際に得られるエラーメッセージです。
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ------^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `result` is borrowed here
For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
問題は、result が longest 関数の終わりでスコープを抜け、クリーンアップ
されることです。さらに、私たちはその関数から result への参照を返そうと
しています。このダングリング参照を変えることができるようなライフタイム
パラメータの指定方法は存在せず、Rust はダングリング参照を作ることを許し
ません。この場合の最善の修正は、参照ではなく所有権を持つデータ型を返す
ことです。そうすれば、その値のクリーンアップは呼び出し元の関数の責任に
なります。
最終的に、ライフタイム構文とは、関数のさまざまなパラメータや戻り値の ライフタイムを結び付けるためのものです。ひとたびそれらが結び付けられれば、 Rust はメモリ安全な操作を許可し、ダングリングポインタを作成したり、その ほかの形でメモリ安全性を損なったりする操作を拒否するための十分な情報を 得られます。
構造体定義内
ここまでに定義してきた構造体は、すべて所有権を持つ型を保持していました。
参照を保持する構造体を定義することもできますが、その場合は構造体定義内の
すべての参照にライフタイム注釈を追加する必要があります。リスト 10-24 には、
文字列スライスを保持する ImportantExcerpt という名前の構造体があります。
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
この構造体には、文字列スライスを保持する part という単一のフィールドが
あります。文字列スライスは参照です。ジェネリックなデータ型と同様に、構造体名の
後ろの山かっこ内にジェネリックなライフタイムパラメータ名を宣言します。これに
より、構造体定義の本体でそのライフタイムパラメータを使用できます。この注釈は、
ImportantExcerpt のインスタンスが、その part フィールドに保持している
参照より長く生存できないことを意味します。
ここでの main 関数は、変数 novel が所有する String の最初の文への参照を
保持する ImportantExcerpt 構造体のインスタンスを作成します。novel の
データは ImportantExcerpt インスタンスが作成される前から存在しています。
さらに、novel は ImportantExcerpt がスコープを抜けた後までスコープを
抜けないため、ImportantExcerpt インスタンス内の参照は有効です。
ライフタイム省略
すべての参照にはライフタイムがあり、参照を使う関数や構造体にはライフタイム パラメータを指定する必要があることを学びました。しかし、リスト 4-9 で定義した 関数には、ライフタイム注釈がなくてもコンパイルできたものがありました。これを リスト 10-25 に再掲します。
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// first_word works on slices of `String`s
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word works on slices of string literals
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
この関数がライフタイム注釈なしでコンパイルできる理由は、歴史的なものです。 Rust の初期バージョン(1.0 より前)では、このコードはコンパイルできません でした。というのも、すべての参照に明示的なライフタイムが必要だったからです。 当時、この関数シグネチャは次のように書かれていたはずです。
fn first_word<'a>(s: &'a str) -> &'a str {
大量のRustコードを書いたあと、Rustチームは、Rustプログラマが特定の状況で同じライフタイム注釈を何度も繰り返し書いていることを見いだしました。こうした状況は予測可能で、いくつかの決定的なパターンに従っていました。開発者たちはこれらのパターンをコンパイラのコードに組み込み、その結果、借用チェッカーはこうした状況ではライフタイムを推論できるようになり、明示的な注釈を必要としなくなりました。
このRustの歴史の一片が重要なのは、今後さらに多くの決定的なパターンが見つかり、コンパイラに追加される可能性があるからです。将来的には、必要となるライフタイム注釈はさらに少なくなるかもしれません。
Rustの参照解析に組み込まれているパターンは、ライフタイム省略規則 と呼ばれます。これらはプログラマが従うための規則ではありません。コンパイラが考慮する特定のケースの集合であり、あなたのコードがそれらのケースに当てはまるなら、ライフタイムを明示的に書く必要はありません。
省略規則は完全な推論を提供するわけではありません。Rustがこれらの規則を適用したあとでも、参照がどのライフタイムを持つかについて曖昧さが残る場合、コンパイラは残った参照のライフタイムがどうあるべきかを推測しません。推測する代わりに、コンパイラはエラーを出し、あなたはライフタイム注釈を追加することでそれを解決できます。
関数やメソッドのパラメータ上のライフタイムは 入力ライフタイム と呼ばれ、戻り値上のライフタイムは 出力ライフタイム と呼ばれます。
コンパイラは、明示的な注釈がないときに参照のライフタイムを導き出すために3つの規則を使います。最初の規則は入力ライフタイムに適用され、2番目と3番目の規則は出力ライフタイムに適用されます。コンパイラが3つの規則の終わりまで到達しても、なおライフタイムを導き出せない参照がある場合、コンパイラはエラーで停止します。これらの規則は fn 定義だけでなく impl ブロックにも適用されます。
最初の規則は、コンパイラが参照である各パラメータにライフタイムパラメータを割り当てる、というものです。言い換えると、1つのパラメータを持つ関数は1つのライフタイムパラメータを受け取ります: fn foo<'a>(x: &'a i32)。2つのパラメータを持つ関数は2つの別個のライフタイムパラメータを受け取ります: fn foo<'a, 'b>(x: &'a i32, y: &'b i32)。以降も同様です。
2番目の規則は、入力ライフタイムパラメータがちょうど1つである場合、そのライフタイムがすべての出力ライフタイムパラメータに割り当てられる、というものです: fn foo<'a>(x: &'a i32) -> &'a i32。
3番目の規則は、入力ライフタイムパラメータが複数あるが、そのうちの1つがメソッドであるため &self または &mut self である場合、self のライフタイムがすべての出力ライフタイムパラメータに割り当てられる、というものです。この3番目の規則により、必要な記号が少なくなるため、メソッドははるかに読み書きしやすくなります。
では、私たちがコンパイラだと仮定してみましょう。これらの規則を適用して、リスト10-25の first_word 関数のシグネチャにある参照のライフタイムを導き出します。シグネチャは、最初は参照に関連付けられたライフタイムがない状態で始まります。
fn first_word(s: &str) -> &str {
次に、コンパイラは最初の規則を適用します。この規則では、各パラメータがそれぞれ独自のライフタイムを受け取るとされています。いつものようにこれを 'a と呼ぶことにすると、シグネチャは次のようになります。
fn first_word<'a>(s: &'a str) -> &str {
入力ライフタイムがちょうど1つあるため、2番目の規則が適用されます。2番目の規則では、1つの入力パラメータのライフタイムが出力ライフタイムに割り当てられると定められているので、シグネチャは次のようになります。
fn first_word<'a>(s: &'a str) -> &'a str {
これで、この関数シグネチャ内のすべての参照がライフタイムを持つことになり、コンパイラは、この関数シグネチャでプログラマがライフタイムを注釈しなくても解析を続行できます。
次は別の例を見てみましょう。今度は、リスト10-20で最初に扱い始めたときにはライフタイムパラメータを持っていなかった longest 関数を使います。
fn longest(x: &str, y: &str) -> &str {
最初の規則を適用してみましょう。各パラメータがそれぞれ独自のライフタイムを受け取ります。今回は1つではなく2つのパラメータがあるので、2つのライフタイムがあります。
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
2つ以上の入力ライフタイムがあるため、2番目の規則は適用されないことがわかります。3番目の規則も適用されません。longest はメソッドではなく関数なので、どのパラメータも self ではないからです。3つの規則をすべて当てはめてみても、戻り値の型のライフタイムが何かはまだわかりません。これが、リスト10-20のコードをコンパイルしようとしたときにエラーになった理由です。コンパイラはライフタイム省略規則をひととおり適用しましたが、それでもシグネチャ内の参照のライフタイムをすべて導き出せませんでした。
3番目の規則は実際にはメソッドシグネチャにしか適用されないので、次はその文脈でライフタイムを見て、なぜ3番目の規則によってメソッドシグネチャでライフタイムを注釈する必要があまりないのかを確認しましょう。
メソッド定義において
ライフタイムを持つ構造体に対してメソッドを実装するときは、リスト10-11に示したように、ジェネリック型パラメータと同じ構文を使います。ライフタイムパラメータをどこで宣言して使うかは、それらが構造体のフィールドに関連しているのか、それともメソッドのパラメータと戻り値に関連しているのかによって決まります。
構造体フィールドのライフタイム名は、常に impl キーワードの後で宣言し、その後で構造体名の後に使う必要があります。なぜなら、それらのライフタイムは構造体の型の一部だからです。
impl ブロック内のメソッドシグネチャでは、参照が構造体のフィールド内の参照のライフタイムに結び付いている場合もあれば、独立している場合もあります。さらに、ライフタイム省略規則により、メソッドシグネチャではライフタイム注釈が不要になることがよくあります。リスト10-24で定義した ImportantExcerpt という名前の構造体を使ったいくつかの例を見てみましょう。
まず、level という名前のメソッドを使います。このメソッドの唯一のパラメータは self への参照で、戻り値は i32 であり、何かへの参照ではありません。
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
impl の後にあるライフタイムパラメータ宣言と、型名の後でのその使用は必要ですが、最初の省略規則のおかげで、self への参照のライフタイムを注釈する必要はありません。
次は、3番目のライフタイム省略規則が適用される例です。
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
2つの入力ライフタイムがあるため、Rustは最初のライフタイム省略規則を適用し、&self と announcement の両方にそれぞれ独自のライフタイムを与えます。そして、パラメータの1つが &self であるため、戻り値の型は &self のライフタイムを受け取り、すべてのライフタイムが考慮されたことになります。
静的ライフタイム
ここで取り上げる必要のある特別なライフタイムの 1 つが 'static で、これは対象の参照がプログラムの実行期間全体にわたって生存 できる ことを示します。すべての文字列リテラルは 'static ライフタイムを持っており、次のように注釈を付けられます。
#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}
この文字列のテキストはプログラムのバイナリに直接格納されており、常に利用可能です。そのため、すべての文字列リテラルのライフタイムは 'static です。
エラーメッセージの中で、'static ライフタイムを使うよう提案されることがあります。しかし、参照のライフタイムとして 'static を指定する前に、その参照が実際にプログラムのライフタイム全体にわたって生存するのか、そしてそうしてよいのかを考えてください。ほとんどの場合、'static ライフタイムを提案するエラーメッセージは、ダングリング参照を作ろうとしているか、利用可能なライフタイムの不一致があることに起因します。そのような場合の解決策は、'static ライフタイムを指定することではなく、それらの問題を修正することです。
ジェネリック型パラメータ、トレイト境界、ライフタイム
ジェネリック型パラメータ、トレイト境界、ライフタイムをすべて 1 つの関数で指定する構文を、簡単に見てみましょう。
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest_with_an_announcement(
string1.as_str(),
string2,
"Today is someone's birthday!",
);
println!("The longest string is {result}");
}
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {ann}");
if x.len() > y.len() { x } else { y }
}
これは、2 つの文字列スライスのうち長いほうを返す、リスト 10-21 の longest 関数です。ただし今度は、ジェネリック型 T の ann という追加のパラメータがあります。この T には、where 句で指定されているように Display トレイトを実装する任意の型を入れることができます。この追加のパラメータは {} を使って表示されるため、Display トレイト境界が必要です。ライフタイムはジェネリックの一種なので、ライフタイムパラメータ 'a とジェネリック型パラメータ T の宣言は、関数名の後の山かっこ内にある同じリストに入ります。
まとめ
この章ではたくさんの内容を扱いました。ジェネリック型パラメータ、トレイトとトレイト境界、そしてジェネリックなライフタイムパラメータについて理解したので、さまざまな状況で動作する、重複のないコードを書けるようになりました。ジェネリック型パラメータを使うと、異なる型に対して同じコードを適用できます。トレイトとトレイト境界は、型がジェネリックであっても、そのコードが必要とする振る舞いを備えていることを保証します。また、ライフタイム注釈を使って、この柔軟なコードにダングリング参照が含まれないようにする方法も学びました。そして、この解析はすべてコンパイル時に行われるため、実行時の性能には影響しません。
信じられないかもしれませんが、この章で扱ったトピックについては、まだまだ学ぶべきことがあります。第 18 章では、トレイトを使うもう 1 つの方法であるトレイトオブジェクトについて説明します。また、ライフタイム注釈に関するより複雑なシナリオもありますが、それらが必要になるのは非常に高度な場面だけです。そのような内容については、Rust Reference を読むとよいでしょう。次は、コードが期待どおりに動作していることを確認できるように、Rust でテストを書く方法を学びます。
自動テストを書く
1972年のエッセイ「The Humble Programmer」で、エドガー・W・ダイクストラは「プログラム テストはバグの存在を示すには非常に効果的な方法になりうるが、その不在を示すには 絶望的なまでに不十分である」と述べています。だからといって、できる限り多くを テストしようとしなくてよい、という意味ではありません!
私たちのプログラムにおける 正しさ とは、コードが私たちの意図したとおりのことを 行う度合いのことです。Rust はプログラムの正しさを非常に重視して設計されています が、正しさは複雑であり、証明するのは容易ではありません。Rust の型システムはこの 負担の大部分を担いますが、型システムですべてを捉えられるわけではありません。そ のため、Rust には自動ソフトウェアテストを書くためのサポートが含まれています。
たとえば、渡された数値に 2 を足す関数 add_two を書いたとします。この関数のシグ
ネチャは整数を引数として受け取り、結果として整数を返します。その関数を実装してコ
ンパイルするとき、Rust はこれまで学んできた型チェックと借用チェックをすべて行い、
たとえばこの関数に String の値や無効な参照を渡していないことを保証します。しか
し Rust には、この関数が私たちの意図どおりに正確に動作すること、つまり引数に 10
を足したり 50 を引いたりするのではなく、引数に 2 を足した値を返すことまでは
確認できません。そこでテストの出番です。
たとえば、add_two 関数に 3 を渡したとき、返される値が 5 であることを検証す
るテストを書くことができます。コードに変更を加えるたびにこれらのテストを実行すれ
ば、既存の正しい振る舞いが変わっていないことを確認できます。
テストは複雑な技能です。良いテストの書き方に関するあらゆる詳細を 1 章で網羅する ことはできませんが、この章では Rust のテスト機能の仕組みについて説明します。テ ストを書くときに利用できるアノテーションやマクロ、テストを実行する際に提供される デフォルトの挙動やオプション、そしてテストをユニットテストと統合テストにどのよう に整理するかについて取り上げます。
テストの書き方
テストの書き方
テスト とは、テスト対象ではないコードが期待どおりに機能していることを検証する Rust の関数です。テスト関数の本体は、通常、次の 3 つの動作を行います。
- 必要なデータや状態をセットアップする。
- テストしたいコードを実行する。
- 結果が期待どおりであることをアサートする。
これらの動作を行うテストを書くために Rust が特別に提供している機能を見ていきましょう。これには、test 属性、いくつかのマクロ、そして should_panic 属性が含まれます。
テスト関数の構成
最も単純な形では、Rust のテストは test 属性が付けられた関数です。属性は Rust コードの各要素に付加されるメタデータです。その一例が、第 5 章で構造体とともに使った derive 属性です。関数をテスト関数に変えるには、fn の前の行に #[test] を追加します。cargo test コマンドでテストを実行すると、Rust は注釈付きの関数を実行するテストランナーバイナリをビルドし、各テスト関数が成功したか失敗したかを報告します。
Cargo で新しいライブラリプロジェクトを作ると、テスト関数を含むテストモジュールが自動的に生成されます。このモジュールはテストを書くためのテンプレートを提供してくれるので、新しいプロジェクトを始めるたびに正確な構造や構文を調べ直す必要がありません。追加のテスト関数やテストモジュールはいくつでも追加できます。
実際にコードをテストする前に、まずはテンプレートのテストを試しながら、テストがどのように動作するのかのいくつかの側面を見ていきましょう。その後、自分たちで書いたコードを呼び出し、その振る舞いが正しいことをアサートする実践的なテストを書きます。
2 つの数値を加算する adder という新しいライブラリプロジェクトを作成しましょう。
$ cargo new adder --lib
Created library `adder` project
$ cd adder
あなたの adder ライブラリの src/lib.rs ファイルの内容は、リスト 11-1 のようになるはずです。
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
このファイルは、テストする対象を用意するためのサンプル add 関数で始まっています。
今のところは、it_works 関数だけに注目しましょう。#[test] アノテーションに注目してください。この属性は、この関数がテスト関数であることを示します。そのため、テストランナーはこの関数をテストとして扱うべきだと分かります。共通のシナリオをセットアップしたり、共通の操作を実行したりするのを助けるために、tests モジュールの中にテストではない関数を置くこともあるので、どの関数がテストなのかを常に明示する必要があります。
このサンプル関数の本体では、assert_eq! マクロを使って、add を 2 と 2 で呼び出した結果を含む result が 4 に等しいことをアサートしています。このアサーションは、典型的なテストの形式の例になっています。このテストが成功することを確認するために、実行してみましょう。
cargo test コマンドは、リスト 11-2 に示すように、プロジェクト内のすべてのテストを実行します。
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.57s
Running unittests src/lib.rs (target/debug/deps/adder-01ad14159ff659ab)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Cargo はテストをコンパイルして実行しました。running 1 test という行が表示されています。次の行には、自動生成された tests::it_works という名前のテスト関数と、そのテストを実行した結果が ok であることが示されています。全体の要約 test result: ok. は、すべてのテストが成功したことを意味し、1 passed; 0 failed という部分は成功したテストと失敗したテストの件数を示しています。
特定の場合に実行されないように、テストを無視対象としてマークすることもできます。これについては、この章の後半にある 「明示的に要求された場合を除いてテストを無視する」 節で扱います。ここではまだそうしていないので、要約には 0 ignored と表示されています。また、cargo test コマンドに引数を渡して、名前がある文字列に一致するテストだけを実行することもできます。これは フィルタリング と呼ばれ、この章の 「名前でテストの一部を実行する」 節で扱います。ここでは実行するテストを絞り込んでいないので、要約の最後には 0 filtered out と表示されています。
0 measured という統計は、性能を測定するベンチマークテストのためのものです。本書執筆時点では、ベンチマークテストは nightly Rust でのみ利用できます。詳しくは、ベンチマークテストに関するドキュメントを参照してください。
Doc-tests adder から始まるテスト出力の次の部分は、ドキュメンテーションテストの結果です。まだドキュメンテーションテストはありませんが、Rust は API ドキュメントに含まれるコード例をコンパイルできます。この機能は、ドキュメントとコードの同期を保つのに役立ちます。ドキュメンテーションテストの書き方については、第 14 章の 「テストとしてのドキュメンテーションコメント」 節で説明します。今のところは、Doc-tests の出力は無視しましょう。
では、テストを自分たちのニーズに合わせてカスタマイズしていきましょう。まず、it_works 関数の名前を exploration のような別の名前に変更します。
ファイル名: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
次に、もう一度 cargo test を実行します。出力には、it_works ではなく exploration が表示されるようになります。
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.59s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
次に、別のテストを追加しますが、今回は失敗するテストを作ります。テスト関数の中で何かがパニックを起こすと、テストは失敗します。各テストは新しいスレッドで実行され、メインスレッドがテストスレッドがパニックによって終了したことを検知すると、そのテストは失敗としてマークされます。第 9 章で説明したように、パニックを起こす最も単純な方法は panic! マクロを呼び出すことです。新しいテストを another という名前の関数として追加すると、src/lib.rs ファイルはリスト 11-3 のようになります。
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
fn another() {
panic!("Make this test fail");
}
}
cargo test を使って再びテストを実行します。出力はリスト 11-4 のようになるはずで、exploration テストは成功し、another は失敗したことが分かります。
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.72s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok
failures:
---- tests::another stdout ----
thread 'tests::another' panicked at src/lib.rs:17:9:
Make this test fail
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::another
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
ok の代わりに、test tests::another という行には FAILED と表示されています。個々の結果と要約の間には、新たに2つのセクションが現れます。1つ目は、各テスト失敗の詳細な理由を表示します。この場合、tests::another が失敗したのは、src/lib.rs ファイルの17行目で Make this test fail というメッセージとともにパニックしたためだ、という詳細が表示されます。次のセクションには、失敗したすべてのテストの名前だけが一覧表示されます。これは、テストが大量にあり、失敗時の詳細な出力も大量にある場合に便利です。失敗したテストの名前を使って、そのテストだけを実行し、より簡単にデバッグできます。テストの実行方法については、「テストの実行方法を制御する」節でもう少し詳しく説明します。
最後に表示される要約行は次のとおりです。全体として、テスト結果は FAILED です。1つのテストは通り、1つのテストは失敗しました。
これで、さまざまな状況でテスト結果がどのように見えるかがわかったので、次は panic! 以外でテストに役立つマクロをいくつか見ていきましょう。
assert! で結果を確認する
標準ライブラリが提供する assert! マクロは、テスト内のある条件が true と評価されることを確認したい場合に便利です。assert! マクロには、評価結果が真偽値になる引数を渡します。値が true であれば何も起こらず、テストは成功します。値が false であれば、assert! マクロは panic! を呼び出してテストを失敗させます。assert! マクロを使うと、コードが意図したとおりに動作しているかを確認しやすくなります。
第5章のリスト5-15では、Rectangle 構造体と can_hold メソッドを使いましたが、それらをここでリスト11-5として再掲します。このコードを src/lib.rs ファイルに入れ、その後 assert! マクロを使っていくつかテストを書いてみましょう。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
can_hold メソッドは真偽値を返すので、assert! マクロにはまさにうってつけのユースケースです。リスト11-6では、幅8、高さ7の Rectangle インスタンスを作成し、それが幅5、高さ1の別の Rectangle インスタンスを収められることをアサートすることで、can_hold メソッドをテストします。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
}
tests モジュール内の use super::*; という行に注目してください。tests モジュールは通常のモジュールであり、第7章の「モジュールツリー内の要素を参照するためのパス」
節で扱った通常の可視性ルールに従います。tests モジュールは内部モジュールなので、外側のモジュールにあるテスト対象コードを内部モジュールのスコープに持ち込む必要があります。ここではグロブを使っているため、外側のモジュールで定義したものはすべてこの tests モジュールから利用できます。
このテストには larger_can_hold_smaller という名前を付け、必要な2つの Rectangle インスタンスを作成しています。次に、assert! マクロを呼び出し、larger.can_hold(&smaller) を呼び出した結果を渡しました。この式は true を返すはずなので、テストは成功するはずです。確認してみましょう!
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 1 test
test tests::larger_can_hold_smaller ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
確かに通りました! 今度は、小さい長方形が大きい長方形を収められないことをアサートする別のテストを追加してみましょう。
ファイル名: src/lib.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
// --snip--
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
この場合、can_hold 関数の正しい結果は false なので、その結果を assert! マクロに渡す前に否定する必要があります。そのため、can_hold が false を返せば、このテストは成功します。
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
2つのテストが通りました! では次に、コードにバグを入れたときにテスト結果がどうなるかを見てみましょう。can_hold メソッドの実装を変更し、幅を比較している箇所で大なり記号(>)を小なり記号(<)に置き換えます。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// --snip--
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width < other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
ここでテストを実行すると、次のようになります。
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok
failures:
---- tests::larger_can_hold_smaller stdout ----
thread 'tests::larger_can_hold_smaller' panicked at src/lib.rs:28:9:
assertion failed: larger.can_hold(&smaller)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::larger_can_hold_smaller
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
テストがバグを見つけました! larger.width は 8、smaller.width は 5 なので、can_hold における幅の比較は теперь false を返します。8 は 5 より小さくないからです。
assert_eq! と assert_ne! で等価性をテストする
機能を検証する一般的な方法は、テスト対象コードの結果と、そのコードが返すと期待している値とが等しいかどうかをテストすることです。これは assert! マクロを使い、== 演算子を使った式を渡すことでも行えます。しかし、これは非常によくあるテストなので、標準ライブラリには、このテストをより便利に行うための一対のマクロ、assert_eq! と assert_ne! が用意されています。これらのマクロは、それぞれ2つの引数を等価または非等価で比較します。また、アサーションが失敗した場合には2つの値も表示してくれるため、テストが なぜ 失敗したのかを把握しやすくなります。これに対して assert! マクロは、== 式の評価結果が false だったことしか示さず、その false に至った値は表示しません。
リスト11-7では、引数に 2 を足す add_two という関数を書き、その関数を assert_eq! マクロを使ってテストします。
pub fn add_two(a: u64) -> u64 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
}
これが通ることを確認しましょう!
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
まず、add_two(2) を呼び出した結果を保持する result という名前の変数を作成します。次に、result と 4 を assert_eq! マクロの引数として渡します。このテストの出力行は test tests::it_adds_two ... ok であり、この ok という表示がテストに成功したことを示しています。
では、assert_eq! が失敗したときにどのように見えるかを知るために、コードにバグを入れてみましょう。add_two 関数の実装を変更して、代わりに 3 を足すようにします。
pub fn add_two(a: u64) -> u64 {
a + 3
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
}
もう一度テストを実行します。
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... FAILED
failures:
---- tests::it_adds_two stdout ----
thread 'tests::it_adds_two' panicked at src/lib.rs:12:9:
assertion `left == right` failed
left: 5
right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_adds_two
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
テストによってバグを検出できました! tests::it_adds_two テストは失敗し、メッセージは失敗したアサーションが left == right であったこと、そして left と right の値がそれぞれ何であるかを教えてくれます。このメッセージは、デバッグを始める助けになります。left 引数、つまり add_two(2) を呼び出した結果を入れていた側は 5 でしたが、right 引数は 4 でした。たくさんのテストを実行しているときには、これが特に役に立つことが想像できるでしょう。
一部の言語やテストフレームワークでは、等価性を検証するアサーション関数のパラメータは expected と actual と呼ばれ、引数を指定する順序が重要になります。しかし、Rust ではそれらは left と right と呼ばれ、期待する値とコードが生成した値を指定する順序は重要ではありません。このテストのアサーションを assert_eq!(4, result) と書いても、assertion `left == right` failed と表示される同じ失敗メッセージになります。
assert_ne! マクロは、渡した 2 つの値が等しくない場合に成功し、等しい場合に失敗します。このマクロは、ある値が最終的にどうなるかは分からないものの、その値が絶対に なってはいけない ものは分かっている、という場合に最も役立ちます。たとえば、入力を何らかの形で変更することが保証されている関数をテストしているものの、その変更のされ方がテストを実行する曜日によって変わる場合、最善のアサーションは、その関数の出力が入力と等しくないことを確認することかもしれません。
内部的には、assert_eq! マクロと assert_ne! マクロは、それぞれ == 演算子と != 演算子を使用しています。アサーションが失敗すると、これらのマクロは debug フォーマットを使って引数を出力します。つまり、比較される値は PartialEq トレイトと Debug トレイトを実装していなければなりません。すべてのプリミティブ型と標準ライブラリのほとんどの型は、これらのトレイトを実装しています。自分で定義する構造体や列挙型については、それらの型の等価性をアサートするために PartialEq を実装する必要があります。また、アサーションが失敗したときに値を出力するために Debug も実装する必要があります。どちらのトレイトも導出可能なトレイトであり、第 5 章のリスト 5-12 で述べたように、通常は構造体または列挙型の定義に #[derive(PartialEq, Debug)] アノテーションを追加するだけで済みます。これらやその他の導出可能なトレイトの詳細については、付録 C の 「導出可能なトレイト」 を参照してください。
カスタム失敗メッセージの追加
assert!、assert_eq!、assert_ne! マクロには、失敗メッセージとともに出力されるカスタムメッセージを、オプション引数として追加することもできます。必須引数の後に指定した引数はすべて format! マクロに渡されます(第 8 章の 「+ または format! による連結」 で説明しました)。そのため、{} プレースホルダーを含むフォーマット文字列と、そのプレースホルダーに入る値を渡すことができます。カスタムメッセージは、アサーションが何を意味しているのかを文書化するのに役立ちます。テストが失敗したとき、コードのどこに問題があるのかをより把握しやすくなります。
たとえば、人に名前であいさつする関数があり、その関数に渡した名前が出力に現れることをテストしたいとしましょう。
ファイル名: src/lib.rs
pub fn greeting(name: &str) -> String {
format!("Hello {name}!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
このプログラムの要件はまだ合意されておらず、あいさつの先頭にある Hello というテキストは変更される可能性が高いと考えています。要件が変わるたびにテストを更新したくないので、greeting 関数から返される値との完全一致を確認する代わりに、出力に入力パラメータのテキストが含まれていることだけをアサートすることにしました。
では、このコードにバグを入れて、greeting から name を除外するように変更し、デフォルトのテスト失敗がどのように見えるかを確認してみましょう。
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
このテストを実行すると、次のようになります。
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
assertion failed: result.contains("Carol")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
この結果は、アサーションが失敗したことと、そのアサーションが何行目にあるかを示しているだけです。もっと役に立つ失敗メッセージであれば、greeting 関数からの値を出力してくれるでしょう。実際に greeting 関数から得られた値をプレースホルダーに埋め込むフォーマット文字列からなる、カスタム失敗メッセージを追加してみましょう。
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{result}`"
);
}
}
これでテストを実行すると、より多くの情報を含むエラーメッセージが得られます。
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.93s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
Greeting did not contain name, value was `Hello!`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
テスト出力には実際に得られた値が表示されるので、何が起きると期待していたかではなく、実際に何が起きたのかをデバッグする助けになります。
should_panic による panic の確認
戻り値を確認することに加えて、コードがエラー条件を期待どおりに処理することを確認することも重要です。たとえば、第 9 章のリスト 9-13 で作成した Guess 型を考えてみましょう。Guess を使う他のコードは、Guess のインスタンスが 1 から 100 の値だけを含むという保証に依存しています。その範囲外の値で Guess インスタンスを作成しようとすると panic することを保証するテストを書くことができます。
これを行うには、テスト関数に should_panic 属性を追加します。関数内のコードが panic すればテストは成功し、panic しなければテストは失敗します。
リスト 11-8 は、Guess::new のエラー条件が期待どおりに発生することを確認するテストを示しています。
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
#[should_panic] 属性は、#[test] 属性の後で、それが適用されるテスト関数の前に置きます。このテストが成功したときの結果を見てみましょう。
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests guessing_game
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
よさそうです! では、値が 100 より大きい場合に new 関数が panic するという条件を削除して、コードにバグを入れてみましょう。
pub struct Guess {
value: i32,
}
// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
リスト 11-8 のテストを実行すると、失敗します。
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
note: test did not panic as expected at src/lib.rs:21:8
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
この場合、あまり役に立つメッセージは得られませんが、テスト関数を見ると、#[should_panic] が付いていることがわかります。ここで得られた失敗は、テスト関数内のコードがパニックを起こさなかったことを意味します。
should_panic を使うテストは、不正確になることがあります。should_panic テストは、期待していたものとは別の理由でテストがパニックを起こした場合でも成功してしまいます。should_panic テストをより正確にするために、should_panic 属性にオプションの expected パラメータを追加できます。テストハーネスは、失敗メッセージに指定したテキストが含まれていることを確認します。たとえば、リスト11-9にある Guess の修正後のコードを考えてみましょう。このコードでは、new 関数は値が小さすぎるか大きすぎるかに応じて異なるメッセージでパニックを起こします。
pub struct Guess {
value: i32,
}
// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
このテストは成功します。というのも、should_panic 属性の expected パラメータに指定した値が、Guess::new 関数がパニック時に出力するメッセージの部分文字列になっているからです。期待するパニックメッセージ全体を指定することもでき、その場合は Guess value must be less than or equal to 100, got 200 になります。何を指定するかは、パニックメッセージのどの程度が一意であるか、あるいは動的であるか、そしてテストをどの程度正確にしたいかによって決まります。この場合、テスト関数内のコードが else if value > 100 のケースを実行していることを保証するには、パニックメッセージの部分文字列で十分です。
expected メッセージ付きの should_panic テストが失敗するとどうなるかを見るために、もう一度コードにバグを入れてみましょう。if value < 1 と else if value > 100 のブロック本体を入れ替えます。
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
今度 should_panic テストを実行すると、失敗します。
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
thread 'tests::greater_than_100' panicked at src/lib.rs:12:13:
Guess value must be greater than or equal to 1, got 200.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: "Guess value must be greater than or equal to 1, got 200."
expected substring: "less than or equal to 100"
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
失敗メッセージは、このテストが実際に期待どおりパニックを起こしたものの、そのパニックメッセージに期待していた文字列 less than or equal to 100 が含まれていなかったことを示しています。この場合に実際に得られたパニックメッセージは Guess value must be greater than or equal to 1, got 200 でした。これで、バグがどこにあるのかを調べ始めることができます。
テストで Result<T, E> を使う
これまでのテストは、失敗するとすべてパニックを起こしていました。Result<T, E> を使うテストを書くこともできます! リスト11-1のテストを、Result<T, E> を使い、パニックする代わりに Err を返すように書き換えたものを示します。
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() -> Result<(), String> {
let result = add(2, 2);
if result == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
it_works 関数は、現在 Result<(), String> の戻り値型を持っています。関数本体では、assert_eq! マクロを呼び出す代わりに、テストが成功したときは Ok(()) を返し、失敗したときは内部に String を持つ Err を返します。
テストが Result<T, E> を返すように書くと、テスト本体で疑問符演算子を使えるようになります。これは、その内部のいずれかの操作が Err バリアントを返した場合に失敗すべきテストを書くうえで便利な方法です。
Result<T, E> を使うテストでは、#[should_panic] アノテーションは使えません。ある操作が Err バリアントを返すことを検証するには、Result<T, E> の値に対して疑問符演算子を使っては いけません。代わりに、assert!(value.is_err()) を使ってください。
テストを書くいくつかの方法がわかったので、次はテストを実行するときに何が起きているのかを見ていき、cargo test で使えるさまざまなオプションを探っていきましょう。
テストの実行方法を制御する
テストの実行方法を制御する
cargo run がコードをコンパイルして生成されたバイナリを実行するのと同様に、cargo test はコードをテストモードでコンパイルし、生成されたテストバイナリを実行します。cargo test によって生成されるバイナリのデフォルトの挙動では、すべてのテストを並列に実行し、テスト実行中に生成された出力をキャプチャします。これにより出力は表示されなくなり、テスト結果に関する出力を読みやすくできます。ただし、このデフォルトの挙動はコマンドラインオプションで変更できます。
一部のコマンドラインオプションは cargo test に渡され、別のものは生成されたテストバイナリに渡されます。これら 2 種類の引数を分けるには、まず cargo test に渡す引数を並べ、その後に区切り文字 -- を置き、続けてテストバイナリに渡す引数を指定します。cargo test --help を実行すると cargo test で使用できるオプションが表示され、cargo test -- --help を実行すると区切り文字の後で使用できるオプションが表示されます。これらのオプションは、The rustc Book の「Tests」セクション にも記載されています。
テストを並列または順番に実行する
複数のテストを実行すると、デフォルトではスレッドを使って並列に実行されるため、より早く完了し、より早くフィードバックを得られます。テストは同時に実行されるため、テスト同士が互いに依存していないこと、また現在の作業ディレクトリや環境変数のような共有環境を含む、いかなる共有状態にも依存していないことを必ず確認しなければなりません。
たとえば、それぞれのテストがディスク上に test-output.txt という名前のファイルを作成し、そのファイルに何らかのデータを書き込むコードを実行するとします。次に、各テストがそのファイルのデータを読み取り、ファイルに特定の値が含まれていることをアサートするとします。その値は各テストで異なります。テストは同時に実行されるため、あるテストがファイルを書き込んでから読み取るまでの間に、別のテストがそのファイルを上書きしてしまうかもしれません。すると後者のテストは失敗しますが、それはコードが誤っているからではなく、並列実行中にテスト同士が干渉したためです。1 つの解決策は、各テストが別々のファイルに書き込むようにすることです。別の解決策は、テストを 1 つずつ実行することです。
テストを並列実行したくない場合や、使用するスレッド数をより細かく制御したい場合は、--test-threads フラグと使用したいスレッド数をテストバイナリに渡せます。次の例を見てください:
$ cargo test -- --test-threads=1
ここではテストスレッド数を 1 に設定しており、プログラムに並列実行を行わないよう伝えています。1 つのスレッドでテストを実行すると、並列実行する場合より時間はかかりますが、共有状態があってもテスト同士が干渉しなくなります。
関数の出力を表示する
デフォルトでは、テストが成功した場合、Rust のテストライブラリは標準出力に表示されたものをすべてキャプチャします。たとえば、テスト内で println! を呼び出してそのテストが成功しても、ターミナルには println! の出力は表示されず、そのテストが成功したことを示す行だけが表示されます。テストが失敗した場合は、失敗メッセージの残りの部分とともに、標準出力に表示された内容がすべて表示されます。
例として、リスト 11-10 には、引数の値を表示して 10 を返すくだらない関数と、成功するテストおよび失敗するテストがあります。
fn prints_and_returns_10(a: i32) -> i32 {
println!("I got the value {a}");
10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq!(value, 10);
}
#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(value, 5);
}
}
これらのテストを cargo test で実行すると、次のような出力が表示されます:
$ cargo test
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
この出力のどこにも、成功するテストの実行時に表示される I got the value 4 が見当たらないことに注意してください。その出力はキャプチャされています。失敗したテストからの出力である I got the value 8 は、テスト失敗の原因も示されるテスト概要出力のセクションに現れます。
成功したテストについても表示された値を見たい場合は、--show-output を使って、成功したテストの出力も表示するよう Rust に指示できます:
$ cargo test -- --show-output
リスト 11-10 のテストを --show-output フラグ付きでもう一度実行すると、次のような出力が表示されます:
$ cargo test -- --show-output
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
successes:
---- tests::this_test_will_pass stdout ----
I got the value 4
successes:
tests::this_test_will_pass
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
名前でテストの一部を実行する
テストスイート全体の実行には、時間がかかることがあります。コードの特定の領域を作業している場合、そのコードに関連するテストだけを実行したいかもしれません。cargo test に、実行したいテストの名前を引数として渡すことで、どのテストを実行するかを選べます。
テストの一部を実行する方法を示すために、まずリスト 11-11 に示すように add_two 関数に対する 3 つのテストを作成し、どれを実行するか選びます。
pub fn add_two(a: u64) -> u64 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
#[test]
fn add_three_and_two() {
let result = add_two(3);
assert_eq!(result, 5);
}
#[test]
fn one_hundred() {
let result = add_two(100);
assert_eq!(result, 102);
}
}
先ほど見たように、引数を何も渡さずにテストを実行すると、すべてのテストが並列に実行されます:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 3 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
単一のテストを実行する
任意のテスト関数の名前を cargo test に渡すことで、そのテストだけを実行できます:
$ cargo test one_hundred
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.69s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::one_hundred ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
実行されたのは one_hundred という名前のテストだけで、ほかの 2 つのテストはその名前に一致しませんでした。テスト出力の末尾に 2 filtered out と表示されることで、実行されなかったテストがほかにもあったことがわかります。
この方法では複数のテスト名を指定できません。cargo test に与えた最初の値だけが使用されます。しかし、複数のテストを実行する方法はあります。
フィルタリングして複数のテストを実行する
テスト名の一部を指定すると、その値に一致する名前を持つテストが実行されます。たとえば、私たちのテストのうち 2 つの名前に add が含まれているので、cargo test add を実行することでその 2 つを実行できます:
$ cargo test add
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
このコマンドは、名前に add を含むすべてのテストを実行し、one_hundred という名前のテストを除外しました。また、テストが含まれるモジュールはテスト名の一部になることにも注意してください。そのため、モジュール名でフィルタリングすることで、そのモジュール内のすべてのテストを実行できます。
明示的に要求された場合を除いてテストを無視する
ときには、いくつかの特定のテストの実行に非常に時間がかかることがあるため、cargo test の大半の実行ではそれらを除外したい場合があります。実行したいすべてのテストを引数として列挙する代わりに、ここに示すように、時間のかかるテストに ignore 属性を付けて除外できます:
ファイル名: src/lib.rs
```rust,noplayground
# pub fn add(left: u64, right: u64) -> u64 {
# left + right
# }
#
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
}
#[test] の後に、除外したいテストに #[ignore] 行を追加します。
これでテストを実行すると、it_works は実行されますが、expensive_test は実行されません:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::expensive_test ... ignored
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
expensive_test 関数は ignored として表示されています。無視されたテストだけを実行したい
場合は、cargo test -- --ignored を使用できます:
$ cargo test -- --ignored
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
どのテストを実行するかを制御することで、cargo test の結果が
すばやく返ってくるようにできます。ignored テストの結果を確認するのが適切な
段階にあり、その結果を待つ時間があるなら、代わりに cargo test -- --ignored を
実行できます。無視されているかどうかにかかわらず、すべてのテストを実行したい
場合は、cargo test -- --include-ignored を実行できます。
テストの構成
テストの構成
この章の冒頭で述べたように、テストは複雑な分野であり、人によって用語や構成の仕方が異なります。Rust コミュニティでは、テストを主に 2 つのカテゴリ、すなわち単体テストと結合テストとして捉えます。単体テスト は小さく、より焦点が絞られており、一度に 1 つのモジュールを他から切り離してテストし、非公開インターフェースもテストできます。結合テスト はライブラリの完全に外部にあり、他の外部コードが行うのと同じ方法であなたのコードを使います。つまり、公開インターフェースだけを使い、1 つのテストで複数のモジュールを扱うこともあります。
ライブラリを構成する各部分が、個別にも組み合わせても期待どおりに動作していることを確認するには、両方の種類のテストを書くことが重要です。
単体テスト
単体テストの目的は、コードの各単位を残りのコードから切り離してテストし、コードのどこが期待どおりに動作し、どこが動作していないのかをすばやく特定することです。単体テストは、テスト対象のコードがある各ファイル内の src ディレクトリに置きます。慣例では、各ファイルにテスト関数を含む tests という名前のモジュールを作成し、そのモジュールに cfg(test) を付けます。
tests モジュールと #[cfg(test)]
tests モジュールに付ける #[cfg(test)] アノテーションは、Rust に対して、cargo test を実行したときだけテストコードをコンパイルして実行し、cargo build を実行したときにはそうしないよう伝えます。これにより、ライブラリだけをビルドしたいときのコンパイル時間を節約でき、テストが含まれないため、生成されるコンパイル成果物の容量も節約できます。結合テストは別のディレクトリに置かれるので、#[cfg(test)] アノテーションは必要ありません。しかし、単体テストはコードと同じファイルに置かれるため、コンパイル結果に含めるべきでないことを指定するために #[cfg(test)] を使います。
この章の最初の節で新しい adder プロジェクトを生成したとき、Cargo が次のコードを生成してくれたことを思い出してください。
ファイル名: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
自動生成された tests モジュールでは、属性 cfg は 設定 を表し、特定の設定オプションが与えられた場合にのみ後続の項目を含めるよう Rust に伝えます。この場合の設定オプションは test で、Rust がテストをコンパイルして実行するために提供しているものです。cfg 属性を使うことで、Cargo は cargo test で明示的にテストを実行した場合にのみテストコードをコンパイルします。これには、#[test] が付けられた関数に加えて、このモジュール内にある補助関数も含まれます。
非公開関数のテスト
テストコミュニティの中では、非公開関数を直接テストすべきかどうかについて議論があります。また、他の言語では非公開関数のテストが難しかったり不可能だったりすることもあります。どのようなテストの考え方に従うにせよ、Rust の可視性ルールでは非公開関数をテストできます。リスト 11-12 の、非公開関数 internal_adder を含むコードを見てみましょう。
pub fn add_two(a: u64) -> u64 {
internal_adder(a, 2)
}
fn internal_adder(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
let result = internal_adder(2, 2);
assert_eq!(result, 4);
}
}
internal_adder 関数には pub が付いていないことに注目してください。テストも単なる Rust コードであり、tests モジュールも単なる別のモジュールです。「モジュールツリー内のアイテムを参照するためのパス」 で説明したように、子モジュール内のアイテムは、その祖先モジュール内のアイテムを使えます。このテストでは、use super::* を使って tests モジュールの親に属するすべてのアイテムをスコープに取り込み、そのうえでテストから internal_adder を呼び出せます。非公開関数をテストすべきではないと考えるなら、Rust にそれを強制するものは何もありません。
結合テスト
Rust では、結合テストはライブラリの完全に外部にあります。それらは他のコードと同じ方法であなたのライブラリを使うため、呼び出せるのはライブラリの公開 API の一部である関数だけです。その目的は、ライブラリの多くの部分が正しく連携して動作するかどうかをテストすることです。それぞれ単独では正しく動作するコードの単位でも、統合すると問題が起こることがあります。そのため、統合されたコードに対するテストカバレッジも重要です。結合テストを作成するには、まず tests ディレクトリが必要です。
tests ディレクトリ
プロジェクトディレクトリの最上位で、src の隣に tests ディレクトリを作成します。Cargo はこのディレクトリ内で結合テストファイルを探すようになっています。その後は必要なだけテストファイルを作成でき、Cargo はそれぞれのファイルを個別のクレートとしてコンパイルします。
結合テストを作成してみましょう。引き続き src/lib.rs ファイルにあるリスト 11-12 のコードを使って、tests ディレクトリを作成し、tests/integration_test.rs という新しいファイルを作成します。ディレクトリ構造は次のようになります。
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
tests/integration_test.rs ファイルにリスト 11-13 のコードを入力してください。
use adder::add_two;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
tests ディレクトリ内の各ファイルは別々のクレートなので、各テストクレートのスコープに自分たちのライブラリを取り込む必要があります。そのため、単体テストでは不要だった use adder::add_two; をコードの先頭に追加します。
tests/integration_test.rs 内のコードに #[cfg(test)] を付ける必要はありません。Cargo は tests ディレクトリを特別扱いし、このディレクトリ内のファイルは cargo test を実行したときにだけコンパイルします。では、ここで cargo test を実行しましょう。
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
出力は 3 つのセクションに分かれており、単体テスト、結合テスト、ドキュメントテストが含まれています。あるセクション内のテストが 1 つでも失敗すると、それ以降のセクションは実行されない点に注意してください。たとえば、単体テストが失敗すると、結合テストやドキュメントテストの出力は表示されません。これらのテストは、すべての単体テストが成功した場合にのみ実行されるからです。
単体テストの最初のセクションは、これまで見てきたものと同じです。各単体テストごとに 1 行(リスト 11-12 で追加した internal という名前のものが 1 つあります)があり、その後に単体テストの要約行が続きます。
結合テストのセクションは、Running tests/integration_test.rs という行で始まります。次に、その結合テスト内の各テスト関数について 1 行ずつ表示され、Doc-tests adder セクションが始まる直前に、結合テストの結果の要約行が表示されます。
各結合テストファイルにはそれぞれ専用のセクションがあるので、tests ディレクトリにさらにファイルを追加すれば、結合テストのセクションも増えます。
特定の統合テスト関数は、cargo test の引数としてテスト関数名を指定することで、引き続き実行できます。特定の統合テストファイル内のすべてのテストを実行するには、cargo test の --test 引数の後にファイル名を指定します。
$ cargo test --test integration_test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
このコマンドは、tests/integration_test.rs ファイル内のテストだけを実行します。
統合テスト内のサブモジュール
統合テストをさらに追加していくと、それらを整理しやすくするために tests ディレクトリ内にさらにファイルを作りたくなるかもしれません。たとえば、テスト関数を、テスト対象の機能ごとにグループ化できます。前に述べたように、tests ディレクトリ内の各ファイルは、それぞれ独立した別個のクレートとしてコンパイルされます。これは、個別のスコープを作成して、エンドユーザーがあなたのクレートを使う方法をより厳密に模倣するうえで便利です。しかしこれは、tests ディレクトリ内のファイルが、src 内のファイルと同じ振る舞いを共有しないことも意味します。これは、コードをモジュールとファイルに分割する方法について第7章で学んだとおりです。
tests ディレクトリ内のファイルのこの違いは、複数の統合テストファイルで使うヘルパー関数群があり、それらを共通モジュールに抽出するために、第7章の「モジュールを別々のファイルに分割する」節の手順に従おうとしたときに、もっとも顕著に現れます。たとえば、tests/common.rs を作成して、その中に setup という名前の関数を置いた場合、複数のテストファイル内の複数のテスト関数から呼び出したいコードを setup に追加できます。
ファイル名: tests/common.rs
pub fn setup() {
// setup code specific to your library's tests would go here
}
再びテストを実行すると、common.rs ファイルにはテスト関数がまったく含まれておらず、また setup 関数をどこからも呼び出していないにもかかわらず、テスト出力に common.rs ファイル用の新しいセクションが表示されます。
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.89s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
テスト結果に common が現れ、その項目に running 0 tests と表示されるのは、私たちが望んでいたことではありません。私たちは単に、ほかの統合テストファイルとコードを共有したかっただけです。common がテスト出力に現れないようにするため、tests/common.rs を作成する代わりに、tests/common/mod.rs を作成します。これでプロジェクトディレクトリは次のようになります。
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
これは、Rust が理解する古い命名規約であり、第7章の「別のファイルパス」で触れたものです。このようにファイルに名前を付けることで、common モジュールを統合テストファイルとして扱わないよう Rust に伝えられます。setup 関数のコードを tests/common/mod.rs に移し、tests/common.rs ファイルを削除すると、そのセクションはテスト出力に表示されなくなります。tests ディレクトリのサブディレクトリ内にあるファイルは、独立したクレートとしてコンパイルされず、テスト出力内にセクションも作られません。
tests/common/mod.rs を作成した後は、どの統合テストファイルからでもそれをモジュールとして使えます。次は、tests/integration_test.rs の it_adds_two テストから setup 関数を呼び出す例です。
ファイル名: tests/integration_test.rs
use adder::add_two;
mod common;
#[test]
fn it_adds_two() {
common::setup();
let result = add_two(2);
assert_eq!(result, 4);
}
mod common; 宣言は、リスト7-21で示したモジュール宣言と同じであることに注意してください。そしてテスト関数内では、common::setup() 関数を呼び出せます。
バイナリクレートの統合テスト
プロジェクトが src/main.rs ファイルしか含まず、src/lib.rs ファイルを持たないバイナリクレートである場合、tests ディレクトリに統合テストを作成して、src/main.rs ファイルで定義された関数を use 文でスコープに持ち込むことはできません。他のクレートが利用できる関数を公開するのはライブラリクレートだけであり、バイナリクレートは単独で実行されることを意図しているためです。
これは、バイナリを提供する Rust プロジェクトが、src/lib.rs ファイル内にあるロジックを呼び出す、単純な src/main.rs ファイルを持つ構造になっている理由の1つです。その構造を使えば、統合テストは use によってライブラリクレートをテストし、重要な機能を利用可能にできます。重要な機能が正しく動作すれば、src/main.rs ファイル内の少量のコードも同様に動作します。そして、その少量のコードはテストする必要がありません。
まとめ
Rust のテスト機能は、コードがどのように動作すべきかを指定する手段を提供し、変更を加えても期待どおりに動作し続けることを保証できるようにします。ユニットテストはライブラリのさまざまな部分を個別に検証し、非公開の実装詳細もテストできます。統合テストはライブラリの多くの部分が正しく連携して動作することを確認し、外部コードが利用するのと同じ方法でライブラリの公開 API を使ってコードをテストします。Rust の型システムと所有権ルールは、ある種のバグを防ぐのに役立ちますが、それでも、コードがどのように振る舞うべきかに関わるロジックバグを減らすためにテストは重要です。
この章とこれまでの章で学んだ知識を組み合わせて、プロジェクトに取り組んでみましょう。
I/Oプロジェクト: コマンドラインプログラムを構築する
この章では、これまでに学んだ多くのスキルを振り返るとともに、 標準ライブラリのいくつかの追加機能を探ります。ファイルおよび コマンドラインの入出力とやり取りするコマンドラインツールを構築して、 これまでに身につけたRustの概念をいくつか実践していきます。
Rustの速度、安全性、単一バイナリを出力できること、そして
クロスプラットフォーム対応により、Rustはコマンドラインツールを作成する
うえで理想的な言語です。そこでこのプロジェクトでは、古典的な
コマンドライン検索ツール grep(globally search a regular
expression and print)の独自版を作ります。もっとも単純な
ユースケースでは、grep は指定されたファイルから指定された文字列を
検索します。そのために、grep は引数としてファイルパスと文字列を
受け取ります。次に、ファイルを読み込み、その文字列引数を含む行を
見つけ、それらの行を出力します。
その過程で、多くのコマンドラインツールが利用しているターミナル機能を、
私たちのコマンドラインツールでも使う方法を示します。環境変数の値を
読み取って、ユーザーがツールの動作を設定できるようにします。また、
エラーメッセージを標準出力 (stdout) ではなく標準エラーストリーム
(stderr) に出力することで、たとえば正常な出力をファイルに
リダイレクトしつつ、エラーメッセージは引き続き画面上で確認できるように
します。
Rustコミュニティの一員であるAndrew Gallantは、ripgrep と呼ばれる、
高機能で非常に高速な grep の実装をすでに作成しています。これに
比べると、私たちの版はかなり単純なものになりますが、この章では
ripgrep のような実世界のプロジェクトを理解するために必要な背景知識を
いくつか身につけられます。
私たちの grep プロジェクトでは、これまでに学んださまざまな概念を
組み合わせます。
また、クロージャ、イテレータ、トレイトオブジェクトについても簡単に 紹介します。これらについては、第13章 と 第18章 で詳しく扱います。
コマンドライン引数を受け取る
コマンドライン引数を受け取る
いつものように、cargo new を使って新しいプロジェクトを作成しましょう。すでにシステム上にあるかもしれない grep ツールと区別するために、プロジェクト名は
minigrep にします。
$ cargo new minigrep
Created binary (application) `minigrep` project
$ cd minigrep
最初のタスクは、minigrep が 2 つのコマンドライン引数、つまりファイルパスと検索対象の文字列を受け取れるようにすることです。つまり、cargo run に続けて、後続の引数が cargo 用ではなく自分たちのプログラム用であることを示す 2 つのハイフン、検索する文字列、そして検索対象ファイルへのパスを指定して、次のようにプログラムを実行できるようにしたいのです。
$ cargo run -- searchstring example-filename.txt
現時点では、cargo new によって生成されたプログラムは、渡された引数を処理できません。crates.io には、コマンドライン引数を受け取るプログラムを書くのを助けてくれる既存のライブラリもありますが、いまはこの概念を学んでいるところなので、この機能は自分たちで実装してみましょう。
引数の値を読み取る
minigrep が渡されたコマンドライン引数の値を読み取れるようにするには、Rust の標準ライブラリが提供する std::env::args 関数が必要です。この関数は、minigrep に渡されたコマンドライン引数のイテレータを返します。イテレータについては 第 13 章 で詳しく扱います。今のところは、イテレータについて 2 つの点だけ知っておけば十分です。イテレータは一連の値を生成すること、そしてイテレータに対して collect メソッドを呼び出すことで、生成されたすべての要素を含むベクタのようなコレクションに変換できることです。
リスト 12-1 のコードでは、minigrep プログラムが渡された任意のコマンドライン引数を読み取り、その値をベクタに集められるようにしています。
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
まず、use 文で std::env モジュールをスコープに導入し、その args 関数を使えるようにします。std::env::args 関数は 2 段階のモジュールの中にネストされていることに注目してください。第 7 章 で説明したように、目的の関数が複数のモジュールにまたがってネストされている場合、私たちは関数そのものではなく親モジュールをスコープに導入することを選んでいます。そうすることで、std::env からほかの関数も簡単に使えます。また、use std::env::args を追加してから単に args として関数を呼び出すよりも曖昧さが少なくなります。というのも、args は現在のモジュール内で定義された関数と簡単に取り違えられる可能性があるからです。
args関数と無効な Unicode
std::env::argsは、いずれかの引数に無効な Unicode が含まれていると panic することに注意してください。プログラムで無効な Unicode を含む引数を受け取る必要がある場合は、代わりにstd::env::args_osを使ってください。この関数は、String値ではなくOsString値を生成するイテレータを返します。ここでは、簡潔さを優先してstd::env::argsを使うことにしました。というのも、OsStringの値はプラットフォームごとに異なり、String値よりも扱いが複雑だからです。
main の最初の行で env::args を呼び出し、そのイテレータを、生成されるすべての値を含むベクタへ変換するために、すぐに collect を使っています。collect 関数は多くの種類のコレクションを作るために使えるので、文字列のベクタが欲しいことを指定するために、args の型を明示的に注釈しています。Rust では型注釈が必要になることはごくまれですが、collect は、Rust がどの種類のコレクションを望んでいるのか推論できないため、しばしば型注釈が必要になる関数の 1 つです。
最後に、デバッグマクロを使ってベクタを表示します。まずは引数なしでコードを実行し、その後で 2 つの引数を付けて実行してみましょう。
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/minigrep`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
]
$ cargo run -- needle haystack
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.57s
Running `target/debug/minigrep needle haystack`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
"needle",
"haystack",
]
ベクタの最初の値が "target/debug/minigrep" になっていることに注目してください。これは私たちのバイナリの名前です。これは C における引数リストの振る舞いと一致しており、プログラムは実行時にどの名前で呼び出されたかを利用できます。メッセージに表示したい場合や、どのコマンドラインエイリアスで起動されたかに応じてプログラムの振る舞いを変えたい場合には、プログラム名にアクセスできると便利なことがよくあります。しかしこの章では、それを無視して、必要な 2 つの引数だけを保存します。
引数の値を変数に保存する
現在、このプログラムはコマンドライン引数として指定された値にアクセスできます。次は、その 2 つの引数の値を変数に保存して、プログラムの残りの部分全体でその値を使えるようにする必要があります。これをリスト 12-2 で行います。
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("Searching for {query}");
println!("In file {file_path}");
}
ベクタを表示したときに見たように、プログラム名が args[0] にあるベクタの最初の値を占めるため、引数はインデックス 1 から使い始めます。minigrep が受け取る最初の引数は検索したい文字列なので、最初の引数への参照を変数 query に入れます。2 番目の引数はファイルパスなので、2 番目の引数への参照を変数 file_path に入れます。
コードが意図どおりに動作していることを確認するために、これらの変数の値を一時的に表示します。このプログラムを、今度は test と sample.txt という引数付きで再び実行してみましょう。
$ cargo run -- test sample.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt
すばらしいことに、このプログラムは動いています。必要な引数の値は、正しい変数に保存されています。後で、ユーザーが引数をまったく指定しなかった場合のような、起こりうる誤った状況に対処するためのエラーハンドリングを追加します。今はその状況は無視して、代わりにファイル読み取り機能を追加する作業に進みましょう。
ファイルを読み込む
ファイルを読む
次に、file_path
引数で指定されたファイルを読む機能を追加します。まず、これをテストするためのサンプルファイルが必要です。複数行にわたる少量のテキストと、いくつかの繰り返し語を含むファイルを使います。リスト12-3
には、これにちょうどよい Emily Dickinson の詩があります! プロジェクトのルートレベルに
poem.txt というファイルを作成し、詩「私は何者でもない!あなたは誰?」
を入力してください。
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
テキストを配置したら、src/main.rs を編集し、ファイルを読むコードを追加します。これは リスト12-4に示されています。
use std::env;
use std::fs;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("Searching for {query}");
println!("In file {file_path}");
let contents = fs::read_to_string(file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
まず、use
文を使って標準ライブラリの関連部分を取り込みます。ファイルを扱うには std::fs が必要です。
main では、新しい文 fs::read_to_string が file_path を受け取り、
そのファイルを開き、ファイルの内容を含む std::io::Result<String> 型の値を返します。
その後、ファイルが読まれたあとに contents の値を出力する一時的な println!
文を再び追加し、ここまでプログラムが正しく動いていることを確認できるようにします。
このコードを、最初のコマンドライン引数には任意の文字列を(まだ検索部分を実装していないため)、 2番目の引数には poem.txt ファイルを指定して実行してみましょう。
$ cargo run -- the poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
すばらしいです! コードはファイルの内容を読み取り、それを出力しました。しかし、このコード
にはいくつかの欠点があります。現時点では、main 関数は複数の責務を
持っています。一般に、各関数がただ1つの考えだけを担当しているほうが、関数はより明確で保守しやすくなります。もう1つの問題は、エラー処理が
できる限りうまく行われていないことです。プログラムはまだ小さいので、これらの
欠点は大きな問題ではありません。しかし、プログラムが大きくなるにつれて、それらをきれいに修正することは難しくなります。プログラムを開発するときは、早い段階でリファクタリングを始めるのがよい習慣です。というのも、少ない量のコードのほうがリファクタリングしやすいからです。次はそれを行います。
モジュール性とエラー処理を改善するためにリファクタリングする
モジュール性とエラーハンドリングを改善するためのリファクタリング
プログラムを改善するために、プログラムの構造と潜在的なエラーの
扱い方に関する4つの問題を修正します。まず、現在の main
関数は2つのタスクを実行しています。引数を解析することと、ファイルを
読み込むことです。プログラムが大きくなるにつれて、main
関数が扱う個別のタスクの数は増えていきます。関数の責務が増えるほど、
その関数を把握するのは難しくなり、テストもしにくくなり、どこか一部を
壊さずに変更することも難しくなります。各関数が1つのタスクだけを担当
するように、機能を分離するのが最善です。
この問題は2つ目の問題にも関係しています。query と file_path
はプログラムの設定変数ですが、contents のような変数は
プログラムのロジックを実行するために使われます。main が長くなるほど、
スコープに持ち込む必要のある変数は増えます。スコープ内の変数が増える
ほど、それぞれの目的を追跡するのは難しくなります。設定変数は1つの
構造体にまとめて、その目的を明確にするのが最善です。
3つ目の問題は、ファイルの読み込みに失敗したときにエラーメッセージを
表示するために expect を使っていますが、そのエラーメッセージが
単に Should have been able to read the file と表示されるだけである
ことです。ファイルの読み込みはさまざまな理由で失敗する可能性があり
ます。たとえば、ファイルが存在しないかもしれませんし、開く権限が
ないかもしれません。現状では、状況にかかわらず、すべてに対して同じ
エラーメッセージを表示することになり、ユーザーに何の情報も与えられ
ません!
4つ目は、エラー処理に expect を使っているため、ユーザーが十分な数の
引数を指定せずにプログラムを実行すると、問題を明確に説明しない
index out of bounds エラーを Rust から受け取ることです。
エラーハンドリングのロジックを変更する必要が生じたときに、将来の
保守担当者が参照すべきコードの場所が1つだけになるよう、すべての
エラーハンドリング用コードが1か所にあるのが最善でしょう。
エラーハンドリング用コードを1か所にまとめることで、エンドユーザーに
とって意味のあるメッセージを確実に表示できるようにもなります。
プロジェクトをリファクタリングして、これら4つの問題に対処しましょう。
バイナリプロジェクトで責務を分離する
複数のタスクの責務を main 関数に割り当ててしまうという構成上の問題は、
多くのバイナリプロジェクトに共通しています。そのため、多くの Rust
プログラマーは、main 関数が大きくなり始めたら、バイナリプログラムの
別々の責務を分割するのが有用だと考えています。このプロセスは次の
手順からなります。
- プログラムを main.rs ファイルと lib.rs ファイルに分け、プログラム のロジックを lib.rs に移す。
- コマンドライン解析ロジックが小さいうちは、それを
main関数に残しておける。 - コマンドライン解析ロジックが複雑になり始めたら、それを
main関数から他の関数や型へ切り出す。
このプロセスの後に main 関数に残る責務は、次のものに限定するべきです。
- 引数の値を使ってコマンドライン解析ロジックを呼び出すこと
- その他の設定を行うこと
- lib.rs の
run関数を呼び出すこと runがエラーを返した場合にそのエラーを処理すること
このパターンは責務の分離に関するものです。main.rs はプログラムの実行を
担当し、lib.rs は目の前のタスクのロジックをすべて担当します。
main 関数は直接テストできないため、この構造にすると、プログラムの
ロジックを main 関数の外に移すことで、そのすべてをテストできるように
なります。main 関数に残るコードは、読んで正しさを確認できるほど
小さくなります。このプロセスに従って、プログラムを作り直してみましょう。
引数パーサーを抽出する
引数を解析する機能を、main が呼び出す関数に抽出します。
リスト12-5は、新しい関数 parse_config を呼び出す main
関数の新しい冒頭部分を示しています。この関数は src/main.rs に
定義します。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let (query, file_path) = parse_config(&args);
// --snip--
println!("Searching for {query}");
println!("In file {file_path}");
let contents = fs::read_to_string(file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let file_path = &args[2];
(query, file_path)
}
コマンドライン引数をベクタに収集している点は変わりませんが、
main 関数の中でインデックス1の引数値を変数 query に、
インデックス2の引数値を変数 file_path に代入する代わりに、
ベクタ全体を parse_config 関数に渡します。すると parse_config
関数が、どの引数をどの変数に入れるかを決定するロジックを保持し、
その値を main に返します。main の中で query と file_path
という変数は引き続き作成しますが、コマンドライン引数と変数を
どのように対応付けるかを決定する責務は、もはや main にはあり
ません。
この作り直しは、小さなプログラムに対してはやりすぎに見えるかも しれませんが、私たちは小さな段階的ステップでリファクタリングを 進めています。この変更を行ったら、引数の解析が引き続き機能する ことを確認するために、もう一度プログラムを実行してください。 問題が起きたときにその原因を特定しやすくするため、進捗をこまめに 確認するのは良いことです。
設定値をグループ化する
parse_config 関数をさらに改善するために、もう1つ小さなステップを
踏むことができます。現時点ではタプルを返していますが、その直後に
そのタプルを再び個々の部分に分解しています。これは、まだ適切な
抽象化ができていないのかもしれないという兆候です。
改善の余地があることを示すもう1つの指標は、parse_config の
config という部分です。これは、返している2つの値が関連しており、
どちらも1つの設定値の一部であることを示唆しています。現在は、
2つの値をタプルにまとめる以外に、この意味をデータ構造の中で表現
できていません。そこで代わりに、2つの値を1つの struct に入れ、
各 struct フィールドに意味のある名前を付けます。そうすることで、
将来このコードを保守する人が、異なる値どうしがどのように関係して
いるのか、そしてそれぞれの目的が何なのかを理解しやすくなります。
リスト12-6は、parse_config 関数に対する改善を示しています。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
// --snip--
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
query と file_path という名前のフィールドを持つように定義された
Config という名前の struct を追加しました。parse_config の
シグネチャは、返り値が Config 値であることを示すようになって
います。以前は parse_config の本体で、args 内の String
値を参照する文字列スライスを返していましたが、今では Config に
所有権を持つ String 値を含めるように定義しています。main の
args 変数は引数値の所有者であり、parse_config 関数にはそれらを
借用させているだけなので、Config が args 内の値の所有権を取得
しようとすると、Rust の借用規則に違反することになります。
String データを扱う方法はいくつかあります。最も簡単なのは、
やや非効率ではあるものの、値に対して clone メソッドを呼び出す方法です。
これにより、Config インスタンスが所有するためのデータの完全なコピーが作られ、
文字列データへの参照を保持するよりも多くの時間とメモリを消費します。
しかし、データをクローンすると参照のライフタイムを管理する必要がなくなるため、
コードも非常に単純になります。この状況では、
単純さを得るために少しの性能を犠牲にするのは、価値のあるトレードオフです。
cloneを使うことのトレードオフ多くの Rustacean には、実行時コストのために所有権の問題を解決する目的で
cloneを使うのを避ける傾向があります。 第13章では、この種の状況でより効率的な方法を 使う方法を学びます。しかし今のところは、先に進むためにいくつかの文字列を コピーしても問題ありません。これらのコピーは一度しか行われず、ファイルパスと クエリ文字列も非常に小さいからです。最初の実装段階でコードを過度に最適化 しようとするよりも、少し非効率でも動作するプログラムを持つほうがよいのです。 Rust に慣れてくると、最も効率的な解決策から始めるのも簡単になりますが、 今の段階ではcloneを呼び出すことはまったく問題ありません。
main を更新し、parse_config が返す Config のインスタンスを config
という名前の変数に格納するようにしました。また、以前は別々の query 変数と
file_path 変数を使っていたコードも更新し、代わりに Config 構造体の
フィールドを使うようにしました。
これで、query と file_path が関係しており、その目的がプログラムの
動作方法を設定することであると、コードがより明確に伝えるようになりました。
これらの値を使うコードは、それらを config インスタンス内の、
その目的に応じた名前のフィールドに見つければよいことが分かります。
Config のコンストラクタを作成する
ここまでは、コマンドライン引数の解析を担当するロジックを main から切り出し、
parse_config 関数に配置してきました。そうすることで、query と file_path
の値が関係していることが見えてきました。そして、その関係はコードで表現される
べきです。そこで Config 構造体を追加し、query と file_path の
関連した目的に名前を付けるとともに、parse_config 関数から値の名前を構造体の
フィールド名として返せるようにしました。
では、parse_config 関数の目的が Config インスタンスを作成することだと
分かったので、parse_config を単なる関数から、Config 構造体に関連付けられた
new という名前の関数に変更できます。この変更により、コードはより
イディオマティックになります。標準ライブラリ内の String のような型の
インスタンスは、String::new を呼び出すことで作成できます。同様に、
parse_config を Config に関連付けられた new 関数に変更すれば、
Config::new を呼び出して Config のインスタンスを作成できるようになります。
リスト 12-7 は、必要な変更を示しています。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
// --snip--
}
// --snip--
struct Config {
query: String,
file_path: String,
}
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
main では、parse_config を呼び出していた箇所を更新し、代わりに
Config::new を呼び出すようにしました。また、parse_config の名前を
new に変更し、impl ブロックの中へ移動しました。これにより、new
関数は Config に関連付けられます。このコードが動作することを確認するために、
もう一度コンパイルしてみてください。
エラーハンドリングを修正する
次は、エラーハンドリングの修正に取り組みます。args ベクタのインデックス 1 または
インデックス 2 の値にアクセスしようとすると、ベクタに 3 つ未満の要素しか
含まれていない場合にプログラムがパニックすることを思い出してください。
引数なしでプログラムを実行してみると、次のようになります。
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
index out of bounds: the len is 1 but the index is 1 という行は、
プログラマ向けのエラーメッセージです。これは、エンドユーザーが代わりに何を
すべきかを理解する助けにはなりません。今からこれを修正しましょう。
エラーメッセージを改善する
リスト 12-8 では、インデックス 1 とインデックス 2 にアクセスする前に、
スライスが十分な長さを持っていることを確認するチェックを new 関数に追加します。
スライスの長さが足りない場合、プログラムはパニックし、よりよい
エラーメッセージを表示します。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
// --snip--
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("not enough arguments");
}
// --snip--
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
このコードは、リスト 9-13 で書いた Guess::new 関数
に似ています。そこでは、value 引数が有効な値の範囲外だったときに
panic! を呼び出しました。ここでは値の範囲を確認する代わりに、args
の長さが少なくとも 3 であることを確認しており、関数の残りの部分は
この条件が満たされているという前提で処理できます。args の要素数が 3 未満なら、
この条件は true になり、panic! マクロを呼び出してプログラムを即座に
終了します。
new にこの数行のコードを追加したので、引数なしでプログラムをもう一度実行し、
エラーが今どのように見えるかを確認してみましょう。
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:26:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
この出力は改善されています。これで、妥当なエラーメッセージが得られるように
なりました。しかし、ユーザーには見せたくない余計な情報も含まれています。
おそらく、リスト 9-13 で使った手法はここで使うには最適ではありません。
第9章で説明したように、
panic! の呼び出しは使用上の問題よりもプログラミング上の問題に
適しています。代わりに、第9章で学んだもう 1 つの手法、つまり成功または
エラーのいずれかを示す Result を返すこと
を使います。
panic! を呼び出す代わりに Result を返す
代わりに Result 値を返すことで、成功した場合には Config インスタンスを、
エラーの場合には問題の内容を含められます。また、関数名を new から build
に変更します。というのも、多くのプログラマは new 関数は決して失敗しないと
期待するからです。Config::build が main とやり取りするときには、
Result 型を使って問題が発生したことを知らせられます。そうすれば、
main を変更して、Err バリアントを、panic! の呼び出しで生じる
thread 'main' や RUST_BACKTRACE に関する前後の文言なしの、
より実用的なエラーへと変換できます。
リスト 12-9 は、いま Config::build と呼んでいる関数の戻り値と、
Result を返すために必要な関数本体への変更を示しています。これについては、
次のリストで main も更新するまでコンパイルできないことに注意してください。
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
私たちの build 関数は、成功時には Config インスタンスを、エラー時には文字列リテラルを含む Result を返します。エラー値は常に
'static ライフタイムを持つ文字列リテラルになります。
この関数の本体には 2 つの変更を加えました。ユーザーが十分な数の引数を渡さなかったときに panic!
を呼び出す代わりに Err 値を返すようにし、さらに Config の戻り値を Ok で包みました。これらの変更により、この関数は新しい型シグネチャに適合します。
Config::build から Err 値を返すことで、main 関数は build 関数から返された Result 値を
処理し、エラー時によりクリーンにプロセスを終了できるようになります。
Config::build を呼び出してエラーを処理する
エラー時を処理してユーザーフレンドリーなメッセージを表示するには、
リスト 12-10 に示すように、Config::build から返される Result を処理するよう
main を更新する必要があります。また、コマンドラインツールをゼロ以外のエラーコードで終了させる責務を
panic! から切り離し、代わりに自前で実装します。ゼロ以外の終了ステータスは、私たちのプログラムを呼び出した
プロセスに対して、そのプログラムがエラー状態で終了したことを知らせる慣例です。
use std::env;
use std::fs;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
このリストでは、まだ詳しく扱っていないメソッド
unwrap_or_else を使っています。これは標準ライブラリによって Result<T, E> に定義されています。
unwrap_or_else を使うと、panic! ではない独自のエラー処理を定義できます。
Result が Ok 値であれば、このメソッドの振る舞いは unwrap と似ています。つまり、Ok が包んでいる内部の値を返します。しかし、その
値が Err 値であれば、このメソッドはクロージャ内のコードを呼び出します。クロージャとは、私たちが定義して
unwrap_or_else に引数として渡す無名関数です。
クロージャについては 第13章 でさらに詳しく扱います。今のところは、
unwrap_or_else が Err の内部の値を渡す、ということだけ知っていれば十分です。この場合、その値は
リスト 12-9 で追加した静的文字列 "not enough arguments" であり、縦棒の間に現れる引数 err を通じて
クロージャに渡されます。すると、クロージャ内のコードは実行時にその
err 値を使うことができます。
標準ライブラリから process をスコープに導入するために、新しい use 行を追加しました。
エラー時に実行されるクロージャ内のコードは 2 行だけです。err 値を表示し、その後で
process::exit を呼び出します。process::exit 関数はプログラムを即座に停止し、
終了ステータスコードとして渡された数値を返します。これは
リスト 12-8 で使った panic! ベースの処理に似ていますが、余分な出力がすべて表示されることはなくなります。
試してみましょう。
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments
すばらしいですね。この出力は、ユーザーにとってずっと親切です。
main からロジックを抽出する
設定のパースのリファクタリングが終わったので、次は
プログラムのロジックに移りましょう。「バイナリプロジェクトで関心を分離する」 で述べたとおり、
設定の準備やエラー処理に関わらない、現在 main 関数にあるすべてのロジックを保持する
run という名前の関数を抽出します。これが終わると、main 関数は簡潔で、目で確認するだけでも検証しやすくなり、
それ以外のすべてのロジックに対してテストを書けるようになります。
リスト 12-11 は、run
関数を抽出する小さな段階的改善を示しています。
use std::env;
use std::fs;
use std::process;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
run(config);
}
fn run(config: Config) {
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
// --snip--
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
run 関数には、ファイルの読み込みから始まる、main に残っていたすべてのロジックが
入るようになりました。run 関数は Config インスタンスを
引数として受け取ります。
run からエラーを返す
残りのプログラムロジックを run 関数に分離したので、
リスト 12-9 の Config::build と同じように、エラー処理を改善できます。
expect を呼び出してプログラムを panic させるのではなく、何か問題が起きたときに run
関数が Result<T, E> を返すようにします。こうすることで、
ユーザーフレンドリーな形でのエラー処理のロジックを、さらに main に集約できます。
リスト 12-12 は、run のシグネチャと本体に必要な変更を示しています。
use std::env;
use std::fs;
use std::process;
use std::error::Error;
// --snip--
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
run(config);
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
ここでは 3 つの重要な変更を行いました。まず、run 関数の戻り値の型を
Result<(), Box<dyn Error>> に変更しました。この関数は以前は
ユニット型 () を返しており、Ok の場合に返す値としてはそれを維持しています。
エラー型には、トレイトオブジェクト Box<dyn Error> を使いました(そして
先頭の use 文で std::error::Error をスコープに導入しました)。トレイトオブジェクトについては
第18章 で扱います。今のところは、Box<dyn Error> が
その関数が Error トレイトを実装する型を返すことを意味しつつも、
戻り値がどの具体的な型になるかまでは指定する必要がない、ということだけ知っていれば十分です。
これにより、異なるエラー時に異なる型のエラー値を返せる
柔軟性が得られます。dyn キーワードは dynamic の略です。
次に、第9章 で説明したように、
expect の呼び出しを削除して ? 演算子を使うようにしました。
エラー時に panic! する代わりに、? は現在の関数からエラー値を返し、
呼び出し元に処理を委ねます。
3 つ目に、run 関数は成功時に Ok 値を返すようになりました。
シグネチャで run 関数の成功型を () と宣言しているため、
ユニット型の値を Ok 値で包む必要があります。この
Ok(()) という構文は、最初は少し奇妙に見えるかもしれません。しかし、このように () を使うのは、
副作用のためだけに run を呼び出していることを示す慣用的な方法です。
必要な値を返しているわけではありません。
このコードを実行すると、コンパイルは通りますが警告が表示されます。
$ cargo run -- the poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
--> src/main.rs:19:5
|
19 | run(config);
| ^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
19 | let _ = run(config);
| +++++++
warning: `minigrep` (bin "minigrep") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
Rust は、私たちのコードが Result 値を無視しており、その Result 値は
エラーが発生したことを示しているかもしれない、と教えてくれています。しかし、実際に
エラーがあったかどうかを確認しておらず、コンパイラはここに何らかの
エラー処理コードを書くつもりだったはずだと警告してくれます。では、この問題を今すぐ修正しましょう。
main で run から返されるエラーを処理する
エラーを確認し、リスト 12-10 の Config::build で使用したものと似た手法で処理しますが、少し違いがあります。
ファイル名: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
run が Err 値を返したかどうかを確認し、返した場合に process::exit(1) を呼び出すために、unwrap_or_else ではなく if let を使います。run 関数は、Config::build が Config インスタンスを返すのと同じ形で unwrap したい値を返しません。run は成功時には () を返すため、私たちが気にするのはエラーを検出することだけです。したがって、アンラップした値を返す unwrap_or_else は必要ありません。その値も () にすぎないからです。
if let と unwrap_or_else の本体は、どちらの場合も同じです。エラーを表示して終了します。
コードをライブラリクレートに分割する
ここまでで minigrep プロジェクトはかなり良い状態になっています。次は src/main.rs ファイルを分割し、いくつかのコードを src/lib.rs ファイルに移します。そうすることで、コードをテストできるようになり、src/main.rs ファイルの責務も少なくできます。
テキスト検索を担当するコードは src/main.rs ではなく src/lib.rs に定義しましょう。そうすることで、私たち自身(あるいは minigrep ライブラリを使う他の誰か)が、minigrep バイナリよりも多くのコンテキストから検索関数を呼び出せるようになります。
まず、リスト 12-13 に示すように、src/lib.rs で search 関数のシグネチャを定義し、本体では unimplemented! マクロを呼び出すようにします。シグネチャについては、実装を埋めるときにより詳しく説明します。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
unimplemented!();
}
関数定義に pub キーワードを使って、search をライブラリクレートの公開 API の一部として指定しました。これで、バイナリクレートから利用でき、テストもできるライブラリクレートができました。
次に、src/lib.rs で定義したコードを src/main.rs のバイナリクレートのスコープに持ち込み、それを呼び出す必要があります。これはリスト 12-14 に示されています。
use std::env;
use std::error::Error;
use std::fs;
use std::process;
// --snip--
use minigrep::search;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
// --snip--
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
for line in search(&config.query, &contents) {
println!("{line}");
}
Ok(())
}
ライブラリクレートから search 関数をバイナリクレートのスコープに持ち込むために、use minigrep::search という行を追加します。次に run 関数では、ファイルの内容を表示する代わりに search 関数を呼び出し、config.query の値と contents を引数として渡します。そして run は、for ループを使って、クエリに一致した search の返り値の各行を表示します。また、エラーが発生しない限りプログラムが検索結果だけを表示するように、クエリとファイルパスを表示していた main 関数内の println! 呼び出しを削除するのにも良いタイミングです。
検索関数は、表示が行われる前に、返り値となるベクタにすべての結果を集めることに注意してください。この実装は、大きなファイルを検索する場合に結果の表示が遅くなる可能性があります。結果は見つかるたびに表示されるのではないからです。これをイテレータを使って改善する方法については、第 13 章で説明します。
ふう! かなりの作業でしたが、将来の成功に向けた土台を整えることができました。これでエラー処理はずっと簡単になり、コードもよりモジュール化されました。これ以降の作業のほとんどは src/lib.rs で行うことになります。
この新たに得られたモジュール性を活かして、古いコードでは難しかったが新しいコードなら簡単なことをしてみましょう。テストを書きます!
テスト駆動開発で機能を追加する
テスト駆動開発で機能を追加する
これで検索ロジックを src/lib.rs の main 関数から分離できたので、コードの中核となる機能に対するテストをずっと簡単に書けるようになりました。コマンドラインからバイナリを呼び出さなくても、さまざまな引数で関数を直接呼び出し、戻り値を確認できます。
このセクションでは、次の手順に従うテスト駆動開発(TDD)のプロセスを使って、minigrep プログラムに検索ロジックを追加します。
- 失敗するテストを書き、期待した理由で失敗することを確認するためにそれを実行する。
- 新しいテストが通るようにするために必要な最小限のコードだけを書く、または修正する。
- 追加または変更したコードをリファクタリングし、テストが引き続き通ることを確認する。
- 手順 1 から繰り返す!
これはソフトウェアを書く多くの方法のうちの 1 つにすぎませんが、TDD はコード設計を導くのに役立ちます。テストを通すコードを書く前にテストを書くことで、プロセス全体を通して高いテストカバレッジを維持しやすくなります。
ここでは、ファイル内容の中からクエリ文字列を実際に検索し、クエリに一致した行の一覧を生成する機能の実装をテスト駆動で進めます。この機能は search という関数に追加します。
失敗するテストを書く
src/lib.rs では、第11章 で行ったように、テスト関数を持つ tests モジュールを追加します。テスト関数では、search 関数に持たせたい振る舞いを指定します。つまり、クエリと検索対象のテキストを受け取り、クエリを含むテキスト中の行だけを返すようにします。リスト 12-15 にこのテストを示します。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
unimplemented!();
}
// --snip--
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
このテストでは文字列 "duct" を検索します。検索対象のテキストは 3 行あり、そのうち "duct" を含むのは 1 行だけです(先頭の二重引用符の直後にあるバックスラッシュは、この文字列リテラルの内容の先頭に Rust が改行文字を入れないようにするものです)。search 関数から返される値が、期待するその 1 行だけを含んでいることをアサートします。
このテストを実行すると、現在は unimplemented! マクロが “not implemented” というメッセージでパニックを起こすため失敗します。TDD の原則に従い、まずは小さな一歩として、search 関数を常に空のベクタを返すように定義することで、その関数を呼び出したときにテストがパニックしないようにするための最小限のコードだけを追加します。これはリスト 12-16 に示しています。すると、テストはコンパイルできるようになり、空のベクタが "safe, fast, productive." という行を含むベクタと一致しないため、失敗するはずです。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
では、なぜ search のシグネチャに明示的なライフタイム 'a を定義し、そのライフタイムを contents 引数と戻り値に使う必要があるのかを説明しましょう。第10章 で見たように、ライフタイムパラメータは、どの引数のライフタイムが戻り値のライフタイムと結び付いているかを指定します。この場合、返されるベクタには、contents 引数のスライスを参照する文字列スライスが含まれるべきであることを示しています(query 引数のスライスではありません)。
言い換えると、search 関数が返すデータは、search 関数に contents 引数として渡されたデータと同じだけ生存すると Rust に伝えているのです。これは重要です! スライスが参照しているデータは、その参照が有効であるために有効でなければなりません。もしコンパイラが、contents ではなく query の文字列スライスを作っていると仮定すると、安全性チェックを誤って行うことになります。
もしライフタイム注釈を付け忘れてこの関数をコンパイルしようとすると、次のエラーが出ます。
$ cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:1:51
|
1 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
|
1 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` (lib) due to 1 previous error
Rust には、出力に対して 2 つのパラメータのうちどちらが必要なのかわからないため、明示的に伝える必要があります。ヘルプテキストでは、すべてのパラメータと出力型に同じライフタイムパラメータを指定するよう提案されていますが、これは誤りであることに注意してください! contents は私たちのすべてのテキストを含んでいるパラメータであり、そのテキストのうち一致した部分を返したいので、ライフタイム構文を使って戻り値と結び付けるべきなのは contents だけだとわかります。
他のプログラミング言語では、シグネチャ内で引数と戻り値を結び付けることを要求しませんが、このやり方にも時間とともに慣れていくでしょう。この例を、第10章の 「ライフタイムで参照を検証する」 セクションの例と比べてみるとよいかもしれません。
テストを通すコードを書く
現在、テストが失敗しているのは、常に空のベクタを返しているからです。これを修正して search を実装するために、プログラムは次の手順に従う必要があります。
- 内容の各行を順にたどる。
- その行にクエリ文字列が含まれているかを確認する。
- 含まれていれば、返す値の一覧に追加する。
- 含まれていなければ、何もしない。
- 一致した結果の一覧を返す。
では、最初に各行を順にたどるところから、各手順を見ていきましょう。
lines メソッドで各行を順にたどる
Rust には、文字列を 1 行ずつ反復処理するための便利なメソッドがあり、名前もそのまま lines です。これはリスト 12-17 に示すように動作します。なお、これはまだコンパイルできません。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// do something with line
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
lines メソッドはイテレータを返します。イテレータについては 第13章 で詳しく扱います。しかし、コレクションの各要素に対して何らかのコードを実行するために、イテレータを使った for ループを リスト 3-5 で使っていたことを思い出してください。
各行にクエリが含まれているかを調べる
次に、現在の行にクエリ文字列が含まれているかどうかを調べます。ありがたいことに、文字列にはこれをしてくれる contains という便利なメソッドがあります! リスト 12-18 に示すように、search 関数に contains メソッドの呼び出しを追加しましょう。なお、これもまだコンパイルできません。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(query) {
// do something with line
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
現時点では、機能を少しずつ積み上げています。コードをコンパイルできるようにするには、関数シグネチャで示したとおり、本体から値を返す必要があります。
一致した行の保存
この関数を完成させるには、返したい一致した行を保存する方法が必要です。
そのために、for ループの前で可変ベクターを作成し、push
メソッドを呼び出してベクターに line を保存できます。for
ループの後で、リスト 12-19 に示すように、そのベクターを返します。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
これで search 関数は query を含む行だけを返すはずであり、
テストも通るはずです。テストを実行してみましょう。
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
テストは通りました。これで、うまく動作することがわかります。
この時点で、テストを通したまま同じ機能を維持しつつ、search 関数の実装をリファクタリングできる箇所を検討できます。search 関数のコードはそれほど悪くありませんが、イテレーターの便利な機能をいくつか活用できていません。この例には 第13章で戻り、そこでイテレーターを詳しく探りながら、 これをどのように改善できるかを見ていきます。
これでプログラム全体が動作するはずです! 実際に試してみましょう。まずは、 エミリー・ディキンソンの詩からちょうど1行だけ返るはずの単語、frog です。
$ cargo run -- frog poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep frog poem.txt`
How public, like a frog
いいですね! では次に、body のように複数の行に一致する単語を試してみましょう。
$ cargo run -- body poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!
最後に、monomorphization のように、詩のどこにも存在しない単語を検索したときには、どの行も取得されないことを確認しましょう。
$ cargo run -- monomorphization poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep monomorphization poem.txt`
すばらしいです! 私たちは古典的なツールのミニ版を自分たちで作り、 アプリケーションをどのように構成するかについて多くを学びました。また、 ファイルの入力と出力、ライフタイム、テスト、コマンドライン解析についても少し学びました。
このプロジェクトの締めくくりとして、環境変数の扱い方と標準エラーへの出力方法を簡単に示します。どちらも、 コマンドラインプログラムを書くときに役立ちます。
環境変数を扱う
環境変数を扱う
追加機能として、環境変数を介してユーザーが有効化できる、大文字と小文字を区別しない検索オプションを追加して、minigrep バイナリを改善しましょう。この機能をコマンドラインオプションにして、必要になるたびに毎回ユーザーに指定してもらうこともできますが、代わりに環境変数にすることで、ユーザーは一度だけ環境変数を設定すれば、そのターミナルセッション中のすべての検索で大文字と小文字を区別しないようにできます。
大文字と小文字を区別しない検索のための失敗するテストを書く
まず、環境変数に値があるときに呼び出される新しい search_case_insensitive 関数を minigrep ライブラリに追加します。引き続き TDD のプロセスに従うので、最初のステップもやはり失敗するテストを書くことです。新しい search_case_insensitive 関数用の新しいテストを追加し、2 つのテストの違いを明確にするために、既存のテスト名を one_result から case_sensitive に変更します。これはリスト 12-20 に示されています。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
既存のテストの contents も編集したことに注目してください。大文字の D を使った "Duct tape." という新しい行を追加しました。これは、大文字と小文字を区別する方法で検索している場合、クエリ "duct" には一致しないはずです。このように既存のテストを変更することで、すでに実装した大文字と小文字を区別する検索機能を誤って壊していないことを確認しやすくなります。このテストは今の時点で通るはずであり、大文字と小文字を区別しない検索の実装を進める間も通り続けるはずです。
大文字と小文字を区別しない検索用の新しいテストでは、クエリとして "rUsT" を使います。これから追加する search_case_insensitive 関数では、クエリ "rUsT" は大文字の R を含む "Rust:" を含む行に一致し、さらに "Trust me." という行にも一致するはずです。どちらもクエリとは大文字小文字の使い方が異なっていても一致します。これが失敗するテストであり、まだ search_case_insensitive 関数を定義していないため、コンパイルに失敗します。リスト 12-16 の search 関数で行ったのと同様に、常に空のベクタを返す骨組みの実装を追加して、テストがコンパイルし、そして失敗することを確認してみてもかまいません。
search_case_insensitive 関数を実装する
リスト 12-21 に示す search_case_insensitive 関数は、search 関数とほとんど同じになります。唯一の違いは、query と各 line を小文字化することです。そうすることで、入力引数の大文字小文字がどうであっても、行にクエリが含まれているかを調べるときには同じ大文字小文字で比較できます。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
まず、query 文字列を小文字化し、元の query をシャドーイングする同名の新しい変数に保存します。クエリに対して to_lowercase を呼び出す必要があるのは、ユーザーのクエリが "rust"、"RUST"、"Rust"、あるいは "rUsT" のいずれであっても、クエリを "rust" であるかのように扱い、大文字小文字を区別しないようにするためです。to_lowercase は基本的な Unicode を処理しますが、100 パーセント正確というわけではありません。実際のアプリケーションを書いているなら、ここでもう少し手を加えたくなるでしょう。しかし、この節の主題は Unicode ではなく環境変数なので、ここではこのままにしておきます。
ここで query は文字列スライスではなく String になっていることに注意してください。to_lowercase を呼び出すと、既存のデータを参照するのではなく、新しいデータが作られるためです。たとえばクエリが "rUsT" だとしましょう。この文字列スライスには、使える小文字の u や t は含まれていないので、"rust" を含む新しい String を確保する必要があります。いま query を引数として contains メソッドに渡す際には、アンパサンドを付ける必要があります。これは、contains のシグネチャが文字列スライスを受け取るように定義されているためです。
次に、各 line に対しても to_lowercase を呼び出し、すべての文字を小文字にします。line と query の両方を小文字に変換したので、クエリの大文字小文字がどうであっても一致を見つけられるようになります。
この実装がテストを通るか見てみましょう。
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.33s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
すばらしいです! テストは通りました。では次に、新しい search_case_insensitive 関数を run 関数から呼び出しましょう。まず、大文字と小文字を区別する検索と区別しない検索を切り替えるための設定オプションを Config 構造体に追加します。このフィールドを追加すると、まだどこでもこのフィールドを初期化していないため、コンパイラエラーが発生します。
ファイル名: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
// --snip--
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
bool 値を保持する ignore_case フィールドを追加しました。次に、run 関数が ignore_case フィールドの値を確認し、その値に応じて search 関数と search_case_insensitive 関数のどちらを呼び出すか決める必要があります。これはリスト 12-22 に示されています。これもまだコンパイルは通りません。
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
// --snip--
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
最後に、環境変数を確認する必要があります。環境変数を扱うための関数は標準ライブラリの env モジュールにあり、これはすでに src/main.rs の先頭でスコープに入っています。env モジュールの var 関数を使って、IGNORE_CASE という名前の環境変数に何らかの値が設定されているかどうかを確認します。これはリスト 12-23 に示されています。
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
ここでは、新しい変数 ignore_case を作成しています。その値を設定するために、env::var 関数を呼び出し、IGNORE_CASE 環境変数の名前を渡します。env::var 関数は Result を返し、環境変数が何らかの値に設定されている場合には、その値を含む成功の Ok バリアントになります。環境変数が設定されていない場合は、Err バリアントを返します。
Result の is_ok メソッドを使って環境変数が設定されているかどうかを確認しています。これは、プログラムが大文字と小文字を区別しない検索を行うべきであることを意味します。IGNORE_CASE 環境変数に何も設定されていなければ、is_ok は false を返し、プログラムは大文字と小文字を区別する検索を実行します。環境変数の 値 自体は気にしておらず、設定されているか未設定かだけが重要なので、unwrap、expect、あるいはこれまでに見てきた Result のほかのメソッドを使うのではなく、is_ok を確認しています。
run 関数がその値を読み取り、リスト 12-22 で実装したように search_case_insensitive と search のどちらを呼ぶかを判断できるように、ignore_case 変数の値を Config インスタンスに渡します。
試してみましょう! まず、環境変数を設定せず、クエリを to にしてプログラムを実行します。これは、すべて小文字の単語 to を含むすべての行にマッチするはずです。
$ cargo run -- to poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
問題なく動いているようです! では次に、IGNORE_CASE を 1 に設定し、同じクエリ to でプログラムを実行してみましょう。
$ IGNORE_CASE=1 cargo run -- to poem.txt
PowerShell を使っている場合は、環境変数を設定してプログラムを実行する必要があります。
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
これにより、IGNORE_CASE はシェルセッションの残りの間、設定されたままになります。Remove-Item cmdlet を使えば解除できます。
PS> Remove-Item Env:IGNORE_CASE
大文字を含む可能性のある to を含んだ行が得られるはずです。
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
すばらしいことに、To を含む行も得られました! これで minigrep プログラムは、環境変数で制御される大文字と小文字を区別しない検索を実行できるようになりました。これで、コマンドライン引数または環境変数を使って設定されるオプションをどのように管理するかがわかりました。
同じ設定に対して引数 と 環境変数の両方を許可するプログラムもあります。そのような場合、プログラムはどちらか一方を優先するように決めます。もう 1 つ自分でやってみる練習として、コマンドライン引数または環境変数のどちらかで大文字と小文字の区別を制御してみてください。プログラムを、一方では大文字と小文字を区別する設定、もう一方では区別しない設定で実行した場合に、コマンドライン引数と環境変数のどちらを優先するべきかを決めてください。
std::env モジュールには、環境変数を扱うためのさらに多くの便利な機能が含まれています。どのようなものが利用できるかは、そのドキュメントを確認してください。
標準出力ではなく標準エラー出力にエラーを書き込む
エラーを標準エラーにリダイレクトする
現時点では、すべての出力を println! マクロを使ってターミナルに書き込んでいます。ほとんどのターミナルには 2 種類の出力があります。一般的な情報のための 標準出力 (stdout) と、エラーメッセージのための 標準エラー (stderr) です。この区別によって、ユーザーはプログラムの正常な出力をファイルに送る一方で、エラーメッセージは引き続き画面に表示することを選べます。
println! マクロは標準出力にしか出力できないため、標準エラーに出力するには別のものを使う必要があります。
エラーがどこに書き込まれているかを確認する
まず、minigrep が出力している内容が現在どのように標準出力に書き込まれているのかを確認しましょう。これには、本来は標準エラーに書き込みたいエラーメッセージも含まれています。これを確認するために、意図的にエラーを発生させつつ、標準出力ストリームをファイルにリダイレクトします。標準エラーストリームはリダイレクトしないので、標準エラーに送られた内容は引き続き画面に表示されます。
コマンドラインプログラムは、標準出力ストリームをファイルにリダイレクトした場合でも画面上でエラーメッセージを確認できるよう、エラーメッセージを標準エラーストリームに送ることが期待されます。現在の私たちのプログラムはそのように適切には振る舞っていません。エラーメッセージの出力を代わりにファイルへ保存してしまうことを、これから確認します。
この挙動を示すために、> と、標準出力ストリームのリダイレクト先にしたいファイルパス output.txt を指定してプログラムを実行します。引数は何も渡しません。これによりエラーが発生するはずです。
$ cargo run > output.txt
> という構文は、標準出力の内容を画面ではなく output.txt に書き込むようシェルに指示します。予想していたエラーメッセージは画面に表示されなかったので、これはそのメッセージがファイルに入ったことを意味します。output.txt の中身は次のとおりです。
Problem parsing arguments: not enough arguments
そのとおり、エラーメッセージは標準出力に出力されています。このようなエラーメッセージは、正常実行時のデータだけがファイルに入るように、標準エラーに出力されるほうがずっと有用です。これを変更しましょう。
エラーを標準エラーに出力する
リスト 12-24 のコードを使って、エラーメッセージの出力方法を変更します。この章の前半で行ったリファクタリングのおかげで、エラーメッセージを出力するコードはすべて 1 つの関数 main にあります。標準ライブラリには標準エラーストリームに出力する eprintln! マクロが用意されているので、エラーを出力するために println! を呼び出していた 2 か所を、eprintln! を使うように変更しましょう。
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
それでは、先ほどと同じように、引数を渡さず、> で標準出力をリダイレクトして、もう一度プログラムを実行してみましょう。
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
今度は画面上にエラーが表示され、output.txt には何も含まれません。これはコマンドラインプログラムに期待される挙動です。
次に、エラーは発生しないが標準出力はファイルにリダイレクトする引数を付けて、もう一度プログラムを実行してみましょう。次のようにします。
$ cargo run -- to poem.txt > output.txt
ターミナルには何も出力されず、output.txt には結果が入ります。
ファイル名: output.txt
Are you nobody, too?
How dreary to be somebody!
これで、正常な出力には標準出力を、エラー出力には標準エラーを、それぞれ適切に使うようになったことがわかります。
まとめ
この章では、これまで学んできた主要な概念のいくつかを振り返り、Rust で一般的な I/O 操作を行う方法を扱いました。コマンドライン引数、ファイル、環境変数、そしてエラーを出力するための eprintln! マクロを使うことで、コマンドラインアプリケーションを書く準備が整いました。前の章までの概念と組み合わせれば、コードはよく整理され、適切なデータ構造に効率的にデータを格納し、エラーをうまく扱い、十分にテストされるようになります。
次は、関数型言語の影響を受けた Rust の機能、すなわちクロージャとイテレータを見ていきます。
関数型言語の機能: イテレータとクロージャ
Rust の設計は、多くの既存の言語や 技法から着想を得ており、その中でも重要な影響の 1 つが 関数型プログラミング です。 関数型スタイルでのプログラミングには、関数を値として扱い、 引数として渡したり、他の関数から返したり、後で実行するために変数に 代入したりすることなどがよく含まれます。
この章では、関数型プログラミングとは何か、何ではないかという問題を議論するのではなく、 代わりに、しばしば関数型と呼ばれる多くの言語の機能に似た Rust の いくつかの機能について説明します。
より具体的には、次の内容を扱います。
- クロージャ、変数に格納できる関数に似た構成要素
- イテレータ、一連の要素を処理する方法
- 第 12 章の I/O プロジェクトを改善するためにクロージャとイテレータを使う方法
- クロージャとイテレータのパフォーマンス(ネタバレ: 思っているより 高速です!)
パターンマッチングや enum のような、関数型スタイルの影響を受けた他の Rust の機能については、すでに扱いました。 クロージャとイテレータを習得することは、高速で Rust らしい Rust コードを書くうえで重要な部分であるため、この章全体をそれらに充てます。
クロージャ
クロージャ
Rust のクロージャは、変数に保存したり、他の関数に引数として渡したりできる無名関数です。ある場所でクロージャを作成し、その後、別の場所でそのクロージャを呼び出して、異なるコンテキストで評価できます。関数とは異なり、クロージャは定義されたスコープから値をキャプチャできます。ここでは、こうしたクロージャの機能によって、コードの再利用や振る舞いのカスタマイズがどのように可能になるかを示します。
環境のキャプチャ
まず、クロージャを使って、定義された環境から値をキャプチャし、あとで使う方法を見ていきましょう。状況は次のとおりです。私たちの T シャツ会社では、販促のために、ときどきメーリングリストの登録者の誰かに限定版の特別なシャツを無料で配布しています。メーリングリストの登録者は、任意でプロフィールに好きな色を追加できます。無料シャツの当選者に好きな色が設定されていれば、その色のシャツを受け取ります。好きな色を指定していなければ、その時点で会社に最も多く在庫がある色のシャツを受け取ります。
これを実装する方法はたくさんあります。この例では、簡単にするため、利用可能な色数を制限して、Red と Blue というバリアントを持つ ShirtColor という enum を使います。会社の在庫は Inventory 構造体で表し、その shirts という名前のフィールドには、現在在庫にあるシャツの色を表す Vec<ShirtColor> が入っています。Inventory に定義された giveaway メソッドは、無料シャツ当選者の任意の色の希望を受け取り、その人が受け取るシャツの色を返します。この構成をリスト 13-1 に示します。
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
let user_pref1 = Some(ShirtColor::Red);
let giveaway1 = store.giveaway(user_pref1);
println!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}
main で定義されている store には、この限定版プロモーション用に配布するシャツとして、青いシャツが 2 枚、赤いシャツが 1 枚残っています。giveaway メソッドを、赤いシャツを希望するユーザーと、希望をまったく持たないユーザーに対して呼び出します。
繰り返しになりますが、このコードはさまざまな方法で実装できます。ここではクロージャに焦点を当てるため、すでに学んだ概念に絞っています。例外は、クロージャを使っている giveaway メソッドの本体だけです。giveaway メソッドでは、ユーザーの希望を Option<ShirtColor> 型の引数として受け取り、user_preference に対して unwrap_or_else メソッドを呼び出します。Option<T> の unwrap_or_else メソッドは標準ライブラリで定義されています。これは 1 つの引数を取ります。引数は、引数をまったく取らず、値 T を返すクロージャです(この場合の T は Option<T> の Some バリアントに格納される型であり、ShirtColor です)。Option<T> が Some バリアントであれば、unwrap_or_else は Some の中の値を返します。Option<T> が None バリアントであれば、unwrap_or_else はクロージャを呼び出し、そのクロージャが返した値を返します。
unwrap_or_else への引数として、クロージャ式 || self.most_stocked() を指定します。これは、それ自体では何の引数も取らないクロージャです(クロージャに引数がある場合は、2 本の縦棒の間に書かれます)。クロージャの本体は self.most_stocked() を呼び出します。ここでクロージャを定義しており、unwrap_or_else の実装は、結果が必要になったときにあとでこのクロージャを評価します。
このコードを実行すると、次のように出力されます。
$ cargo run
Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue
ここで興味深い点の 1 つは、現在の Inventory インスタンスに対して self.most_stocked() を呼び出すクロージャを渡していることです。標準ライブラリは、私たちが定義した Inventory や ShirtColor 型についても、この場面で使いたいロジックについても、何も知る必要がありませんでした。クロージャは self である Inventory インスタンスへの不変参照をキャプチャし、私たちが指定したコードとともにそれを unwrap_or_else メソッドに渡します。一方、関数はこのように環境をキャプチャすることはできません。
クロージャ型の推論と注釈
関数とクロージャの違いは、ほかにもあります。クロージャでは通常、fn 関数のように、引数や戻り値の型を注釈する必要はありません。関数では、型がユーザーに公開される明示的なインターフェースの一部なので、型注釈が必要です。このインターフェースを厳密に定義することは、関数がどの型の値を使い、どの型の値を返すのかについて、全員の認識を一致させるために重要です。一方、クロージャはこのような公開インターフェースでは使われません。クロージャは変数に保存され、名前を付けてライブラリの利用者に公開することなく使われます。
クロージャは通常短く、任意の場面で使われるというより、狭い文脈の中でのみ意味を持ちます。こうした限定された文脈では、コンパイラはほとんどの変数の型を推論できるのと同じように、引数の型や戻り値の型を推論できます(コンパイラがクロージャの型注釈も必要とするまれな場合はあります)。
変数と同様に、厳密には必要なくても、より冗長になる代わりに明示性と明確さを高めたいなら、型注釈を追加できます。クロージャに型注釈を付けると、リスト 13-2 に示す定義のようになります。この例では、リスト 13-1 のように引数として渡す位置でクロージャを定義するのではなく、クロージャを定義して変数に格納しています。
use std::thread;
use std::time::Duration;
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!("Today, do {} pushups!", expensive_closure(intensity));
println!("Next, do {} situps!", expensive_closure(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_closure(intensity)
);
}
}
}
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
型注釈を追加すると、クロージャの構文は関数の構文とより似たものに見えます。ここでは比較のために、引数に 1 を加える関数と、同じ振る舞いを持つクロージャを定義しています。対応する部分がそろうように、いくつか空白を追加しています。これにより、クロージャの構文が、パイプの使用と省略可能な構文の量を除けば、関数の構文と似ていることがわかります。
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
最初の行は関数定義を示し、2 行目は型注釈を完全に付けた
クロージャ定義を示しています。3 行目では、クロージャ定義から型注釈を
取り除いています。4 行目では、中括弧を取り除いています。これは、
クロージャ本体が式を 1 つだけ持つため省略可能です。これらはすべて
有効な定義であり、呼び出されたときに同じ振る舞いをします。
add_one_v3 と add_one_v4 の行は、コンパイルできるようにするために
クロージャが評価される必要があります。これは、型がその使用方法から
推論されるためです。これは、let v = Vec::new(); で、Rust が型を
推論できるようにするには、型注釈を付けるか、何らかの型の値を Vec に
挿入する必要があるのと似ています。
クロージャ定義では、コンパイラは各パラメータと戻り値に対して 1 つの
具体的な型を推論します。たとえば、リスト 13-3 は、パラメータとして
受け取った値をそのまま返すだけの短いクロージャの定義を示しています。
このクロージャは、この例の目的以外ではあまり有用ではありません。
この定義には型注釈をまったく追加していないことに注目してください。
型注釈がないため、このクロージャはどの型でも呼び出すことができ、
ここでは最初に String でそれを行っています。その後、
example_closure を整数で呼び出そうとすると、エラーになります。
fn main() {
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
コンパイラは次のエラーを出します。
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = example_closure(5);
| --------------- ^ expected `String`, found integer
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:4:29
|
4 | let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2 | let example_closure = |x| x;
| ^
help: try using a conversion method
|
5 | let n = example_closure(5.to_string());
| ++++++++++++
For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error
最初に String 値で example_closure を呼び出したとき、コンパイラは
x の型とクロージャの戻り値の型を String だと推論します。すると、
それらの型は example_closure 内のクロージャに固定され、次に同じ
クロージャで別の型を使おうとすると型エラーになります。
参照をキャプチャする、または所有権をムーブする
クロージャは、その環境から値を 3 つの方法でキャプチャできます。これらは、 関数がパラメータを受け取る 3 つの方法、すなわち不変借用、可変借用、 所有権の取得に直接対応しています。クロージャは、関数本体がキャプチャ した値をどのように使うかに基づいて、どの方法を使うかを決定します。
リスト 13-4 では、list という名前のベクタへの不変参照をキャプチャする
クロージャを定義しています。これは、値を出力するために不変参照だけが
必要だからです。
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let only_borrows = || println!("From closure: {list:?}");
println!("Before calling closure: {list:?}");
only_borrows();
println!("After calling closure: {list:?}");
}
この例は、変数をクロージャ定義に束縛できること、そして後でその変数名と 丸括弧を使って、まるでその変数名が関数名であるかのようにクロージャを 呼び出せることも示しています。
同時に list への不変参照を複数持てるため、list にはクロージャ定義の
前のコードからも、クロージャ定義の後でクロージャが呼び出される前からも、
そしてクロージャが呼び出された後からも引き続きアクセスできます。この
コードはコンパイルされ、実行され、次のように出力されます。
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]
次に、リスト 13-5 では、クロージャ本体を変更して list ベクタに要素を
追加するようにします。これにより、クロージャは可変参照をキャプチャする
ようになります。
fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("After calling closure: {list:?}");
}
このコードはコンパイルされ、実行され、次のように出力されます。
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]
定義と borrows_mutably クロージャの呼び出しの間には、もはや
println! がないことに注意してください。borrows_mutably が定義される
とき、それは list への可変参照をキャプチャします。クロージャが
呼び出された後でそのクロージャを再び使うことはないので、可変借用は
そこで終わります。クロージャ定義とクロージャ呼び出しの間では、出力の
ための不変借用は許されません。可変借用があるときは、ほかの借用は
一切許されないからです。そこに println! を追加して、どのような
エラーメッセージが出るか試してみてください!
環境内で使う値について、クロージャ本体が厳密には所有権を必要としない
場合でも、クロージャに所有権を取らせたいなら、パラメータリストの前に
move キーワードを使えます。
この手法は主に、クロージャを新しいスレッドに渡してデータをムーブし、
その新しいスレッドが所有するようにしたいときに役立ちます。スレッドと、
なぜそれを使いたいのかについては、第 16 章で並行性を扱う際に詳しく
説明しますが、ここでは今のところ、move キーワードを必要とする
クロージャを使って新しいスレッドを生成する方法を簡単に見てみましょう。
リスト 13-6 は、リスト 13-4 を変更し、メインスレッドではなく新しい
スレッドでベクタを出力するようにしたものです。
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
thread::spawn(move || println!("From thread: {list:?}"))
.join()
.unwrap();
}
新しいスレッドを生成し、そのスレッドに実行するクロージャを引数として
渡します。クロージャ本体はリストを出力します。リスト 13-4 では、
クロージャは list を不変参照でのみキャプチャしていました。これは、
list を出力するのに必要なアクセスがそれで十分だったからです。この例では、
クロージャ本体は依然として不変参照しか必要としないにもかかわらず、
クロージャ定義の先頭に move キーワードを置くことで、list を
クロージャの中へムーブするよう明示する必要があります。メインスレッドが
新しいスレッドに対して join を呼び出す前にさらに多くの操作を行うと、
新しいスレッドがメインスレッドの残りの処理より先に終了するかもしれませんし、
逆にメインスレッドが先に終了するかもしれません。もしメインスレッドが
list の所有権を保持したまま新しいスレッドより先に終了し、list を
ドロップした場合、スレッド内の不変参照は無効になります。したがって、
コンパイラは、新しいスレッドに渡されるクロージャの中へ list を
ムーブすることを要求します。そうすることで、その参照が有効になるからです。
move キーワードを削除したり、クロージャが定義された後でメインスレッド
内で list を使ったりして、どのようなコンパイラエラーが出るか試して
みてください!
クロージャからキャプチャした値をムーブする
クロージャが、定義された環境から参照をキャプチャしたり、値の所有権を キャプチャしたりすると(それによって、何がクロージャ 内に ムーブされるのか、あるいは何もムーブされないのかが影響を受けます)、 その後でクロージャが評価されるときにそれらの参照や値に何が起こるかは、 クロージャ本体のコードによって決まります(それによって、何がクロージャ 外に ムーブされるのか、あるいは何もムーブされないのかが影響を受けます)。
クロージャ本体は、次のいずれも行えます。キャプチャした値をクロージャの 外へムーブする、キャプチャした値を変更する、値をムーブも変更もしない、 あるいはそもそも環境から何もキャプチャしない、のいずれかです。
クロージャが環境から値をどのようにキャプチャし、それらをどう扱うかは、
そのクロージャがどのトレイトを実装するかに影響します。トレイトは、
関数や構造体がどの種類のクロージャを使えるかを指定する方法です。
クロージャは、クロージャ本体が値をどのように扱うかに応じて、これらの
Fn トレイトのうち 1 つ、2 つ、または 3 つすべてを、累積的に
自動実装します。
FnOnceは、1 回呼び出せるクロージャに適用されます。すべてのクロージャは 呼び出すことができるため、少なくともこのトレイトは実装します。キャプチャ した値を本体から外へムーブするクロージャは、1 回しか呼び出せないため、FnOnceのみを実装し、他のFnトレイトは実装しません。FnMutは、キャプチャした値を本体から外へムーブしないものの、キャプチャ した値を変更する可能性があるクロージャに適用されます。これらのクロージャは 複数回呼び出すことができます。Fnは、キャプチャした値を本体から外へムーブせず、かつキャプチャした値を 変更もしないクロージャに適用されます。また、環境から何もキャプチャしない クロージャにも適用されます。これらのクロージャは、環境を変更することなく 複数回呼び出すことができます。これは、たとえばクロージャを並行して複数回 呼び出すような場合に重要です。
それでは、リスト 13-1 で使った Option<T> の unwrap_or_else
メソッドの定義を見てみましょう。
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
T は、Option の Some バリアントに入っている値の型を表す
ジェネリック型であることを思い出してください。その型 T は、
unwrap_or_else 関数の戻り値の型でもあります。たとえば、
Option<String> に対して unwrap_or_else を呼び出すコードは、
String を受け取ることになります。
次に、unwrap_or_else 関数には追加のジェネリック型パラメータ F
があることに注目してください。型 F は f という名前の引数の型で、
これは unwrap_or_else を呼び出すときに渡すクロージャです。
ジェネリック型 F に指定されているトレイト境界は FnOnce() -> T
です。これは、F が 1 回呼び出し可能で、引数を取らず、T を返さ
なければならないことを意味します。トレイト境界に FnOnce を使うことで、
unwrap_or_else が f を 1 回より多く呼び出さないという制約を
表しています。unwrap_or_else の本体を見ると、Option が Some
であれば f は呼び出されません。Option が None であれば、f
は 1 回呼び出されます。すべてのクロージャは FnOnce を実装している
ため、unwrap_or_else は 3 種類すべてのクロージャを受け取ることが
でき、可能な限り柔軟です。
注: やりたいことが環境から値をキャプチャする必要がないのであれば、
Fnトレイトのいずれかを実装する何かが必要な箇所で、クロージャの代わりに 関数名を使うことができます。たとえば、Option<Vec<T>>の値に対しては、 値がNoneのときに新しい空のベクタを得るためにunwrap_or_else(Vec::new)を呼び出せます。コンパイラは、関数定義に 対して適用可能なFnトレイトを自動的に実装します。
次に、スライスに定義されている標準ライブラリメソッド sort_by_key を
見て、これが unwrap_or_else とどう異なるのか、そしてなぜ
sort_by_key がトレイト境界に FnOnce ではなく FnMut を使うのかを
確認しましょう。クロージャは、現在対象となっているスライス内の要素への
参照という形で 1 つの引数を受け取り、順序付け可能な型 K の値を返します。
この関数は、各要素の特定の属性でスライスをソートしたいときに便利です。
リスト 13-7 では、Rectangle インスタンスのリストがあり、それらを
width 属性の小さい順に並べるために sort_by_key を使います。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
list.sort_by_key(|r| r.width);
println!("{list:#?}");
}
このコードは次を出力します。
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/rectangles`
[
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
Rectangle {
width: 10,
height: 1,
},
]
sort_by_key が FnMut クロージャを受け取るように定義されている理由は、
そのクロージャを複数回呼び出すからです。スライス内の各要素について 1 回ずつ
呼び出します。クロージャ |r| r.width は、環境から何もキャプチャせず、何も変更せず、何も外へムーブしない
ため、トレイト境界の要件を満たします。
対照的に、リスト 13-8 は、環境から値をムーブするため、FnOnce
トレイトしか実装しないクロージャの例を示しています。コンパイラは、この
クロージャを sort_by_key で使うことを許してくれません。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("closure called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
これは、list をソートするときに sort_by_key がクロージャを何回
呼び出すかを数えようとする、不自然で回りくどい方法です(しかも動作しません)。
このコードは、クロージャの環境にある String である value を
sort_operations ベクタに push することで、その回数を数えようと
しています。クロージャは value をキャプチャし、その後 value
の所有権を sort_operations ベクタに移すことで、value をクロージャの
外へムーブします。このクロージャは 1 回しか呼び出せません。2 回目に
呼び出そうとしても、value はもはや環境内になく、再び
sort_operations に push できないからです。したがって、このクロージャは
FnOnce しか実装しません。このコードをコンパイルしようとすると、
クロージャは FnMut を実装しなければならないため、value を
クロージャの外へムーブできないという次のエラーが出ます。
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let value = String::from("closure called");
| ----- ------------------------------ move occurs because `value` has type `String`, which does not implement the `Copy` trait
| |
| captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | sort_operations.push(value);
| ^^^^^ `value` is moved here
|
help: consider cloning the value if the performance cost is acceptable
|
18 | sort_operations.push(value.clone());
| ++++++++
For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
このエラーは、value を環境から外へムーブしているクロージャ本体の行を
指しています。これを修正するには、クロージャ本体が環境から値をムーブしない
ように変更する必要があります。環境内にカウンタを保持し、クロージャ本体で
その値をインクリメントするほうが、クロージャが何回呼び出されたかを数える
より素直な方法です。リスト 13-9 のクロージャが sort_by_key で
動作するのは、num_sort_operations カウンタへの可変参照だけを
キャプチャしているためであり、したがって複数回呼び出すことができます。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut num_sort_operations = 0;
list.sort_by_key(|r| {
num_sort_operations += 1;
r.width
});
println!("{list:#?}, sorted in {num_sort_operations} operations");
}
イテレータで一連の要素を処理する
イテレータで一連の要素を処理する
イテレータパターンを使うと、ある一連の要素に対して順番に何らかの処理を実行できます。イテレータは、各要素を反復処理するロジックと、シーケンスがいつ終了したかを判断する責務を持ちます。イテレータを使えば、そのロジックを自分で再実装する必要はありません。
Rust では、イテレータは 遅延評価される ため、イテレータを消費して使い切るメソッドを呼ぶまでは何の効果もありません。たとえば、リスト13-10のコードは、Vec<T> に定義されている iter メソッドを呼び出して、ベクタ v1 の要素に対するイテレータを作成します。このコード自体は、単独では何も有用なことをしません。
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
}
このイテレータは v1_iter 変数に格納されます。イテレータを作成したら、それをさまざまな方法で使えます。リスト3-5では、配列に対して for ループを使って反復処理し、その各要素に対して何らかのコードを実行しました。その裏側では、暗黙的にイテレータが作成されて消費されていましたが、それが正確にどのように動くのかについては、これまで詳しく触れていませんでした。
リスト13-11の例では、イテレータの作成と、for ループでのイテレータの使用を分けています。v1_iter にあるイテレータを使って for ループが呼び出されると、イテレータ内の各要素がループの1回の反復で使われ、それぞれの値が表示されます。
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {val}");
}
}
標準ライブラリでイテレータが提供されていない言語では、おそらく同じ機能を実現するために、まずインデックス0の変数から始めて、その変数を使ってベクタにインデックスアクセスして値を取得し、ループの中でその変数の値を増やしていき、ベクタ内の要素総数に達するまで繰り返すことになるでしょう。
イテレータはそのロジックをすべて処理してくれるため、繰り返し書く必要があるうえに間違える可能性のあるコードを減らせます。イテレータを使うと、ベクタのようにインデックスアクセスできるデータ構造だけでなく、多くの異なる種類のシーケンスに対して同じロジックを使えるようになり、柔軟性も高まります。では、イテレータがどのようにそれを実現しているのか見ていきましょう。
Iterator トレイトと next メソッド
すべてのイテレータは、標準ライブラリで定義されている Iterator という名前のトレイトを実装しています。そのトレイトの定義は次のようになっています。
#![allow(unused)]
fn main() {
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
}
}
この定義では、新しい構文がいくつか使われていることに注目してください。type Item と Self::Item は、このトレイトに関連型を定義しています。関連型については第20章で詳しく説明します。今のところ知っておくべきことは、このコードが、Iterator トレイトを実装するには Item 型も定義する必要があり、この Item 型が next メソッドの戻り値の型で使われる、ということです。言い換えると、Item 型はそのイテレータから返される型になります。
Iterator トレイトが実装者に定義を要求するメソッドは1つだけです。それが next メソッドで、このメソッドはイテレータの要素を一度に1つずつ返し、その値を Some で包み、反復処理が終了したときには None を返します。
イテレータに対して next メソッドを直接呼び出すこともできます。リスト13-12では、ベクタから作成したイテレータに対して next を繰り返し呼び出したときに、どのような値が返されるかを示しています。
#[cfg(test)]
mod tests {
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
}
ここで v1_iter をミュータブルにする必要があったことに注意してください。イテレータに対して next メソッドを呼び出すと、イテレータがシーケンスのどこにいるかを追跡するために使っている内部状態が変化します。言い換えると、このコードはイテレータを 消費 する、つまり使い切ります。next を呼び出すたびに、イテレータから1つの要素が取り出されます。for ループを使ったときには v1_iter をミュータブルにする必要はありませんでした。これは、ループが v1_iter の所有権を受け取り、裏側でそれをミュータブルにしていたからです。
また、next の呼び出しで得られる値は、ベクタ内の値への不変参照であることにも注意してください。iter メソッドは、不変参照に対するイテレータを生成します。v1 の所有権を受け取り、所有された値を返すイテレータを作りたい場合は、iter ではなく into_iter を呼び出します。同様に、可変参照に対して反復処理したい場合は、iter ではなく iter_mut を呼び出します。
イテレータを消費するメソッド
Iterator トレイトには、標準ライブラリによってデフォルト実装が提供されている多数の異なるメソッドがあります。これらのメソッドについては、Iterator トレイトの標準ライブラリ API ドキュメントを見ると調べられます。これらのメソッドの中には、その定義の中で next メソッドを呼び出すものがあります。そのため、Iterator トレイトを実装する際には next メソッドの実装が必須なのです。
next を呼び出すメソッドは 消費アダプタ と呼ばれます。なぜなら、それらを呼び出すとイテレータが使い切られるからです。1つの例が sum メソッドです。これはイテレータの所有権を受け取り、next を繰り返し呼び出して要素を走査することで、イテレータを消費します。反復処理の間、それぞれの要素を累積合計に加算し、反復が完了すると合計を返します。リスト13-13には、sum メソッドの使用例を示すテストがあります。
#[cfg(test)]
mod tests {
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
}
sum は、それを呼び出したイテレータの所有権を受け取るため、sum の呼び出し後に v1_iter を使うことはできません。
別のイテレータを生成するメソッド
イテレータアダプタ とは、Iterator トレイトに定義されているメソッドで、イテレータを消費しないものを指します。代わりに、元のイテレータの何らかの性質を変えることで、異なるイテレータを生成します。
リスト13-14は、イテレータアダプタメソッド map を呼び出す例を示しています。map は、要素が反復処理されるときに各要素に対して呼び出すクロージャを受け取ります。map メソッドは、変更後の要素を生成する新しいイテレータを返します。ここでのクロージャは、ベクタの各要素が1ずつ増加した新しいイテレータを作成します。
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
}
しかし、このコードは警告を生成します。
$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = v1.iter().map(|x| x + 1);
| +++++++
warning: `iterators` (bin "iterators") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
リスト13-14のコードは何もしません。指定したクロージャは一度も呼び出されないのです。この警告はその理由を思い出させてくれます。イテレータアダプタは遅延評価されるため、ここではイテレータを消費する必要があります。
この警告を解消し、イテレータを消費するために、リスト 12-1 で
env::args とともに使った collect メソッドを使用します。このメソッドは
イテレータを消費し、得られた値をコレクションのデータ型に集めます。
リスト 13-15 では、map の呼び出しから返されたイテレータを反復した結果を
ベクタに集めています。このベクタには最終的に、元のベクタの各要素を 1 ずつ
増やしたものが格納されます。
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
}
map はクロージャを受け取るので、各要素に対して実行したい任意の操作を
指定できます。これは、Iterator トレイトが提供するイテレーションの振る舞いを
再利用しつつ、クロージャによって一部の振る舞いをカスタマイズできることを示す
優れた例です。
複数のイテレータアダプタの呼び出しを連結して、複雑な処理を読みやすい方法で 実行できます。しかし、すべてのイテレータは遅延評価されるため、 イテレータアダプタの呼び出しから結果を得るには、消費アダプタメソッドのいずれかを 呼び出さなければなりません。
環境をキャプチャするクロージャ
多くのイテレータアダプタは引数としてクロージャを受け取り、一般的に、 イテレータアダプタに引数として指定するクロージャは、その環境をキャプチャする クロージャになります。
この例では、クロージャを受け取る filter メソッドを使います。この
クロージャはイテレータから 1 つの要素を受け取り、bool を返します。クロージャが
true を返した場合、その値は filter が生成するイテレーションに含まれます。
クロージャが false を返した場合、その値は含まれません。
リスト 13-16 では、環境から shoe_size
変数をキャプチャするクロージャとともに filter を使って、Shoe 構造体の
インスタンスのコレクションを反復します。これにより、指定したサイズの靴だけが
返されます。
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filters_by_size() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];
let in_my_size = shoes_in_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}
shoes_in_size 関数は、靴のベクタと靴のサイズの所有権を引数として受け取ります。
そして、指定したサイズの靴だけを含むベクタを返します。
shoes_in_size の本体では、into_iter を呼び出して、ベクタの所有権を受け取る
イテレータを作成します。次に、filter を呼び出して、そのイテレータを、
クロージャが true を返す要素だけを含む新しいイテレータに変換します。
クロージャは環境から shoe_size パラメータをキャプチャし、その値を各靴の
サイズと比較して、指定したサイズの靴だけを残します。最後に、collect を
呼び出すことで、変換後のイテレータが返す値をベクタに集め、それが関数から
返されます。
このテストは、shoes_in_size を呼び出したときに、指定した値と同じサイズの
靴だけが返されることを示しています。
I/Oプロジェクトを改善する
I/O プロジェクトの改善
イテレータに関するこの新しい知識により、第12章の I/O プロジェクトを改善できます。イテレータを使うことで、コード中のいくつかの箇所をより明確かつ簡潔にできるのです。イテレータによって Config::build 関数と search 関数の実装をどのように改善できるかを見ていきましょう。
イテレータを使って clone を取り除く
リスト 12-6 では、String 値のスライスを受け取り、そのスライスにインデックスでアクセスして値をクローンすることで Config 構造体のインスタンスを作成するコードを追加しました。これにより、Config 構造体がそれらの値を所有できるようにしていました。リスト 13-17 では、リスト 12-23 にあった Config::build 関数の実装を再掲しています。
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
当時は、効率の悪い clone 呼び出しは将来取り除くので気にしなくてよいと言いました。では、その時が来ました!
ここで clone が必要だったのは、引数 args が String 要素を持つスライスであり、build 関数は args を所有していないからです。Config インスタンスの所有権を返すためには、Config インスタンスがその値を所有できるように、query フィールドと file_path フィールドに入れる値をクローンする必要がありました。
イテレータについての新しい知識により、build 関数を、スライスを借用する代わりにイテレータの所有権を引数として受け取るように変更できます。スライスの長さを確認して特定の位置にインデックスでアクセスするコードの代わりに、イテレータの機能を使います。これにより、イテレータが値にアクセスするため、Config::build 関数が何をしているのかがより明確になります。
Config::build がイテレータの所有権を受け取り、借用を伴うインデックス操作をやめれば、clone を呼び出して新たにメモリ確保を行う代わりに、イテレータから String の値を Config にムーブできるようになります。
返されたイテレータを直接使う
I/O プロジェクトの src/main.rs ファイルを開いてください。内容は次のようになっているはずです。
ファイル名: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
まず、リスト 12-24 にあった main 関数の冒頭を、今回はイテレータを使うリスト 13-18 のコードに変更します。Config::build も更新するまでは、これはコンパイルされません。
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
env::args 関数はイテレータを返します! イテレータの値をベクタに集めてからそのスライスを Config::build に渡すのではなく、今度は env::args から返されたイテレータの所有権を直接 Config::build に渡します。
次に、Config::build の定義を更新する必要があります。Config::build のシグネチャをリスト 13-19 のように変更しましょう。関数本体も更新する必要があるので、これもまだコンパイルされません。
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// --snip--
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
標準ライブラリの env::args 関数のドキュメントによると、返されるイテレータの型は std::env::Args であり、この型は Iterator トレイトを実装して String 値を返します。
Config::build 関数のシグネチャを更新し、パラメータ args が &[String] ではなく、トレイト境界 impl Iterator<Item = String> を持つジェネリック型になるようにしました。第10章の 「トレイトを引数として使う」
節で説明した impl Trait 構文のこの使い方は、args が Iterator トレイトを実装し、String 項目を返す任意の型になり得ることを意味します。
args の所有権を受け取り、さらに反復処理によって args を変更するので、args パラメータの指定に mut キーワードを追加して可変にできます。
Iterator トレイトのメソッドを使う
次に、Config::build の本体を修正します。args は Iterator トレイトを実装しているので、next メソッドを呼び出せると分かります! リスト 13-20 では、next メソッドを使うようにリスト 12-23 のコードを更新しています。
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
env::args の戻り値の最初の値はプログラム名であることを思い出してください。これを無視して次の値に進みたいので、まず next を呼び出し、その戻り値には何もしません。次に、Config の query フィールドに入れたい値を取得するために next を呼び出します。next が Some を返した場合は、match を使って値を取り出します。None を返した場合は、引数が十分に与えられていないことを意味するので、Err 値を返して早期に戻ります。file_path の値についても同じことを行います。
イテレータアダプタでコードをより明確にする
I/O プロジェクトの search 関数でもイテレータを活用できます。この関数は、リスト 12-19 のものをリスト 13-21 に再掲しています。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
このコードは、イテレータアダプタメソッドを使うことで、より簡潔に書けます。そうすることで、可変の中間 results ベクタを持たずに済みます。関数型プログラミングのスタイルでは、コードをより明確にするために可変状態の量を最小限に抑えることが好まれます。可変状態を取り除くことで、将来的に検索を並列に実行する改良が可能になるかもしれません。というのも、results ベクタへの同時アクセスを管理する必要がなくなるからです。リスト 13-22 はこの変更を示しています。
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
さらに改善するには、collect の呼び出しを削除し、戻り値の型を impl Iterator<Item = &'a str> に変更して、search 関数からイテレータを返すようにします。そうすると、この関数はイテレータアダプタになります。テストも更新する必要があることに注意してください! この変更を行う前後で minigrep ツールを使って大きなファイルを検索し、動作の違いを観察してください。この変更前は、すべての結果を集め終わるまでプログラムは結果をまったく表示しませんが、変更後は、一致する行が見つかるたびに結果が表示されます。これは、run 関数内の for ループがイテレータの遅延性を活用できるようになるためです。
ループとイテレータのどちらを選ぶか
次に自然に出てくる疑問は、自分のコードではどちらのスタイルを選ぶべきか、そしてなぜかということです。つまり、リスト13-21の元の実装と、リスト13-22のイテレータを使ったバージョンのどちらを選ぶべきかです(イテレータ自体を返すのではなく、返す前にすべての結果を集めると仮定した場合)。ほとんどのRustプログラマは、イテレータスタイルを使うことを好みます。最初は少し慣れるのが大変ですが、さまざまなイテレータアダプタとその働きの感覚をつかめば、イテレータのほうが理解しやすくなることがあります。ループのさまざまな細部を扱ったり、新しいベクタを組み立てたりする代わりに、コードはループの高レベルな目的に集中します。これにより、ありふれたコードの一部が抽象化されるため、イテレータ内の各要素が満たさなければならないフィルタ条件のように、このコードに固有の概念が見えやすくなります。
しかし、この2つの実装は本当に等価なのでしょうか? 直感的には、より低レベルなループのほうが速いと思うかもしれません。パフォーマンスについて話しましょう。
ループとイテレータのパフォーマンス
ループとイテレータにおけるパフォーマンス
ループとイテレータのどちらを使うべきかを判断するには、どちらの
実装がより高速なのかを知る必要があります。つまり、明示的な
for ループを使った search 関数のバージョンと、イテレータを使った
バージョンのどちらが速いかです。
私たちは、アーサー・コナン・ドイル卿の シャーロック・ホームズの冒険 の
全文を String に読み込み、その内容の中から単語 the を探すことで
ベンチマークを実行しました。for ループを使う search のバージョンと、
イテレータを使うバージョンでのベンチマーク結果は次のとおりです。
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
2 つの実装のパフォーマンスはほぼ同じです!ここではベンチマーク コードの説明はしません。というのも、要点は 2 つのバージョンが等価で あることを証明することではなく、これら 2 つの実装のパフォーマンスが おおむねどのように比較されるかの感覚をつかむことだからです。
より包括的なベンチマークを行うには、contents としてさまざまなサイズの
さまざまなテキストを使い、query として異なる単語や長さの異なる単語を
使い、そのほかあらゆる種類の変化を試すべきです。要点は次のとおりです。
イテレータは高水準の抽象化ではありますが、コンパイルされると、あたかも
自分でより低水準のコードを書いたかのような、ほぼ同じコードになります。
イテレータは Rust の ゼロコスト抽象化 の 1 つであり、これはその抽象化を
使っても実行時オーバーヘッドが一切追加されないことを意味します。これは、
C++ の元の設計者であり実装者である Bjarne Stroustrup が、2012 年の ETAPS
基調講演「Foundations of C++」でゼロオーバーヘッドを次のように定義して
いることに対応しています。
一般に、C++ の実装はゼロオーバーヘッドの原則に従います。使わないものに 対しては、コストを支払う必要はありません。さらに、使うものについても、 手書きでそれ以上に良くコード化することはできません。
多くの場合、イテレータを使った Rust コードは、自分で手書きするのと同じ アセンブリにコンパイルされます。ループ展開や配列アクセス時の境界チェックの 除去といった最適化が適用され、その結果のコードは非常に効率的になります。 これでこのことがわかったので、恐れることなくイテレータやクロージャを使う ことができます。これらを使うとコードはより高水準に見えますが、そのための 実行時パフォーマンスのペナルティは課されません。
まとめ
クロージャとイテレータは、関数型プログラミング言語の考え方に着想を得た Rust の機能です。これらは、高水準の考え方を低水準のパフォーマンスで明確に 表現する Rust の能力に貢献しています。クロージャとイテレータの実装は、 実行時パフォーマンスに影響を与えないようになっています。これは、ゼロコスト 抽象化を提供することを目指す Rust の目標の一部です。
I/O プロジェクトの表現力を改善したところで、次は cargo のさらにいくつかの
機能を見ていきましょう。これらは、このプロジェクトを世界と共有するのに
役立ちます。
Cargo と Crates.io の詳細
ここまで、コードのビルド、実行、テストのために Cargo の最も基本的な 機能だけを使ってきましたが、Cargo にはそれ以外にも多くのことができます。 この章では、そのほかのより高度な機能の一部について説明し、次のことを 行う方法を示します。
- リリースプロファイルを通じてビルドをカスタマイズする。
- ライブラリを crates.io で公開する。
- ワークスペースを使って大規模なプロジェクトを整理する。
- crates.io からバイナリをインストールする。
- カスタムコマンドを使って Cargo を拡張する。
Cargo はこの章で扱う機能よりもさらに多くのことができるので、そのすべての 機能についての完全な説明は、ドキュメント を参照してください。
リリースプロファイルでビルドをカスタマイズする
リリースプロファイルによるビルドのカスタマイズ
Rust では、リリースプロファイル は、コードをコンパイルするためのさまざまなオプションをプログラマーがより細かく制御できるようにする、異なる設定を持つ、あらかじめ定義されたカスタマイズ可能なプロファイルです。各プロファイルは、ほかのプロファイルとは独立して設定されます。
Cargo には 2 つの主要なプロファイルがあります。cargo build を実行したときに Cargo が使う dev プロファイルと、cargo build --release を実行したときに Cargo が使う release プロファイルです。dev プロファイルには開発向けの適切なデフォルトが定義されており、release プロファイルにはリリースビルド向けの適切なデフォルトが定義されています。
これらのプロファイル名は、ビルドの出力で見覚えがあるかもしれません。
$ cargo build
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
$ cargo build --release
Finished `release` profile [optimized] target(s) in 0.32s
この dev と release が、コンパイラによって使われる異なるプロファイルです。
Cargo には各プロファイルのデフォルト設定があり、プロジェクトの Cargo.toml ファイルに [profile.*] セクションを明示的に追加していない場合に適用されます。カスタマイズしたいプロファイルに対して [profile.*] セクションを追加すると、デフォルト設定の任意の一部を上書きできます。たとえば、dev および release プロファイルにおける opt-level 設定のデフォルト値は次のとおりです。
ファイル名: Cargo.toml
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
opt-level 設定は、Rust がコードに適用する最適化の量を制御し、その範囲は 0 から 3 です。より多くの最適化を適用するとコンパイル時間は長くなるため、開発中でコードを頻繁にコンパイルするのであれば、生成されるコードの実行速度が遅くなっても、より速くコンパイルできるように最適化は少ないほうがよいでしょう。したがって、dev のデフォルトの opt-level は 0 です。コードをリリースする準備ができたら、コンパイルにより多くの時間をかけるのが最善です。リリースモードでコンパイルするのは 1 回だけですが、コンパイル済みのプログラムは何度も実行します。そのため、リリースモードではコンパイル時間が長くなる代わりに、より高速に動作するコードが得られます。これが、release プロファイルのデフォルトの opt-level が 3 である理由です。
デフォルト設定は、Cargo.toml に異なる値を追加することで上書きできます。たとえば、開発プロファイルで最適化レベル 1 を使いたい場合は、プロジェクトの Cargo.toml ファイルに次の 2 行を追加できます。
ファイル名: Cargo.toml
[profile.dev]
opt-level = 1
このコードは、デフォルト設定である 0 を上書きします。これで cargo build を実行すると、Cargo は dev プロファイルのデフォルト設定に加えて、opt-level に対する私たちのカスタマイズも使用します。opt-level を 1 に設定したため、Cargo はデフォルトより多くの最適化を適用しますが、リリースビルドほど多くはありません。
各プロファイルの設定オプションとデフォルト値の完全な一覧については、Cargoのドキュメント を参照してください。
Crates.ioにクレートを公開する
Crates.ioにクレートを公開する
私たちはプロジェクトの依存関係としてcrates.ioのパッケージを使用してきましたが、自分自身のパッケージを公開して他の人々とコードを共有することもできます。crates.ioのクレートレジストリは、あなたのパッケージのソースコードを配布するので、主にオープンソースのコードをホストしています。
RustとCargoには、公開したパッケージを人々が見つけやすく、また使いやすくするための機能があります。次にこれらの機能のいくつかについて話し、それからパッケージを公開する方法を説明します。
有用なドキュメンテーションコメントの作成
パッケージを正確にドキュメント化することは、他のユーザーがいつ、どのようにそれらを使用するかを知るのに役立ちます。そのため、ドキュメントの記述に時間をかける価値があります。第3章では、2つのスラッシュ // を使ってRustのコードにコメントする方法を議論しました。Rustにはドキュメンテーションのための特別な種類のコメントもあり、都合の良いことに ドキュメンテーションコメント と呼ばれ、HTMLドキュメントを生成します。このHTMLは、クレートがどのように 実装されているか ではなく、クレートをどのように 使用する かを知りたいプログラマを対象とした、公開APIアイテムのドキュメンテーションコメントの内容を表示します。
ドキュメンテーションコメントは2つではなく3つのスラッシュ /// を使用し、テキストをフォーマットするためにMarkdown記法をサポートします。ドキュメンテーションコメントは、ドキュメント化するアイテムの直前に配置します。リスト14-1は、my_crate という名前のクレート内の add_one 関数のドキュメンテーションコメントを示しています。
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
ここでは、add_one 関数が何をするかの説明を与え、Examples という見出しでセクションを開始し、そして add_one 関数の使用方法を実演するコードを提供しています。このドキュメンテーションコメントからHTMLドキュメントを生成するには、cargo doc を実行します。このコマンドはRustと共に配布されている rustdoc ツールを実行し、生成されたHTMLドキュメントを target/doc ディレクトリに配置します。
利便性のために、cargo doc --open を実行すると、現在のクレートのドキュメント(およびクレートのすべての依存関係のドキュメント)のHTMLをビルドし、その結果をWebブラウザで開きます。add_one 関数に移動すると、図14-1に示すように、ドキュメンテーションコメントのテキストがどのようにレンダリングされるかがわかります。
図14-1: add_one 関数のHTMLドキュメント
一般的に使用されるセクション
リスト14-1では、Markdownの見出し # Examples を使用して、HTMLに「Examples」というタイトルのセクションを作成しました。以下に、クレートの作者がドキュメントで一般的に使用する他のセクションをいくつか挙げます:
- Panics: ドキュメント化されている関数がパニックする可能性のあるシナリオです。プログラムをパニックさせたくない関数の呼び出し元は、これらの状況で関数を呼び出さないようにする必要があります。
- Errors: 関数が
Resultを返す場合、発生する可能性のあるエラーの種類と、それらのエラーが返される原因となる条件を説明することは、呼び出し元がさまざまな種類のエラーをさまざまな方法で処理するコードを書くのに役立ちます。 - Safety: 関数を呼び出すのが
unsafeである場合(unsafe性については第20章で議論します)、なぜその関数がunsafeであるかを説明し、関数が呼び出し元に維持を期待する不変条件を網羅したセクションがあるべきです。
ほとんどのドキュメンテーションコメントはこれらのセクションすべてを必要としませんが、これはユーザーが知りたがるであろうコードの側面を思い出すための良いチェックリストです。
テストとしてのドキュメンテーションコメント
ドキュメンテーションコメントにコード例のブロックを追加することは、ライブラリの使用方法を実演するのに役立ち、さらに追加のボーナスがあります。cargo test を実行すると、ドキュメント内のコード例がテストとして実行されます!例付きのドキュメントに勝るものはありません。しかし、ドキュメントが書かれた後にコードが変更されたために機能しない例ほど悪いものはありません。リスト14-1の add_one 関数のドキュメントで cargo test を実行すると、テスト結果に次のようなセクションが表示されます:
Doc-tests my_crate
running 1 test
test src/lib.rs - add_one (line 5) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s
ここで、関数か例のいずれかを変更して、例の中の assert_eq! がパニックするようにし、再度 cargo test を実行すると、ドキュメントテストが例とコードが互いに同期していないことを検出するのがわかります!
コンテナアイテムのコメント
//! スタイルのドキュメントコメントは、コメントに続くアイテムではなく、コメントを含むアイテムにドキュメントを追加します。通常、これらのドキュメントコメントは、クレート全体やモジュール全体をドキュメント化するために、クレートルートファイル(慣例的に_src/lib.rs_)やモジュール内部で使用します。
例えば、add_one 関数を含む my_crate クレートの目的を説明するドキュメントを追加するには、リスト14-2に示すように、//! で始まるドキュメンテーションコメントを src/lib.rs ファイルの先頭に追加します。
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.
/// Adds one to the number given.
// --snip--
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
//! で始まる最後の行の後にコードがないことに注目してください。/// ではなく //! でコメントを開始したため、このコメントに続くアイテムではなく、このコメントを含むアイテムをドキュメント化しています。この場合、そのアイテムはクレートルートである src/lib.rs ファイルです。これらのコメントはクレート全体を説明します。
cargo doc --open を実行すると、これらのコメントは図14-2に示すように、my_crate のドキュメントのフロントページで、クレート内の公開アイテムのリストの上に表示されます。
アイテム内のドキュメンテーションコメントは、特にクレートやモジュールを説明するのに役立ちます。これらを使用してコンテナの全体的な目的を説明し、ユーザーがクレートの構成を理解するのを助けます。
図14-2: クレート全体を説明するコメントを含む、my_crate のレンダリングされたドキュメント
便利な公開APIをエクスポートする
クレートを公開する際、公開APIの構造は主要な考慮事項です。クレートの利用者は、作者ほどその構造に詳しくなく、クレートが大規模なモジュール階層を持つ場合、使いたい部分を見つけるのに苦労するかもしれません。
第7章では、pubキーワードを使ってアイテムを公開する方法と、useキーワードを使ってアイテムをスコープに取り込む方法について説明しました。しかし、クレートを開発している間は理に適っている構造が、利用者にとってあまり便利でないかもしれません。あなたは構造体を複数レベルの階層で整理したいかもしれませんが、その階層の奥深くで定義した型を使いたい人は、その型が存在することを見つけ出すのに苦労するかもしれません。また、use my_crate::UsefulType; ではなく use my_crate::some_module::another_module::UsefulType; と入力しなければならないことにイライラするかもしれません。
良いニュースは、もしその構造が他のライブラリから使うのに便利でなくても、内部の構成を再編成する必要はないということです。代わりに、pub use を使うことでアイテムを再エクスポートし、プライベートな構造とは異なる公開用の構造を作ることができます。再エクスポートは、ある場所にある公開アイテムを取得し、あたかもその別の場所で定義されていたかのように、その場所で公開します。
例えば、芸術的な概念をモデリングするための art という名前のライブラリを作ったとします。このライブラリの中には2つのモジュールがあります。PrimaryColor と SecondaryColor という2つのenumを含む kinds モジュールと、mix という名前の関数を含む utils モジュールです。これをリスト14-3に示します。
//! # Art
//!
//! A library for modeling artistic concepts.
pub mod kinds {
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --snip--
unimplemented!();
}
}
図14-3は、cargo doc によって生成されたこのクレートのドキュメントのトップページがどのように見えるかを示しています。
図14-3: kinds と utils モジュールをリストアップした art のドキュメントのトップページ
PrimaryColor と SecondaryColor 型、そして mix 関数がトップページにリストされていないことに注意してください。それらを見るには kinds と utils をクリックする必要があります。
このライブラリに依存する別のクレートは、art からアイテムをスコープに取り込むために use 文を必要とし、現在定義されているモジュール構造を指定する必要があります。リスト14-4は、art クレートから PrimaryColor と mix アイテムを使用するクレートの例を示しています。
use art::kinds::PrimaryColor;
use art::utils::mix;
fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
art クレートを使用しているリスト14-4のコードの作者は、PrimaryColor が kinds モジュールに、mix が utils モジュールにあることを把握する必要がありました。art クレートのモジュール構造は、それを使用する人よりも、art クレートを開発している開発者にとってより意味のあるものです。内部構造は、art クレートの使い方を理解しようとしている人にとって有用な情報を含んでおらず、むしろ混乱を招きます。なぜなら、それを使う開発者はどこを探せばよいか把握する必要があり、use 文でモジュール名を指定しなければならないからです。
公開APIから内部構成を削除するために、リスト14-3の art クレートのコードを修正して、pub use 文を追加し、アイテムをトップレベルで再エクスポートすることができます。これをリスト14-5に示します。
//! # Art
//!
//! A library for modeling artistic concepts.
pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;
pub mod kinds {
// --snip--
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
// --snip--
use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
SecondaryColor::Orange
}
}
cargo doc がこのクレートのために生成するAPIドキュメントは、図14-4に示すように、トップページに再エクスポートをリストし、リンクするようになります。これにより、PrimaryColor と SecondaryColor 型、そして mix 関数がより見つけやすくなります。
図14-4: 再エクスポートをリストした art のドキュメントのトップページ
art クレートの利用者は、リスト14-4で示したように、リスト14-3の内部構造をまだ見たり使ったりすることができます。あるいは、リスト14-6で示すように、リスト14-5のより便利な構造を使うこともできます。
use art::PrimaryColor;
use art::mix;
fn main() {
// --snip--
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
ネストされたモジュールが多い場合、pub use で型をトップレベルに再エクスポートすることは、クレートを使用する人々の体験に大きな違いをもたらすことができます。pub use のもう一つの一般的な用途は、依存関係の定義を現在のクレートで再エクスポートし、そのクレートの定義を自分のクレートの公開APIの一部にすることです。
有用な公開APIの構造を作ることは、科学というよりは芸術であり、利用者に最適なAPIを見つけるためにイテレーションを重ねることができます。pub use を選択することで、クレートを内部的にどのように構造化するかに柔軟性が生まれ、その内部構造をユーザーに提示するものから切り離すことができます。インストールしたクレートのコードをいくつか見て、その内部構造が公開APIと異なっているかどうか確認してみてください。
Crates.ioアカウントのセットアップ
クレートを公開する前に、crates.ioでアカウントを作成し、APIトークンを取得する必要があります。そのためには、ホームページ crates.io にアクセスし、GitHubアカウントでログインします。(現時点ではGitHubアカウントが必須ですが、将来的にはサイトが他の方法でのアカウント作成をサポートするかもしれません。)ログインしたら、アカウント設定 https://crates.io/me/ にアクセスしてAPIキーを取得します。次に、cargo login コマンドを実行し、プロンプトが表示されたらAPIキーを貼り付けます。以下のようになります。
$ cargo login
abcdefghijklmnopqrstuvwxyz012345
このコマンドはCargoにあなたのAPIトークンを通知し、ローカルの ~/.cargo/credentials.toml に保存します。このトークンは秘密の情報であることに注意してください。他の誰とも共有しないでください。もし何らかの理由で誰かと共有してしまった場合は、crates.io でそれを無効化し、新しいトークンを生成すべきです。
新しいクレートにメタデータを追加する
公開したいクレートがあるとします。公開する前に、クレートの Cargo.toml ファイルの [package] セクションにいくつかのメタデータを追加する必要があります。
あなたのクレートには一意な名前が必要です。ローカルでクレートを開発している間は、好きな名前をクレートに付けることができます。しかし、[crates.io](https://crates.io/)<!-- ignore -->上のクレート名は先願主義で割り当てられます。一度クレート名が取得されると、他の誰もその名前でクレートを公開することはできません。クレートの公開を試みる前に、使用したい名前を検索してください。もしその名前が使われていたら、別の名前を見つけ、`[package]`セクション下の_Cargo.toml_ファイルの`name`フィールドを編集して、公開用に新しい名前を使用する必要があります。以下のようになります。
<span class="filename">ファイル名: Cargo.toml</span>
```toml
[package]
name = "guessing_game"
たとえ一意な名前を選んだとしても、この時点でcargo publishを実行してクレートを公開すると、警告とそれに続くエラーが表示されます。
$ cargo publish
Updating crates.io index
warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
--snip--
error: failed to publish to registry at https://crates.io
Caused by:
the remote server responded with an error (status 400 Bad Request): missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for more information on configuring these fields
これは、いくつかの重要な情報が欠落しているためにエラーになります。人々があなたのクレートが何をするものか、どのような条件で使用できるかを知るために、説明(description)とライセンス(license)が必要です。_Cargo.toml_には、検索結果であなたのクレートと共に表示されるため、1、2文程度の説明を追加してください。licenseフィールドには、_ライセンス識別子の値_を指定する必要があります。Linux FoundationのSoftware Package Data Exchange (SPDX)は、この値に使用できる識別子をリストアップしています。例えば、あなたのクレートをMITライセンスでライセンスしたことを指定するには、MIT識別子を追加します。
ファイル名: Cargo.toml
[package]
name = "guessing_game"
license = "MIT"
SPDXに載っていないライセンスを使用したい場合は、そのライセンスのテキストをファイルに配置し、そのファイルをプロジェクトに含め、licenseキーの代わりにlicense-fileを使用してそのファイルの名前を指定する必要があります。
どのライセンスがあなたのプロジェクトに適切かについてのガイダンスは、この本の範囲を超えています。Rustコミュニティの多くの人々は、MIT OR Apache-2.0のデュアルライセンスを使用して、Rustと同じ方法でプロジェクトをライセンスしています。この慣習は、ORで区切られた複数のライセンス識別子を指定して、プロジェクトに複数のライセンスを持たせることもできることを示しています。
一意な名前、バージョン、説明、そしてライセンスが追加され、公開準備ができたプロジェクトの_Cargo.toml_ファイルは次のようになります。
ファイル名: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"
[dependencies]
Cargoのドキュメントには、他の人があなたのクレートをより簡単に見つけて使用できるようにするために指定できる、その他のメタデータが記述されています。
Crates.ioへの公開
アカウントを作成し、APIトークンを保存し、クレートの名前を決め、必要なメタデータを指定したので、いよいよ公開です!クレートを公開すると、特定のバージョンがcrates.ioにアップロードされ、他の人が使用できるようになります。
公開は_恒久的_なものであるため、注意してください。バージョンは決して上書きできず、コードは特定の状況を除いて削除できません。Crates.ioの主要な目標の一つは、コードの恒久的なアーカイブとして機能し、crates.ioのクレートに依存するすべてのプロジェクトのビルドが機能し続けるようにすることです。バージョンの削除を許可すると、その目標を達成することが不可能になります。ただし、公開できるクレートのバージョン数に制限はありません。
cargo publishコマンドを再度実行してください。今度は成功するはずです。
$ cargo publish
Updating crates.io index
Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
Packaged 6 files, 1.2KiB (895.0B compressed)
Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
Compiling guessing_game v0.1.0
(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
Uploaded guessing_game v0.1.0 to registry `crates-io`
note: waiting for `guessing_game v0.1.0` to be available at registry
`crates-io`.
You may press ctrl-c to skip waiting; the crate should be available shortly.
Published guessing_game v0.1.0 at registry `crates-io`
おめでとうございます!これであなたはRustコミュニティとコードを共有し、誰でも簡単にあなたのクレートを自分のプロジェクトの依存関係として追加できます。
既存のクレートの新しいバージョンを公開する
クレートに変更を加えて新しいバージョンをリリースする準備ができたら、_Cargo.toml_ファイルで指定されているversionの値を変更して再公開します。行った変更の種類に基づいて、セマンティックバージョニングのルールを使用して、次のバージョン番号として適切なものを決定します。その後、cargo publishを実行して新しいバージョンをアップロードします。
Crates.ioからバージョンを非推奨にする
クレートの以前のバージョンを削除することはできませんが、将来のプロジェクトがそれらを新しい依存関係として追加するのを防ぐことはできます。これは、クレートのバージョンが何らかの理由で壊れている場合に便利です。そのような状況では、Cargoはクレートのバージョンのヤンク(yank)をサポートしています。
バージョンを_ヤンク_すると、新しいプロジェクトがそのバージョンに依存するのを防ぎつつ、それに依存する既存のすべてのプロジェクトは継続して動作できます。本質的に、ヤンクとは、_Cargo.lock_を持つすべてのプロジェクトが壊れることなく、将来生成される_Cargo.lock_ファイルはヤンクされたバージョンを使用しないことを意味します。
クレートのバージョンをヤンクするには、以前に公開したクレートのディレクトリで、cargo yankを実行し、どのバージョンをヤンクしたいかを指定します。例えば、guessing_gameという名前のクレートのバージョン1.0.1を公開し、それをヤンクしたい場合、guessing_gameのプロジェクトディレクトリで以下を実行します。
$ cargo yank --vers 1.0.1
Updating crates.io index
Yank guessing_game@1.0.1
コマンドに--undoを追加することで、yankを取り消して、プロジェクトが再びそのバージョンに依存できるようにすることもできます:
$ cargo yank --vers 1.0.1 --undo
Updating crates.io index
Unyank guessing_game@1.0.1
yankはどのコードも_削除しません_。例えば、誤ってアップロードしてしまった秘密情報を削除することはできません。もしそうなった場合は、それらの秘密情報を直ちにリセットしなければなりません。
Cargoワークスペース
Cargo ワークスペース
第12章では、バイナリクレートとライブラリクレートを含むパッケージを構築しました。プロジェクトの開発が進むにつれて、ライブラリクレートがどんどん大きくなり、パッケージをさらに複数のライブラリクレートに分割したくなるかもしれません。Cargo には workspaces と呼ばれる機能があり、並行して開発される複数の関連パッケージの管理に役立ちます。
ワークスペースを作成する
workspace とは、同じ Cargo.lock と出力ディレクトリを共有するパッケージの集合です。ワークスペースを使ったプロジェクトを作ってみましょう。ワークスペースの構造に集中できるように、コードは簡単なものを使います。ワークスペースの構成方法はいくつかありますが、ここでは一般的な方法のひとつだけを示します。バイナリ 1 つとライブラリ 2 つを含むワークスペースを用意します。メインの機能を提供するバイナリは、2 つのライブラリに依存します。1 つのライブラリは add_one 関数を提供し、もう 1 つのライブラリは add_two 関数を提供します。これら 3 つのクレートは同じワークスペースの一部になります。まず、ワークスペース用の新しいディレクトリを作成するところから始めましょう。
$ mkdir add
$ cd add
次に、add ディレクトリ内に、ワークスペース全体を設定する Cargo.toml ファイルを作成します。このファイルには [package] セクションはありません。代わりに、ワークスペースにメンバーを追加できるようにする [workspace] セクションから始まります。また、resolver の値を "3" に設定することで、Cargo の依存関係解決アルゴリズムの最新かつ最良のバージョンをワークスペースで使うようにしています。
ファイル名: Cargo.toml
[workspace]
resolver = "3"
次に、add ディレクトリ内で cargo new を実行して、adder バイナリクレートを作成します。
$ cargo new adder
Created binary (application) `adder` package
Adding `adder` as member of workspace at `file:///projects/add`
ワークスペース内で cargo new を実行すると、新しく作成されたパッケージは、ワークスペースの Cargo.toml にある [workspace] 定義の members キーにも自動的に追加されます。次のようになります。
[workspace]
resolver = "3"
members = ["adder"]
この時点で、cargo build を実行すればワークスペースをビルドできます。add ディレクトリ内のファイルは次のようになっているはずです。
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
ワークスペースには最上位レベルに 1 つの target ディレクトリがあり、コンパイルされた生成物はそこに配置されます。adder パッケージは独自の target ディレクトリを持ちません。たとえ adder ディレクトリの中から cargo build を実行したとしても、コンパイルされた生成物は add/adder/target ではなく add/target に出力されます。Cargo がワークスペース内の target ディレクトリをこのように構成するのは、ワークスペース内のクレート同士が相互に依存することを想定しているからです。各クレートが独自の target ディレクトリを持っていた場合、各クレートは生成物を自分の target ディレクトリに配置するために、ワークスペース内のほかの各クレートを毎回再コンパイルしなければなりません。1 つの target ディレクトリを共有することで、クレートは不要な再ビルドを避けられます。
ワークスペース内に 2 つ目のパッケージを作成する
次に、ワークスペース内に別のメンバーパッケージを作成し、add_one と名付けましょう。add_one という名前の新しいライブラリクレートを生成します。
$ cargo new add_one --lib
Created library `add_one` package
Adding `add_one` as member of workspace at `file:///projects/add`
最上位の Cargo.toml には、members リスト内に add_one のパスが含まれるようになります。
ファイル名: Cargo.toml
[workspace]
resolver = "3"
members = ["adder", "add_one"]
これで add ディレクトリには、次のディレクトリとファイルがあるはずです。
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
add_one/src/lib.rs ファイルに、add_one 関数を追加しましょう。
ファイル名: add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
これで、バイナリを持つ adder パッケージが、ライブラリを持つ add_one パッケージに依存できるようになります。まず、adder/Cargo.toml に add_one へのパス依存関係を追加する必要があります。
ファイル名: adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }
Cargo は、ワークスペース内のクレート同士が相互に依存することを前提にはしていないため、依存関係を明示的に記述する必要があります。
次に、adder クレートで add_one 関数(add_one クレートから提供されるもの)を使いましょう。adder/src/main.rs ファイルを開き、リスト14-7のように main 関数を変更して add_one 関数を呼び出します。
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
最上位の add ディレクトリで cargo build を実行して、ワークスペースをビルドしましょう。
$ cargo build
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s
add ディレクトリからバイナリクレートを実行するには、cargo run で -p 引数とパッケージ名を使って、ワークスペース内のどのパッケージを実行するかを指定できます。
$ cargo run -p adder
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
これにより、add_one クレートに依存している adder/src/main.rs のコードが実行されます。
外部パッケージに依存する
ワークスペースの最上位レベルには Cargo.lock ファイルが 1 つだけあり、
各クレートのディレクトリごとに Cargo.lock があるわけではないことに注目してください。これにより、
すべてのクレートがすべての依存関係について同じバージョンを使うことが保証されます。adder/Cargo.toml
ファイルと add_one/Cargo.toml ファイルに rand パッケージを追加すると、Cargo は
その両方を 1 つの rand のバージョンに解決し、その情報を単一の
Cargo.lock に記録します。ワークスペース内のすべてのクレートで同じ依存関係を
使うようにすると、クレート同士が常に互換性を保てるようになります。では、
add_one クレートで rand クレートを使えるように、add_one/Cargo.toml ファイルの
[dependencies] セクションに rand クレートを追加しましょう:
ファイル名: add_one/Cargo.toml
[dependencies]
rand = "0.8.5"
これで add_one/src/lib.rs ファイルに use rand; を追加できるようになり、add
ディレクトリで cargo build を実行してワークスペース全体をビルドすると、rand
クレートが取り込まれてコンパイルされます。スコープに取り込んだ rand を
参照していないため、警告が 1 つ表示されます:
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
--snip--
Compiling rand v0.8.5
Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
--> add_one/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s
最上位の Cargo.lock には、add_one の
rand への依存関係に関する情報が含まれるようになりました。しかし、rand が
ワークスペース内のどこかで使われているとしても、ほかのクレートで使うには
それらの Cargo.toml ファイルにも rand を追加しなければなりません。たとえば、adder
パッケージの adder/src/main.rs ファイルに use rand;
を追加すると、エラーになります:
$ cargo build
--snip--
Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
--> adder/src/main.rs:2:5
|
2 | use rand;
| ^^^^ no external crate `rand`
これを修正するには、adder パッケージの Cargo.toml ファイルを編集し、
rand がそれにとっても依存関係であることを示します。adder パッケージをビルドすると、
Cargo.lock 内の adder の依存関係一覧に rand が追加されますが、
rand の追加のコピーがダウンロードされることはありません。Cargo は、ワークスペース内の
すべてのパッケージにある、rand パッケージを使うすべてのクレートが、互換性のある
rand のバージョンを指定している限り、同じバージョンを使うことを保証してくれます。これにより、
容量を節約でき、ワークスペース内のクレート同士の互換性も確保されます。
ワークスペース内のクレートが同じ依存関係について互換性のないバージョンを 指定している場合、Cargo はそれぞれを解決しますが、それでも可能な限り 少ないバージョン数に収めようとします。
ワークスペースにテストを追加する
別の改善として、add_one クレート内に
add_one::add_one 関数のテストを追加してみましょう:
ファイル名: add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(3, add_one(2));
}
}
では、最上位の add ディレクトリで cargo test を実行します。このような構成の
ワークスペースで cargo test を実行すると、ワークスペース内のすべてのクレートに対する
テストが実行されます:
$ cargo test
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/adder-3a47283c568d2b6a)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
出力の最初のセクションは、add_one クレートの it_works テストが
通ったことを示しています。次のセクションは、adder
クレートではテストが 0 件見つかったことを示しており、最後のセクションは、
add_one クレートではドキュメントテストが 0 件見つかったことを示しています。
また、ワークスペース内の特定の 1 つのクレートに対するテストだけを、最上位の
ディレクトリから -p フラグを使い、テストしたいクレート名を指定して
実行することもできます:
$ cargo test -p add_one
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
この出力は、cargo test が add_one クレートのテストだけを実行し、
adder クレートのテストは実行しなかったことを示しています。
ワークスペース内のクレートを
crates.io に公開する場合、ワークスペース内の各クレートは
それぞれ個別に公開する必要があります。cargo test と同様に、-p
フラグを使って公開したいクレート名を指定することで、ワークスペース内の
特定のクレートを公開できます。
追加の練習として、add_one クレートと同様の方法で、このワークスペースに
add_two クレートを追加してみてください!
プロジェクトが大きくなってきたら、ワークスペースの利用を検討してください。ワークスペースを使うと、 大きな 1 つのコードの塊ではなく、より小さくて理解しやすいコンポーネントを扱えるようになります。 さらに、クレートが同時に変更されることが多い場合、ワークスペース内にまとめておくことで、 クレート間の調整もしやすくなります。
cargo install でバイナリをインストールする
cargo install を使ったバイナリのインストール
cargo install コマンドを使うと、バイナリクレートをローカルに
インストールして利用できます。これはシステムパッケージを置き換えること
を意図したものではなく、Rust 開発者が他の人々によって
crates.io で共有されたツールを手軽に
インストールするための便利な方法として提供されています。インストール
できるのは、バイナリターゲットを持つパッケージだけであることに注意して
ください。バイナリターゲット とは、クレートに src/main.rs ファイル
またはバイナリとして指定された別のファイルがある場合に作成される、実行
可能なプログラムのことです。これに対して、ライブラリターゲット は
単体では実行できませんが、他のプログラムに組み込むのに適しています。
通常、クレートがライブラリなのか、バイナリターゲットを持つのか、その
両方なのかについての情報は README ファイルに記載されています。
cargo install でインストールされたすべてのバイナリは、インストール
ルートの bin フォルダーに保存されます。Rust を rustup.rs で
インストールしていて、独自の設定をしていない場合、このディレクトリは
$HOME/.cargo/bin になります。このディレクトリが $PATH に含まれて
いることを確認し、cargo install でインストールしたプログラムを実行
できるようにしてください。
たとえば第 12 章では、ファイル検索用の grep ツールの Rust 実装として
ripgrep があることを説明しました。ripgrep をインストールするには、
次のように実行します:
$ cargo install ripgrep
Updating crates.io index
Downloaded ripgrep v14.1.1
Downloaded 1 crate (213.6 KB) in 0.40s
Installing ripgrep v14.1.1
--snip--
Compiling grep v0.3.2
Finished `release` profile [optimized + debuginfo] target(s) in 6.73s
Installing ~/.cargo/bin/rg
Installed package `ripgrep v14.1.1` (executable `rg`)
出力の最後から 2 行目には、インストールされたバイナリの場所と名前が
示されており、ripgrep の場合は rg です。前述のとおり、インストール
ディレクトリが $PATH に含まれていれば、その後 rg --help を実行して、
ファイル検索のための、より高速でより Rust らしいツールを使い始められます!
カスタムコマンドでCargoを拡張する
カスタムコマンドでCargoを拡張する
Cargoは、新しいサブコマンドで拡張できるよう設計されており、そのために
Cargo自体を変更する必要はありません。$PATH 内に cargo-something という
名前のバイナリがあれば、cargo something を実行することで、それをCargoの
サブコマンドであるかのように実行できます。このようなカスタムコマンドは、
cargo --list を実行したときにも一覧表示されます。cargo install を使って
拡張機能をインストールし、その後で組み込みのCargoツールとまったく同じように
実行できるのは、Cargoの設計による非常に便利な利点です!
まとめ
Cargoとcrates.ioでコードを共有できることは、 Rustのエコシステムが多種多様なタスクに役立つ理由の一部です。Rustの 標準ライブラリは小さく安定していますが、クレートは共有、利用、改善が容易で、 そのタイムラインは言語自体のものとは異なります。自分にとって役立つコードを crates.ioで共有することをためらわないでください。きっとそれは、ほかの誰かにとっても 役立つはずです!
スマートポインタ
ポインタは、メモリ内のアドレスを含む変数を指す一般的な概念です。このアドレスは、ほかの何らかのデータを参照し、つまり「指し示し」ます。Rust で最も一般的な種類のポインタは参照であり、これについては第4章で学びました。参照は & 記号で示され、指している値を借用します。参照には、データを参照する以外の特別な機能はなく、オーバーヘッドもありません。
一方、スマートポインタ は、ポインタのように振る舞いながら、追加のメタデータや機能も持つデータ構造です。スマートポインタという概念は Rust 固有のものではありません。スマートポインタは C++ に由来し、ほかの言語にも存在します。Rust には、参照が提供する機能を超える機能を提供するさまざまなスマートポインタが標準ライブラリに定義されています。一般的な概念を探るために、ここではいくつか異なるスマートポインタの例を見ていきます。その中には、参照カウント を行うスマートポインタ型も含まれます。このポインタは、所有者の数を追跡し、所有者がいなくなったときにデータをクリーンアップすることで、データが複数の所有者を持てるようにします。
Rust では、所有権と借用という概念があるため、参照とスマートポインタの間にはさらに別の違いがあります。参照はデータを借用するだけですが、スマートポインタは多くの場合、自分が指しているデータを_所有_します。
スマートポインタは通常、構造体を使って実装されます。通常の構造体とは異なり、スマートポインタは Deref トレイトと Drop トレイトを実装します。Deref トレイトにより、スマートポインタ構造体のインスタンスは参照のように振る舞えるため、参照にもスマートポインタにも対応するコードを書くことができます。Drop トレイトにより、スマートポインタのインスタンスがスコープを抜けるときに実行されるコードをカスタマイズできます。この章では、これら両方のトレイトについて説明し、それらがスマートポインタにとってなぜ重要なのかを示します。
スマートポインタのパターンは Rust で頻繁に使われる一般的な設計パターンであるため、この章では既存のすべてのスマートポインタを扱うわけではありません。多くのライブラリは独自のスマートポインタを持っており、自分でスマートポインタを書くこともできます。ここでは、標準ライブラリにある最も一般的なスマートポインタを取り上げます。
Box<T>: ヒープに値を確保するためのものRc<T>: 複数の所有権を可能にする参照カウント型Ref<T>とRefMut<T>:RefCell<T>を通じてアクセスされる型で、借用ルールをコンパイル時ではなく実行時に強制するもの
さらに、不変な型が内部の値を変更するための API を公開する 内部可変性 パターンについても扱います。また、参照サイクルについても説明します。参照サイクルがどのようにメモリリークを引き起こすのか、そしてそれをどう防ぐのかを見ていきます。
それでは始めましょう!
ヒープ上のデータを指すために Box<T> を使う
Box<T> を使ってヒープ上のデータを指す
最も単純なスマートポインタはボックスで、その型は
Box<T> と書きます。ボックス を使うと、データをスタックではなくヒープに格納できます。
スタックに残るのは、ヒープ上のデータへのポインタです。スタックとヒープの違いを振り返るには、第4章を参照してください。
ボックスには、データをスタックではなくヒープに格納すること以外の パフォーマンス上のオーバーヘッドはありません。しかし、追加の機能も それほど多くはありません。最もよく使うのは、次のような場合です。
- サイズをコンパイル時に知ることができない型があり、その型の値を 正確なサイズが必要なコンテキストで使いたい場合
- 大量のデータがあり、所有権を移動したいが、 その際にデータがコピーされないようにしたい場合
- 値を所有したいが、具体的な型であることではなく、 特定のトレイトを実装している型であることだけが重要な場合
最初のケースは 「ボックスで再帰型を可能にする」 で示します。2番目の ケースでは、大量のデータの所有権を移動するのに長い時間がかかることが あります。これは、データがスタック上でコピーされるためです。この 状況でパフォーマンスを改善するには、大量のデータをボックスでヒープ上に 格納できます。そうすれば、スタック上でコピーされるのは少量のポインタ データだけで、そのポインタが参照するデータはヒープ上の同じ場所に とどまります。3番目のケースは トレイトオブジェクト として知られており、 第18章の「トレイトオブジェクトを使って共有された振る舞いを抽象化する」 でこの話題を扱います。 ですから、ここで学ぶことはその節でもまた活用することになります。
ヒープにデータを格納する
Box<T> をヒープに格納する用途について説明する前に、構文と、
Box<T> の中に格納された値をどのように扱うかを見ていきます。
リスト15-1は、ボックスを使って i32 値をヒープに格納する方法を示しています。
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
b 変数は、ヒープに確保された値 5 を指す Box の値を持つように
定義しています。このプログラムは b = 5 を出力します。この場合、
ボックス内のデータには、そのデータがスタック上にある場合と同様に
アクセスできます。他のあらゆる所有権を持つ値と同様に、b が main の
末尾でそうなるように、ボックスがスコープを抜けると、それは解放されます。
解放は、ボックス自体(スタックに格納)と、それが指すデータ
(ヒープに格納)の両方に対して行われます。
単一の値をヒープに置くだけではあまり有用ではないので、このような形で
ボックス単体を使うことはあまりありません。i32 のような単一の値は、
デフォルトで格納されるスタック上に置いておくほうが、大半の状況では
より適切です。次に、ボックスがあることで、ボックスがなければ
定義できない型を定義できるケースを見てみましょう。
ボックスで再帰型を可能にする
再帰型 の値は、自身の一部として同じ型の別の値を持つことができます。 再帰型は、Rust が型の占有する領域の大きさをコンパイル時に知っておく必要が あるため、問題になります。しかし、再帰型の値の入れ子は理論上無限に 続く可能性があるため、Rust はその値にどれだけの領域が必要かを知ることが できません。ボックスはサイズが既知なので、再帰型の定義にボックスを 挿入することで再帰型を可能にできます。
再帰型の例として、コンスリストを見ていきましょう。これは関数型 プログラミング言語でよく見られるデータ型です。これから定義する コンスリスト型は、再帰であることを除けば単純です。そのため、 これから扱う例の概念は、再帰型が関わるより複雑な状況に取り組むときにも 役立ちます。
コンスリストを理解する
コンスリスト は Lisp プログラミング言語とその方言に由来するデータ構造で、
入れ子になったペアから構成される、Lisp 版の連結リストです。その名前は、
2つの引数から新しいペアを構築する Lisp の cons 関数(construct
function の略)に由来します。値と別のペアからなるペアに対して
cons を呼び出すことで、再帰的なペアからなるコンスリストを
構築できます。
たとえば、各ペアを括弧で囲んだ、リスト 1, 2, 3 を含むコンスリストの
疑似コード表現は次のようになります。
(1, (2, (3, Nil)))
コンスリストの各要素には2つの要素があります。現在の要素の値と、
次の要素です。リストの最後の要素には、次の要素を持たず、
Nil と呼ばれる値だけが含まれます。コンスリストは、cons
関数を再帰的に呼び出すことで作られます。再帰のベースケースを表す
慣例的な名前が Nil です。これは第6章で議論した「null」や「nil」の
概念、つまり無効な値や値が存在しないことを表すものとは同じではないことに
注意してください。
コンスリストは、Rust では一般的に使われるデータ構造ではありません。
Rust で項目のリストを扱う場合、たいていは Vec<T> を使うほうが
適切です。ほかの、より複雑な再帰データ型はさまざまな状況で 実際に
有用ですが、この章ではコンスリストから始めることで、余計なことに
気を取られずに、ボックスによって再帰データ型をどのように定義できるかを
探れます。
リスト15-2には、コンスリストの enum 定義が含まれています。このコードは
まだコンパイルできないことに注意してください。というのも、List 型の
サイズが既知ではないためで、それをこれから示します。
enum List {
Cons(i32, List),
Nil,
}
fn main() {}
注: この例の都合上、
i32値だけを保持するコンスリストを実装しています。 第10章で議論したようにジェネリクスを使って実装し、任意の型の値を格納できる コンスリスト型を定義することもできました。
リスト 1, 2, 3 を格納するために List 型を使うと、リスト15-3のようなコードになります。
enum List {
Cons(i32, List),
Nil,
}
// --snip--
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
最初の Cons 値は 1 と別の List 値を保持します。この List 値は
2 と別の List 値を保持する別の Cons 値です。この List 値は
さらにもう1つの Cons 値で、3 と List 値を保持しており、その
List 値は最終的に、リストの終端を示す非再帰的なバリアントである
Nil になります。
リスト15-3のコードをコンパイルしようとすると、リスト15-4に示すエラーが出ます。
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
error[E0391]: cycle detected when computing when `List` needs drop
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^
|
= note: ...which immediately requires computing when `List` needs drop again
= note: cycle used when computing whether `List` needs drop
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors
このエラーは、この型が「無限のサイズを持つ」ことを示しています。その理由は、List を再帰的なバリアントを持つように定義しているからです。つまり、そのバリアントは自分自身の別の値を直接保持しています。その結果、Rust は List 型の値を格納するのにどれだけの領域が必要かを判断できません。なぜこのエラーが発生するのかを分解して見ていきましょう。まず、Rust が非再帰型の値を格納するのにどれだけの領域が必要かをどのように決めるのかを見ていきます。
非再帰型のサイズを計算する
enum の定義について第 6 章で説明したときに、Listing 6-2 で定義した Message enum を思い出してください。
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {}
Message 型の値にどれだけの領域を割り当てるかを決めるために、Rust は各バリアントを調べ、どのバリアントがもっとも大きな領域を必要とするかを確認します。Rust は、Message::Quit には領域がまったく不要であり、Message::Move には 2 つの i32 値を格納するのに十分な領域が必要であり、という具合に判断します。使われるバリアントは 1 つだけなので、Message 型の値が必要とする最大の領域は、そのバリアントのうちもっとも大きいものを格納するのに必要な領域になります。
これに対して、Rust が Listing 15-2 の List enum のような再帰型にどれだけの領域が必要かを判断しようとすると、事情は異なります。コンパイラはまず Cons バリアントを調べます。このバリアントは、型 i32 の値と型 List の値を保持しています。したがって、Cons に必要な領域の大きさは、i32 のサイズと List のサイズの合計に等しくなります。List 型にどれだけのメモリが必要かを調べるために、コンパイラはバリアントを調べますが、そこでも Cons バリアントから始まります。Cons バリアントは、型 i32 の値と型 List の値を保持しており、この過程は図 15-1 に示すように無限に続きます。
図 15-1: 無限の Cons バリアントから成る無限の List
既知のサイズを持つ再帰型を得る
Rust は再帰的に定義された型にどれだけの領域を割り当てればよいかを判断できないため、コンパイラは次のような役に立つ提案を含むエラーを出します。
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
この提案でいう indirection とは、値を直接格納する代わりに、その値へのポインタを格納することで間接的に値を格納するよう、データ構造を変更すべきだという意味です。
Box<T> はポインタであるため、Rust は Box<T> にどれだけの領域が必要かを常に把握しています。ポインタのサイズは、それが指しているデータ量に応じて変化しないからです。つまり、Cons バリアントには別の List の値を直接入れる代わりに、その中に Box<T> を入れることができます。Box<T> は、Cons バリアントの内側ではなく、ヒープ上に置かれる次の List の値を指します。概念的には、依然としてリストであり、ほかのリストを保持するリストによって作られていますが、この実装は、要素を互いの内側に入れるというより、互いの隣に配置するのに近くなります。
Listing 15-2 にある List enum の定義と、Listing 15-3 にある List の使い方を、Listing 15-5 のコードに変更できます。このコードはコンパイルできます。
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Cons バリアントには、i32 のサイズと、ボックスのポインタデータを格納するための領域が必要です。Nil バリアントは値を一切格納しないため、スタック上で必要な領域は Cons バリアントよりも小さくなります。これで、どの List 型の値も i32 のサイズとボックスのポインタデータのサイズを合わせた大きさになることがわかります。ボックスを使うことで、無限に続く再帰的な連鎖を断ち切ったため、コンパイラは List 型の値を格納するのに必要なサイズを判断できるようになります。図 15-2 は、現在の Cons バリアントがどのようになっているかを示しています。
図 15-2: Cons が Box を保持するため、サイズが無限ではない List
ボックスが提供するのは、間接参照とヒープ割り当てだけです。これから見るほかのスマートポインタ型が持つような、ほかの特別な機能はありません。また、そうした特別な機能に伴うパフォーマンスオーバーヘッドもありません。そのため、間接参照だけが必要な cons リストのようなケースでは役に立ちます。ボックスのさらに多くのユースケースについては、第 18 章で見ていきます。
Box<T> 型はスマートポインタです。これは、Box<T> の値を参照のように扱えるようにする Deref トレイトを実装しているからです。Box<T> の値がスコープを抜けると、そのボックスが指しているヒープ上のデータも、Drop トレイトの実装によってクリーンアップされます。これら 2 つのトレイトは、この章の残りで説明するほかのスマートポインタ型が提供する機能において、さらに重要になります。では、これら 2 つのトレイトをもう少し詳しく見ていきましょう。
スマートポインタを通常の参照のように扱う
スマートポインタを通常の参照のように扱う
Deref トレイトを実装すると、デリファレンス演算子 * の動作をカスタマイズできます(乗算演算子やグロブ演算子と混同しないでください)。スマートポインタが通常の参照のように扱えるような形で Deref を実装すれば、参照に対して動作するコードを書き、そのコードをスマートポインタに対しても使えるようになります。
まず、通常の参照に対してデリファレンス演算子がどのように動作するのかを見ていきましょう。次に、Box<T> のように振る舞う独自の型を定義し、新しく定義した型に対してデリファレンス演算子が参照と同じようには機能しない理由を確認します。さらに、Deref トレイトを実装することで、スマートポインタが参照に似た方法で動作できるようになる仕組みを見ていきます。その後、Rust の deref coercion 機能と、それによって参照とスマートポインタのどちらも扱えるようになる方法を見ていきます。
参照をたどって値に到達する
通常の参照はポインタの一種であり、ポインタは別の場所に格納されている値を指し示す矢印のようなものだと考えることができます。リスト 15-6 では、i32 の値への参照を作成し、その後デリファレンス演算子を使って参照をたどり、その値に到達します。
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
変数 x は i32 の値 5 を保持しています。y には x への参照を代入します。x が 5 に等しいことはアサートできます。しかし、y の中の値についてアサートしたい場合は、*y を使って参照をたどり、それが指している値に到達する必要があります(したがって、dereference です)。そうすることで、コンパイラは実際の値を比較できます。y をデリファレンスすると、y が指している整数値にアクセスでき、それを 5 と比較できます。
代わりに assert_eq!(5, y); と書こうとすると、次のコンパイルエラーが発生します。
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
数値と、その数値への参照は型が異なるため、比較することはできません。参照が指している値に到達するには、デリファレンス演算子を使う必要があります。
Box<T> を参照のように使う
リスト 15-6 のコードは、参照の代わりに Box<T> を使うように書き換えることができます。リスト 15-7 で Box<T> に対して使われているデリファレンス演算子は、リスト 15-6 で参照に対して使われていたものと同じように機能します。
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
リスト 15-7 とリスト 15-6 の主な違いは、ここでは y を、x の値を指す参照ではなく、x のコピーされた値を指すボックスのインスタンスに設定している点です。最後のアサーションでは、y が参照だったときと同じように、デリファレンス演算子を使ってボックスのポインタをたどることができます。次に、独自のボックス型を定義することで、Box<T> のどこが特別で、デリファレンス演算子を使えるようにしているのかを見ていきます。
独自のスマートポインタを定義する
デフォルトでは、スマートポインタ型が参照とは異なる振る舞いをすることを体験するために、標準ライブラリが提供する Box<T> 型に似たラッパー型を作ってみましょう。その後、デリファレンス演算子を使えるようにする方法を見ていきます。
注: これから作る
MyBox<T>型と実際のBox<T>には、大きな違いが 1 つあります。私たちのバージョンは、そのデータをヒープに格納しません。この例ではDerefに焦点を当てているため、データが実際にどこに格納されるかは、ポインタのような振る舞いほど重要ではありません。
Box<T> 型は最終的には 1 つの要素を持つタプル構造体として定義されているため、リスト 15-8 でも同じように MyBox<T> 型を定義します。また、Box<T> に定義されている new 関数に対応する new 関数も定義します。
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {}
MyBox という名前の構造体を定義し、任意の型の値を保持できるように、ジェネリックパラメータ T を宣言しています。MyBox 型は、型 T の 1 つの要素を持つタプル構造体です。MyBox::new 関数は、型 T の引数を 1 つ受け取り、その渡された値を保持する MyBox インスタンスを返します。
それでは、リスト 15-7 の main 関数をリスト 15-8 に追加し、Box<T> の代わりに、私たちが定義した MyBox<T> 型を使うように変更してみましょう。リスト 15-9 のコードはコンパイルされません。なぜなら、Rust は MyBox をどのようにデリファレンスすればよいかを知らないからです。
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
その結果のコンパイルエラーは次のとおりです。
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^ can't be dereferenced
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
MyBox<T> 型は、その機能を型に実装していないため、デリファレンスできません。* 演算子によるデリファレンスを有効にするには、Deref トレイトを実装します。
Deref トレイトを実装する
第 10 章の [「型にトレイトを実装する」][impl-trait] で説明したように、トレイトを実装するには、そのトレイトが要求するメソッドの実装を提供する必要があります。標準ライブラリが提供する Deref トレイトでは、self を借用し、内部のデータへの参照を返す deref という名前のメソッドを 1 つ実装する必要があります。リスト 15-10 には、MyBox<T> の定義に追加する Deref の実装が含まれています。
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
type Target = T; という構文は、Deref トレイトが使う関連型を定義しています。関連型は、ジェネリックパラメータを宣言するための少し異なる方法ですが、今のところ気にする必要はありません。これについては第 20 章でさらに詳しく扱います。
`deref`メソッドの本体を`&self.0`で埋めることで、`deref`は`*`演算子でアクセスしたい値への参照を返すようになります。[第5章の[「タプル構造体で異なる型を作成する」][tuple-structs]]<!--
無視 -->で見たように、`.0`はタプル構造体の最初の値にアクセスします。これで、リスト15-9の`MyBox<T>`値に対して`*`を呼び出している`main`関数はコンパイルされ、アサーションも通ります!
`Deref`トレイトがなければ、コンパイラがデリファレンスできるのは`&`参照だけです。`deref`メソッドは、`Deref`を実装する任意の型の値を受け取り、`deref`メソッドを呼び出して、コンパイラがデリファレンス方法を知っている参照を得る能力をコンパイラに与えます。
リスト15-9で`*y`と入力したとき、Rustは実際には裏で次のコードを実行していました。
```rust,ignore
*(y.deref())
Rustは*演算子をderefメソッドの呼び出しと、その後の通常のデリファレンスに置き換えるため、derefメソッドを呼び出す必要があるかどうかを私たちが意識する必要はありません。このRustの機能により、通常の参照を持っている場合でも、Derefを実装した型を持っている場合でも、同じように機能するコードを書けます。
derefメソッドが値への参照を返し、*(y.deref())の括弧の外側にある通常のデリファレンスが依然として必要である理由は、所有権システムに関係しています。もしderefメソッドが値への参照ではなく値そのものを直接返した場合、その値はselfからムーブされてしまいます。この場合も、デリファレンス演算子を使うほとんどの場合も、MyBox<T>の内側にある値の所有権を取得したいわけではありません。
*演算子は、コード内で*を使うたびに、derefメソッドの呼び出し、続いて*演算子の呼び出しへと1回だけ置き換えられることに注意してください。*演算子の置き換えは無限に再帰しないため、最終的にはi32型のデータが得られ、これはリスト15-9のassert_eq!内の5と一致します。
関数とメソッドでDeref型強制を使う
Deref型強制 は、Derefトレイトを実装した型への参照を、別の型への参照に変換します。たとえば、Stringは&strを返すようにDerefトレイトを実装しているので、Deref型強制は&Stringを&strに変換できます。Deref型強制は、Rustが関数やメソッドへの引数に対して行う便利な機能であり、Derefトレイトを実装している型に対してのみ働きます。これは、ある特定の型の値への参照を、関数またはメソッドの定義にあるパラメータ型と一致しない形で引数として渡したときに自動的に発生します。derefメソッドの呼び出しを連続して行うことで、私たちが渡した型が、パラメータが必要とする型へと変換されます。
Deref型強制は、関数呼び出しやメソッド呼び出しを書くプログラマが、&や*による明示的な参照やデリファレンスをそれほど多く追加しなくて済むようにするためにRustに追加されました。Deref型強制の機能により、参照でもスマートポインタでも動作する、より多くのコードを書けるようにもなります。
Deref型強制が実際にどう動くかを見るために、リスト15-8で定義したMyBox<T>型と、リスト15-10で追加したDerefの実装を使いましょう。リスト15-11は、文字列スライスをパラメータに取る関数の定義を示しています。
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {}
たとえば、引数として文字列スライスを使ってhello関数を呼び出すことができ、hello("Rust");のように書けます。Deref型強制により、リスト15-12に示すように、MyBox<String>型の値への参照を使ってhelloを呼び出すことも可能になります。
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
ここでは、引数&mでhello関数を呼び出しています。これはMyBox<String>値への参照です。リスト15-10でMyBox<T>に対してDerefトレイトを実装したので、Rustはderefを呼び出すことで&MyBox<String>を&Stringに変換できます。標準ライブラリはStringに対するDerefの実装を提供しており、それは文字列スライスを返します。このことはDerefのAPIドキュメントにあります。Rustはさらにもう一度derefを呼び出して&Stringを&strに変換し、これがhello関数の定義に一致します。
もしRustがDeref型強制を実装していなければ、&MyBox<String>型の値を使ってhelloを呼び出すには、リスト15-12のコードではなく、リスト15-13のコードを書かなければならないでしょう。
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
(*m)はMyBox<String>をデリファレンスしてStringにします。次に、&と[..]はString全体に等しいStringの文字列スライスを取り出し、helloのシグネチャに一致させます。Deref型強制がないこのコードは、これらすべての記号が関わるため、読むのも、書くのも、理解するのも難しくなります。Deref型強制により、Rustはこれらの変換を自動的に処理してくれます。
関係する型に対してDerefトレイトが定義されている場合、Rustは型を解析し、パラメータの型に一致する参照を得るために必要な回数だけDeref::derefを使います。Deref::derefを何回挿入する必要があるかはコンパイル時に解決されるため、Deref型強制を活用しても実行時のペナルティはありません!
可変参照に対するDeref型強制の扱い
不変参照に対してDerefトレイトを使って*演算子をオーバーライドするのと同様に、可変参照に対してDerefMutトレイトを使って*演算子をオーバーライドできます。
Rustは、次の3つのケースで型とトレイト実装を見つけるとDeref型強制を行います。
T: Deref<Target=U>のとき、&Tから&UへT: DerefMut<Target=U>のとき、&mut Tから&mut UへT: Deref<Target=U>のとき、&mut Tから&Uへ
最初の2つのケースは、2番目が可変性を実装するという点を除けば同じです。最初のケースは、&Tを持っていて、Tが何らかの型UへのDerefを実装しているなら、透過的に&Uを得られることを述べています。2番目のケースは、同じDeref型強制が可変参照でも起こることを述べています。
3つ目のケースはさらに厄介です。Rust は可変参照を不変参照にも
型強制します。しかし、逆は _不可能_ です。不変参照が可変参照に
型強制されることは決してありません。借用規則のため、可変参照を
持っているなら、その可変参照はそのデータへの唯一の参照でなければ
なりません(そうでなければ、プログラムはコンパイルされません)。
1つの可変参照を1つの不変参照に変換しても、借用規則が破られることは
決してありません。不変参照を可変参照に変換するには、最初の不変参照がその
データへの唯一の不変参照である必要がありますが、借用規則はそれを
保証しません。したがって、Rust は不変参照を可変参照に変換できると
仮定することはできません。
[impl-trait]: ch10-02-traits.html#implementing-a-trait-on-a-type
[tuple-structs]: ch05-01-defining-structs.html#creating-different-types-with-tuple-structs
Drop トレイトでクリーンアップ時にコードを実行する
Drop トレイトでクリーンアップ時にコードを実行する
スマートポインタパターンで重要な 2 つ目のトレイトは Drop です。これにより、
値がスコープを抜けようとするときに何が起こるかをカスタマイズできます。任意の型に
対して Drop トレイトの実装を提供でき、そのコードはファイルやネットワーク接続などの
リソースを解放するために使えます。
ここで Drop をスマートポインタの文脈で導入するのは、Drop トレイトの機能が
スマートポインタを実装するときにほとんど常に使われるからです。たとえば、Box<T> が
ドロップされると、box が指しているヒープ上の領域が解放されます。
一部の言語では、一部の型に対して、その型のインスタンスの使用を終えるたびに、 メモリやリソースを解放するコードをプログラマが呼び出さなければなりません。例としては、 ファイルハンドル、ソケット、ロックなどがあります。プログラマが忘れると、システムに 過剰な負荷がかかってクラッシュする可能性があります。Rust では、値がスコープを抜ける たびに特定のコードを実行するよう指定でき、コンパイラがこのコードを自動的に挿入します。 その結果、特定の型のインスタンスの使用が終わるたびに、プログラムのあらゆる場所へ クリーンアップコードを配置することに気を配る必要はありません。それでもリソースリークは 起きません。
値がスコープを抜けるときに実行するコードは、Drop トレイトを実装することで指定します。
Drop トレイトでは、self への可変参照を受け取る drop という名前のメソッドを
1 つ実装する必要があります。Rust がいつ drop を呼ぶのかを見るために、いまは
println! 文を使って drop を実装してみましょう。
リスト 15-14 は CustomSmartPointer 構造体を示しています。この構造体の唯一の
カスタム機能は、インスタンスがスコープを抜けるときに Dropping CustomSmartPointer! を
表示することです。これにより、Rust がいつ drop メソッドを実行するのかを示しています。
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created");
}
Drop トレイトは prelude に含まれているので、スコープに持ち込む必要はありません。
CustomSmartPointer に対して Drop トレイトを実装し、println! を呼び出す
drop メソッドの実装を提供しています。drop メソッドの本体には、自分の型の
インスタンスがスコープを抜けるときに実行したい任意のロジックを配置します。ここでは、
Rust がいつ drop を呼ぶのかを視覚的に示すために、いくつかのテキストを出力しています。
main では、CustomSmartPointer のインスタンスを 2 つ作成し、その後
CustomSmartPointers created を表示します。main の終わりで、
CustomSmartPointer のインスタンスはスコープを抜け、Rust は drop メソッドに
書いたコードを呼び出して、最後のメッセージを表示します。drop メソッドを明示的に
呼び出す必要がなかったことに注目してください。
このプログラムを実行すると、次のような出力が表示されます。
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
Running `target/debug/drop-example`
CustomSmartPointers created
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
インスタンスがスコープを抜けたとき、Rust は自動的に drop を呼び出し、私たちが
指定したコードを実行しました。変数は作成された順序の逆順でドロップされるため、
d は c より先にドロップされました。この例の目的は、drop メソッドがどのように
動作するかを視覚的に示すことです。通常は、出力メッセージではなく、自分の型で
実行する必要があるクリーンアップコードを指定することになるでしょう。
残念ながら、自動的な drop の機能を無効にするのは簡単ではありません。通常、
drop を無効にする必要はありません。Drop トレイトの要点は、それが自動的に
処理されることにあるからです。ただし、値を早めにクリーンアップしたい場合もあります。
一例として、ロックを管理するスマートポインタを使っているときが挙げられます。同じ
スコープ内のほかのコードがロックを取得できるように、ロックを解放する drop
メソッドを強制的に実行したいことがあるかもしれません。Rust では Drop トレイトの
drop メソッドを手動で呼び出すことはできません。その代わり、スコープの終わりより前に
値を強制的にドロップしたい場合は、標準ライブラリが提供する std::mem::drop
関数を呼び出す必要があります。
リスト 15-14 の main 関数を変更して Drop トレイトの drop メソッドを手動で
呼び出そうとしても、リスト 15-15 に示すようにうまくいきません。
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created");
c.drop();
println!("CustomSmartPointer dropped before the end of main");
}
このコードをコンパイルしようとすると、次のエラーが表示されます。
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
--> src/main.rs:16:7
|
16 | c.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
16 - c.drop();
16 + drop(c);
|
For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` (bin "drop-example") due to 1 previous error
このエラーメッセージは、drop を明示的に呼び出すことは許されていないと述べています。
このエラーメッセージでは、destructor という用語が使われています。これは、
インスタンスをクリーンアップする関数を指す一般的なプログラミング用語です。
destructor は、インスタンスを生成する constructor に対応するものです。
Rust の drop 関数は、ある特定のデストラクタです。
Rust が drop を明示的に呼び出せないようにしているのは、Rust が main の終わりでも
その値に対して自動的に drop を呼ぶからです。そうすると、Rust が同じ値を 2 回
クリーンアップしようとするため、二重解放エラーが発生します。
値がスコープを抜けるときに drop が自動挿入される仕組みは無効にできず、drop
メソッドを明示的に呼び出すこともできません。したがって、値を早めにクリーンアップする
必要がある場合は、std::mem::drop 関数を使います。
std::mem::drop 関数は、Drop トレイトの drop メソッドとは異なります。
強制的にドロップしたい値を引数として渡して呼び出します。この関数は prelude に
含まれているので、リスト 15-15 の main を変更して drop 関数を呼び出すことが
できます。これはリスト 15-16 に示されています。
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created");
drop(c);
println!("CustomSmartPointer dropped before the end of main");
}
このコードを実行すると、次のように表示されます。
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/drop-example`
CustomSmartPointer created
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main
Dropping CustomSmartPointer with data `some data`! というテキストが、
CustomSmartPointer created と CustomSmartPointer dropped before the end of main
のテキストの間に表示されます。これにより、その時点で c をドロップするために
drop メソッドのコードが呼び出されていることがわかります。
Drop トレイト実装で指定したコードは、クリーンアップを便利かつ安全にするために
さまざまな方法で利用できます。たとえば、独自のメモリアロケータを作るためにも
利用できます。Drop トレイトと Rust の所有権システムにより、Rust が自動的に
クリーンアップしてくれるので、自分でそれを覚えておく必要はありません。
また、まだ使われている値を誤ってクリーンアップしてしまうことで生じる問題についても
心配する必要はありません。参照が常に有効であることを保証する所有権システムは、
値がもう使われなくなったときに drop が 1 回だけ呼ばれることも保証します。
ここまでで Box<T> とスマートポインタのいくつかの特徴を見てきたので、次は標準
ライブラリで定義されている別のスマートポインタをいくつか見ていきましょう。
Rc<T>、参照カウント方式のスマートポインタ
Rc<T>、参照カウント方式のスマートポインタ
ほとんどの場合、所有権は明確です。つまり、ある値をどの変数が所有しているのかが正確にわかります。しかし、1つの値に複数の所有者がいる場合もあります。たとえばグラフデータ構造では、複数のエッジが同じノードを指すことがあり、そのノードは概念的にはそれを指しているすべてのエッジに所有されています。ノードを指しているエッジがなくなり、所有者がいなくならないかぎり、そのノードは解放されるべきではありません。
複数の所有権を明示的に有効にするには、Rust の型 Rc<T> を使う必要があります。これは 参照カウント の略です。Rc<T> 型は、ある値への参照の数を追跡し、その値がまだ使用中かどうかを判断します。値への参照が 0 なら、その値はどの参照も無効にすることなく解放できます。
Rc<T> を、家族が使う居間のテレビだと考えてみてください。1人がテレビを見に部屋に入ったら、その人がテレビをつけます。ほかの人も部屋に入ってきて、そのテレビを見ることができます。最後の1人が部屋を出るとき、そのテレビはもう使われていないので消します。ほかの人がまだ見ているのに誰かがテレビを消したら、残っている視聴者は大騒ぎになるでしょう!
Rc<T> 型は、プログラムの複数の部分から読み取るためのデータをヒープに確保したいものの、そのデータの使用を最後に終えるのがどの部分なのかをコンパイル時に判断できない場合に使います。最後に終える部分がわかっているなら、その部分をデータの所有者にすればよく、コンパイル時に適用される通常の所有権規則がそのまま働きます。
Rc<T> はシングルスレッドのシナリオでのみ使用するものだという点に注意してください。第16章で並行性について説明するときに、マルチスレッドプログラムで参照カウントを行う方法を扱います。
データの共有
Listing 15-5 の cons リストの例に戻りましょう。これを Box<T> を使って定義したことを思い出してください。今回は、3つ目のリストの所有権を共有する2つのリストを作成します。概念的には、これは図15-3 に似ています。
図15-3: 2つのリスト b と c が、
3つ目のリスト a の所有権を共有している
5、続いて 10 を含むリスト a を作成します。次に、さらに2つのリストを作ります。3 で始まる b と、4 で始まる c です。その後、b と c の両方のリストは、5 と 10 を含む最初の a リストへと続きます。言い換えると、両方のリストが 5 と 10 を含む最初のリストを共有することになります。
Listing 15-17 に示すように、Box<T> を使った List の定義でこのシナリオを実装しようとしても、うまくいきません。
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
このコードをコンパイルすると、次のエラーが出ます。
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
|
note: if `List` implemented `Clone`, you could clone the value
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ consider implementing `Clone` for this type
...
10 | let b = Cons(3, Box::new(a));
| - you could clone this value
For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
Cons バリアントは保持しているデータを所有しているため、b リストを作成するときに a は b にムーブされ、b が a を所有します。そのため、c を作成するときに再び a を使おうとしても、a はすでにムーブされているので許可されません。
代わりに参照を保持するように Cons の定義を変更することもできますが、そうするとライフタイム引数を指定しなければなりません。ライフタイム引数を指定するということは、リスト内のすべての要素が少なくともリスト全体と同じだけ生きることを指定することになります。これは Listing 15-17 の要素やリストについては成り立ちますが、あらゆるシナリオでそうであるとは限りません。
その代わりに、Listing 15-18 に示すように、List の定義を Box<T> の代わりに Rc<T> を使うよう変更します。これで各 Cons バリアントは、値と、List を指す Rc<T> を保持するようになります。b を作るときには、a の所有権を奪う代わりに、a が保持している Rc<List> をクローンします。これにより参照の数は 1 から 2 に増え、a と b がその Rc<List> 内のデータの所有権を共有できるようになります。c を作るときにも a をクローンするので、参照の数は 2 から 3 に増えます。Rc::clone を呼び出すたびに、Rc<List> 内のデータへの参照カウントが増加し、参照が 0 にならないかぎりデータは解放されません。
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
Rc<T> は prelude に含まれていないため、スコープに導入する use 文を追加する必要があります。main では、5 と 10 を保持するリストを作成し、それを新しい Rc<List> として a に格納します。その後、b と c を作成するときに、Rc::clone 関数を呼び出し、a に入っている Rc<List> への参照を引数として渡します。
Rc::clone(&a) ではなく a.clone() と呼ぶこともできましたが、この場合は Rc::clone を使うのが Rust の慣例です。Rc::clone の実装は、多くの型の clone 実装のようにすべてのデータをディープコピーしません。Rc::clone の呼び出しは参照カウントを増やすだけで、ほとんど時間がかかりません。データのディープコピーには多くの時間がかかることがあります。参照カウントのために Rc::clone を使うことで、ディープコピーを行う種類のクローンと、参照カウントを増やす種類のクローンを見た目で区別できます。コード中の性能上の問題を探すときは、ディープコピーを行うクローンだけを考慮すればよく、Rc::clone の呼び出しは無視できます。
クローンして参照カウントを増やす
Listing 15-18 の動作する例を変更して、a 内の Rc<List> への参照を作成したり破棄したりするにつれて、参照カウントがどのように変化するかを見てみましょう。
Listing 15-19 では、c リストのまわりに内側のスコープを持つように main を変更します。そうすると、c がスコープを抜けたときに参照カウントがどう変わるかを確認できます。
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
// --snip--
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
プログラム内で参照カウントが変化する各時点で、Rc::strong_count 関数を
呼び出して取得した参照カウントを表示しています。この関数が count ではなく
strong_count という名前になっているのは、Rc<T> 型には weak_count も
あるからです。weak_count が何に使われるのかは、
「Weak<T> を使って参照サイクルを防ぐ」
で見ていきます。
このコードは次のように出力します。
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
a の Rc<List> は初期参照カウントが 1 であり、その後 clone を呼び出す
たびにカウントが 1 ずつ増えることがわかります。c がスコープを抜けると、
カウントは 1 減ります。参照カウントを増やすために Rc::clone を呼び出す
必要があるのに対して、参照カウントを減らすために関数を呼び出す必要はあり
ません。Rc<T> の値がスコープを抜けると、Drop トレイトの実装が自動的に
参照カウントを減らしてくれるからです。
この例では見えませんが、main の終わりで b、続いて a がスコープを
抜けると、カウントは 0 になり、Rc<List> は完全にクリーンアップされます。
Rc<T> を使うと、単一の値が複数の所有者を持てるようになり、このカウントに
よって、所有者のいずれかがまだ存在している限り、その値が有効なままである
ことが保証されます。
不変参照を介して、Rc<T> はプログラムの複数の部分の間で、読み取り専用として
データを共有できるようにします。もし Rc<T> が複数の可変参照も持てるように
してしまうと、第 4 章で説明した借用ルールの 1 つに違反する可能性があります。
同じ場所に対する複数の可変借用は、データ競合や不整合を引き起こす可能性が
あるからです。しかし、データを変更できることは非常に有用です! 次の節では、
内部可変性パターンと、この不変性の制約に対処するために Rc<T> と組み合わせて
使える RefCell<T> 型について説明します。
RefCell<T> と内部可変性パターン
RefCell<T> と内部可変性パターン
内部可変性 はRustの設計パターンであり、そのデータへの不変参照が存在している場合でもデータを変更できるようにするものです。通常、この操作は借用規則によって許可されません。このパターンでは、データを変更するために、データ構造の内部で unsafe コードを使って、変更と借用を支配するRustの通常の規則を曲げます。unsafe コードは、コンパイラに対して、それらの規則をコンパイラに任せて検査してもらうのではなく、私たちが手動で検査していることを示します。unsafe コードについては第20章でさらに詳しく説明します。
内部可変性パターンを使う型は、コンパイラには保証できなくても、実行時には借用規則が守られると確実に言える場合にのみ使用できます。そのとき関わる unsafe コードは安全なAPIで包まれており、外側の型自体は依然として不変です。
内部可変性パターンに従う RefCell<T> 型を見ながら、この概念を探っていきましょう。
借用規則を実行時に強制する
Rc<T> とは異なり、RefCell<T> 型は保持しているデータに対する単一所有権を表します。では、RefCell<T> は Box<T> のような型と何が違うのでしょうか? 第4章で学んだ借用規則を思い出してください。
- どの時点でも、持てるのは1つの可変参照か、任意の数の不変参照のどちらかです(両方は不可)。
- 参照は常に有効でなければなりません。
参照や Box<T> では、借用規則の不変条件はコンパイル時に強制されます。RefCell<T> では、これらの不変条件は 実行時に 強制されます。参照では、これらの規則を破るとコンパイラエラーになります。RefCell<T> では、これらの規則を破るとプログラムはパニックして終了します。
コンパイル時に借用規則を検査する利点は、開発プロセスのより早い段階でエラーを検出できることと、解析がすべて事前に完了しているため実行時性能に影響がないことです。そうした理由から、大半の場合においてはコンパイル時に借用規則を検査するのが最善の選択であり、それがRustのデフォルトになっています。
その代わりに実行時に借用規則を検査する利点は、コンパイル時の検査では拒否されていたであろう、特定のメモリ安全なシナリオが許可されるようになることです。Rustコンパイラのような静的解析は、本質的に保守的です。コードを解析するだけでは検出不可能な性質もあります。最も有名な例は停止性問題ですが、これは本書の範囲を超えるため、興味があれば調べてみるとよいでしょう。
一部の解析は不可能であるため、Rustコンパイラはコードが所有権規則に従っていると確信できない場合、正しいプログラムであっても拒否することがあります。そういう意味で、Rustコンパイラは保守的です。もしRustが誤ったプログラムを受け入れてしまうと、ユーザーはRustが提供する保証を信頼できなくなります。しかし、Rustが正しいプログラムを拒否したとしても、プログラマは不便を被るだけで、破滅的なことは起こりません。RefCell<T> 型は、コードが借用規則に従っていると自分では確信しているものの、コンパイラにはそれを理解して保証できない場合に役立ちます。
Rc<T> と同様に、RefCell<T> はシングルスレッドの状況でのみ使用するものであり、マルチスレッドのコンテキストで使おうとするとコンパイル時エラーになります。マルチスレッドプログラムで RefCell<T> と同等の機能を得る方法については、第16章で説明します。
ここで、Box<T>、Rc<T>、RefCell<T> を選ぶ理由をまとめておきます。
Rc<T>は同じデータに対して複数の所有者を可能にします。Box<T>とRefCell<T>は単一の所有者を持ちます。Box<T>はコンパイル時に検査される不変借用または可変借用を許可します。Rc<T>はコンパイル時に検査される不変借用のみを許可します。RefCell<T>は実行時に検査される不変借用または可変借用を許可します。RefCell<T>は実行時に検査される可変借用を許可するため、RefCell<T>自体が不変であっても、その内部の値を変更できます。
不変値の内部にある値を変更することが、内部可変性パターンです。内部可変性が有用な状況を見て、それがどのように可能になるのかを確認しましょう。
内部可変性を使う
借用規則の帰結として、不変値があるとき、それを可変に借用することはできません。たとえば、次のコードはコンパイルされません。
fn main() {
let x = 5;
let y = &mut x;
}
このコードをコンパイルしようとすると、次のようなエラーになります。
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
しかし、その値自身のメソッド内では自分を変更できる一方で、他のコードからは不変に見えると便利な場合があります。その値のメソッドの外側にあるコードは、その値を変更できません。RefCell<T> を使うことは内部可変性を実現する方法の1つですが、RefCell<T> が借用規則を完全に回避するわけではありません。コンパイラ内の借用チェッカがこの内部可変性を許可し、その代わり借用規則は実行時に検査されます。規則に違反すると、コンパイラエラーではなく panic! が発生します。
不変値を変更するために RefCell<T> を使える実践的な例を見ながら、なぜそれが有用なのかを確認しましょう。
モックオブジェクトを使ったテスト
テスト中にプログラマは、特定の振る舞いを観察してそれが正しく実装されていることを検証するために、ある型の代わりに別の型を使うことがあります。この代役となる型は テストダブル と呼ばれます。映画制作におけるスタントダブルを思い浮かべてください。特に難しいシーンを演じるために、ある人が俳優の代わりを務めます。テストダブルは、テストを実行しているときに他の型の代わりを務めます。モックオブジェクト は、テスト中に何が起きたかを記録する特定の種類のテストダブルで、正しい操作が行われたことを検証できるようにするものです。
Rustには他の言語と同じ意味でのオブジェクトはなく、また他のいくつかの言語のように標準ライブラリにモックオブジェクト機能が組み込まれているわけでもありません。しかし、モックオブジェクトと同じ目的を果たす構造体を作ることは十分にできます。
これからテストするシナリオは次のとおりです。最大値に対してある値を追跡し、現在の値が最大値にどれくらい近いかに基づいてメッセージを送るライブラリを作成します。たとえばこのライブラリは、ユーザーに許可されたAPI呼び出し回数のクォータを追跡するために使えます。
私たちのライブラリは、値が最大値にどれだけ近いかを追跡し、どのタイミングでどのメッセージを出すべきかという機能だけを提供します。私たちのライブラリを使用するアプリケーションは、メッセージを送信する仕組みを提供することが期待されます。アプリケーションは、そのメッセージを直接ユーザーに表示してもよいですし、メールを送信してもよいですし、テキストメッセージを送信してもよいですし、何か別のことをしてもかまいません。ライブラリはその詳細を知る必要はありません。必要なのは、私たちが提供する Messenger というトレイトを実装した何かだけです。リスト15-20にライブラリのコードを示します。
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
このコードの重要な部分のひとつは、Messenger トレイトが send という 1 つのメソッドを持っており、そのメソッドが self への不変参照とメッセージのテキストを受け取ることです。このトレイトは、モックが実際のオブジェクトと同じように使えるようにするために、私たちのモックオブジェクトが実装する必要のあるインターフェイスです。もうひとつ重要なのは、LimitTracker に対する set_value メソッドの振る舞いをテストしたいという点です。value パラメータに渡すものは変更できますが、set_value はアサーションに使えるようなものを何も返しません。Messenger トレイトを実装した何かと、max に対する特定の値で LimitTracker を作成した場合に、value に異なる数値を渡したとき、適切なメッセージを送るよう messenger に指示されることを確認できるようにしたいのです。
必要なのはモックオブジェクトです。このモックオブジェクトは、send を呼んだときにメールやテキストメッセージを送信する代わりに、送るように指示されたメッセージを追跡するだけにします。モックオブジェクトの新しいインスタンスを作成し、そのモックオブジェクトを使う LimitTracker を作成し、LimitTracker の set_value メソッドを呼び出してから、モックオブジェクトが期待どおりのメッセージを保持していることを確認できます。リスト15-21は、まさにそれを行うモックオブジェクトの実装を試みたものですが、借用チェッカーがこれを許可しません。
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
このテストコードでは、MockMessenger 構造体を定義しており、その構造体には sent_messages フィールドがあり、そこには送るように指示されたメッセージを追跡するための String 値の Vec が入っています。また、新しい MockMessenger の値を、メッセージの空のリストで開始するように簡単に作成できるよう、関連関数 new も定義しています。次に、MockMessenger に対して Messenger トレイトを実装し、MockMessenger を LimitTracker に渡せるようにしています。send メソッドの定義では、パラメータとして渡されたメッセージを受け取り、それを MockMessenger の sent_messages リストに保存します。
このテストでは、LimitTracker に対して value を max 値の 75 パーセントを超える値に設定するよう指示したときに何が起こるかをテストしています。まず、新しい MockMessenger を作成します。これは空のメッセージリストで開始されます。次に、新しい LimitTracker を作成し、新しい MockMessenger への参照と 100 の max 値を渡します。LimitTracker に対して 80 という値で set_value メソッドを呼び出します。これは 100 の 75 パーセントを超えています。その後、MockMessenger が追跡しているメッセージのリストには 1 件のメッセージが入っているはずだとアサートします。
しかし、ここに示すように、このテストには 1 つ問題があります。
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
|
2 ~ fn send(&mut self, msg: &str);
3 | }
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
send メソッドは self への不変参照を受け取るため、メッセージを追跡するように MockMessenger を変更できません。また、エラーテキストの提案に従って、impl メソッドとトレイト定義の両方で &mut self を使うこともできません。テストのためだけに Messenger トレイトを変更したくはないからです。代わりに、既存の設計のままでテストコードを正しく動作させる方法を見つける必要があります。
これは、内部可変性が役立つ場面です。sent_messages を RefCell<T> の中に格納すれば、send メソッドは sent_messages を変更して、確認したメッセージを保存できるようになります。リスト15-22にその例を示します。
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
sent_messages フィールドは、今では Vec<String> ではなく RefCell<Vec<String>> 型になっています。new 関数では、空のベクタを包む新しい RefCell<Vec<String>> インスタンスを作成します。
send メソッドの実装では、最初のパラメータは依然として self の不変借用であり、これはトレイト定義と一致しています。self.sent_messages 内の RefCell<Vec<String>> に対して borrow_mut を呼び出し、RefCell<Vec<String>> の内側の値、つまりベクタへの可変参照を取得します。そうすれば、ベクタへの可変参照に対して push を呼び出して、テスト中に送信されたメッセージを追跡できます。
最後に変更しなければならないのはアサーションです。内側のベクタにいくつ要素があるかを確認するために、RefCell<Vec<String>> に対して borrow を呼び出し、ベクタへの不変参照を取得します。
RefCell<T> の使い方を見たところで、それがどのように動作するのかを詳しく見ていきましょう。
実行時に借用を追跡する
不変参照および可変参照を作成するとき、私たちはそれぞれ & および &mut 構文を使います。RefCell<T> では、RefCell<T> に属する安全な API の一部である borrow および borrow_mut メソッドを使います。borrow メソッドはスマートポインタ型 Ref<T> を返し、borrow_mut はスマートポインタ型 RefMut<T> を返します。どちらの型も Deref を実装しているので、通常の参照のように扱えます。
RefCell<T> は、現在アクティブな Ref<T> および RefMut<T> スマートポインタがいくつあるかを追跡します。borrow を呼び出すたびに、RefCell<T> はアクティブな不変借用の数のカウントを増やします。Ref<T> の値がスコープを抜けると、不変借用の数は 1 減少します。コンパイル時の借用ルールと同じように、RefCell<T> でも任意の時点で複数の不変借用、または 1 つの可変借用を持つことができます。
これらのルールに違反しようとすると、参照の場合のようにコンパイラエラーになる代わりに、RefCell<T> の実装は実行時に panic します。リスト15-23は、リスト15-22にある send の実装を変更したものです。RefCell<T> が実行時にこれを防ぐことを示すため、同じスコープ内で 2 つの可変借用をアクティブにしようと、意図的に試しています。
```rust,ignore,panics
# pub trait Messenger {
# fn send(&self, msg: &str);
# }
#
# pub struct LimitTracker<'a, T: Messenger> {
# messenger: &'a T,
# value: usize,
# max: usize,
# }
#
# impl<'a, T> LimitTracker<'a, T>
# where
# T: Messenger,
# {
# pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
# LimitTracker {
# messenger,
# value: 0,
# max,
# }
# }
#
# pub fn set_value(&mut self, value: usize) {
# self.value = value;
#
# let percentage_of_max = self.value as f64 / self.max as f64;
#
# if percentage_of_max >= 1.0 {
# self.messenger.send("Error: You are over your quota!");
# } else if percentage_of_max >= 0.9 {
# self.messenger
# .send("Urgent warning: You've used up over 90% of your quota!");
# } else if percentage_of_max >= 0.75 {
# self.messenger
# .send("Warning: You've used up over 75% of your quota!");
# }
# }
# }
#
# #[cfg(test)]
# mod tests {
# use super::*;
# use std::cell::RefCell;
#
# struct MockMessenger {
# sent_messages: RefCell<Vec<String>>,
# }
#
# impl MockMessenger {
# fn new() -> MockMessenger {
# MockMessenger {
# sent_messages: RefCell::new(vec![]),
# }
# }
# }
#
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
#
# #[test]
# fn it_sends_an_over_75_percent_warning_message() {
# let mock_messenger = MockMessenger::new();
# let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
#
# limit_tracker.set_value(80);
#
# assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
# }
# }
borrow_mut から返される RefMut<T> スマートポインタのために、one_borrow という変数を作成します。次に、同じ方法でもう1つの可変借用を two_borrow という変数に作成します。これにより、同じスコープ内に2つの可変参照ができてしまい、これは許可されていません。ライブラリのテストを実行すると、リスト 15-23 のコードはエラーなしでコンパイルされますが、テストは失敗します。
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
コードが already borrowed: BorrowMutError というメッセージでパニックしたことに注目してください。これが、RefCell<T> が実行時に借用ルール違反を処理する方法です。
ここで行ったように、借用エラーをコンパイル時ではなく実行時に検出することを選ぶと、開発プロセスのより後の段階でコード中のミスを見つける可能性があります。場合によっては、コードを本番環境にデプロイするまで見つからないこともあります。また、コンパイル時ではなく実行時に借用を追跡する結果として、コードにはわずかな実行時性能上のペナルティも生じます。しかし RefCell<T> を使うと、不変値しか許可されない文脈で使いながら、受け取ったメッセージを追跡するために自分自身を変更できるモックオブジェクトを書くことが可能になります。こうしたトレードオフはあるものの、通常の参照が提供する以上の機能を得るために RefCell<T> を使うことができます。
可変データに複数の所有者を持たせる
RefCell<T> の一般的な使い方は、Rc<T> と組み合わせることです。Rc<T> を使うと、あるデータに複数の所有者を持たせることができますが、そのデータへのアクセスは不変アクセスだけであることを思い出してください。RefCell<T> を保持する Rc<T> があれば、複数の所有者を持てる うえに 変更もできる値を得られます。
たとえば、リスト 15-18 の cons リストの例では、Rc<T> を使って複数のリストが別のリストの所有権を共有できるようにしていたことを思い出してください。Rc<T> は不変値しか保持しないため、一度作成した後にリスト内の値を変更することはできません。ここで、リスト内の値を変更する能力のために RefCell<T> を追加してみましょう。リスト 15-24 は、Cons の定義で RefCell<T> を使うことで、すべてのリストに格納されている値を変更できることを示しています。
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {a:?}");
println!("b after = {b:?}");
println!("c after = {c:?}");
}
Rc<RefCell<i32>> のインスタンスである値を作成し、それを value という変数に格納して、後で直接アクセスできるようにします。次に、value を保持する Cons バリアントを持つ List を a に作成します。value から a に所有権を移したり、a が value を借用したりするのではなく、a と value の両方が内側の 5 という値の所有権を持つようにするため、value をクローンする必要があります。
リスト a を Rc<T> で包むことで、リスト b と c を作成するときに、それらがどちらも a を参照できるようにします。これはリスト 15-18 で行ったことと同じです。
a、b、c にリストを作成した後、value の中の値に 10 を加えたいと考えます。これを行うために value に対して borrow_mut を呼び出します。これは、第5章の 「-> 演算子はどこへ行った?」 で説明した自動デリファレンス機能を使って、Rc<T> をデリファレンスし、内側の RefCell<T> の値に到達します。borrow_mut メソッドは RefMut<T> スマートポインタを返し、私たちはそれに対してデリファレンス演算子を使って内側の値を変更します。
a、b、c を表示すると、それらがすべて 5 ではなく、変更後の値 15 を持っていることがわかります。
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
このテクニックはかなり巧妙です! RefCell<T> を使うことで、外見上は不変の List 値を持つことができます。しかし、内部可変性へのアクセスを提供する RefCell<T> のメソッドを使えば、必要なときにデータを変更できます。借用ルールの実行時チェックによって、データ競合から保護されます。この柔軟性のために、データ構造において少しの速度を犠牲にする価値があることもあります。なお、RefCell<T> はマルチスレッドのコードでは機能しません。Mutex<T> は RefCell<T> のスレッドセーフ版であり、Mutex<T> については第16章で説明します。
参照サイクルはメモリリークを引き起こすことがある
参照サイクルはメモリリークを引き起こすことがある
Rust のメモリ安全性の保証のおかげで、誤って決してクリーンアップされないメモリ(メモリリーク として知られています)を作ってしまうことは簡単ではありませんが、不可能ではありません。メモリリークを完全に防ぐことは Rust の保証の 1 つではないため、Rust ではメモリリークはメモリ安全性を損ないません。Rc<T> と RefCell<T> を使うと、Rust がメモリリークを許容していることがわかります。要素同士がサイクル状に互いを参照する参照を作成できるのです。これによりメモリリークが発生します。なぜなら、サイクル内の各要素の参照カウントは決して 0 にならず、値が決してドロップされないからです。
参照サイクルを作成する
参照サイクルがどのように発生しうるのか、またそれをどう防ぐのかを見ていきましょう。まずは、リスト 15-25 にある List 列挙型の定義と tail メソッドから始めます。
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {}
ここでは、リスト 15-5 にあった List 定義の別の変形を使っています。Cons バリアントの 2 番目の要素は今では RefCell<Rc<List>> になっており、これは、リスト 15-24 で行ったように i32 値を変更できるようにするのではなく、Cons バリアントが指している List 値を変更したいという意味です。また、値が Cons バリアントである場合に 2 番目の要素へ簡単にアクセスできるよう、tail メソッドも追加しています。
リスト 15-26 では、リスト 15-25 の定義を使う main 関数を追加しています。このコードは、a にリストを作成し、さらに a 内のリストを指すリストを b に作成します。その後、a 内のリストが b を指すように変更し、参照サイクルを作ります。途中には println! 文があり、この過程のさまざまな時点で参照カウントがどうなっているかを示しています。
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation = {}", Rc::strong_count(&a));
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// Uncomment the next line to see that we have a cycle;
// it will overflow the stack.
// println!("a next item = {:?}", a.tail());
}
まず、初期リスト 5, Nil を持つ List 値を保持する Rc<List> インスタンスを変数 a に作成します。次に、値 10 を含み、a 内のリストを指す別の List 値を保持する Rc<List> インスタンスを変数 b に作成します。
次に、a が Nil ではなく b を指すように変更して、サイクルを作成します。そのために、tail メソッドを使って a 内の RefCell<Rc<List>> への参照を取得し、それを変数 link に入れます。続いて、RefCell<Rc<List>> に対して borrow_mut メソッドを使い、内部の値を Nil 値を保持する Rc<List> から b 内の Rc<List> へ変更します。
今のところ最後の println! はコメントアウトしたままでこのコードを実行すると、次の出力が得られます。
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
a 内のリストが b を指すように変更した後では、a と b の両方にある Rc<List> インスタンスの参照カウントは 2 になります。main の末尾では、Rust は変数 b をドロップするので、b の Rc<List> インスタンスの参照カウントは 2 から 1 に減ります。Rc<List> がヒープ上に確保しているメモリは、この時点では参照カウントが 0 ではなく 1 なのでドロップされません。次に Rust は a をドロップし、a の Rc<List> インスタンスの参照カウントも 2 から 1 に減ります。このインスタンスのメモリも、もう一方の Rc<List> インスタンスがまだそれを参照しているため、やはりドロップできません。リストに割り当てられたメモリは永遠に回収されないままになります。この参照サイクルを視覚化したものが図15-4です。
図15-4: 互いを指すリスト a と b
の参照サイクル
最後の println! のコメントを外してプログラムを実行すると、Rust は a が b を指し、b が a を指すという具合にこのサイクルを出力しようとして、最終的にスタックオーバーフローします。
現実のプログラムと比べれば、この例で参照サイクルを作成した結果はそれほど深刻ではありません。参照サイクルを作成した直後にプログラムが終了するからです。しかし、より複雑なプログラムがサイクル内に大量のメモリを確保し、それを長時間保持した場合、そのプログラムは必要以上のメモリを使い、システムを圧迫して、利用可能なメモリを使い果たしてしまうかもしれません。
参照サイクルを作るのは簡単ではありませんが、不可能でもありません。Rc<T> 値を含む RefCell<T> 値、あるいは内部可変性と参照カウントを持つ型の同様の入れ子の組み合わせがある場合は、サイクルを作らないよう自分で保証しなければなりません。Rust がそれを検出してくれると期待することはできません。参照サイクルを作ることはプログラム中のロジックバグであり、自動テスト、コードレビュー、そのほかのソフトウェア開発の実践を使って最小限に抑えるべきです。
参照サイクルを避ける別の解決策は、データ構造を再編成して、一部の参照は所有権を表し、一部の参照は所有権を表さないようにすることです。そうすると、いくつかの所有関係といくつかの非所有関係から成るサイクルを持つことができ、値をドロップできるかどうかに影響するのは所有関係だけになります。リスト 15-25 では、Cons バリアントには常に自分のリストを所有させたいので、データ構造を再編成することはできません。親ノードと子ノードからなるグラフを使った例を見て、所有権を伴わない関係が参照サイクルを防ぐのに適切な方法となるのはどのような場合かを確認しましょう。
Weak<T> を使って参照サイクルを防ぐ
ここまでは、Rc::clone を呼び出すと Rc<T> インスタンスの strong_count が増加し、Rc<T> インスタンスはその strong_count が 0 の場合にのみクリーンアップされることを示してきました。Rc::downgrade を呼び出して Rc<T> への参照を渡すことで、Rc<T> インスタンス内の値への弱参照を作成することもできます。強参照 は、Rc<T> インスタンスの所有権を共有するための仕組みです。弱参照 は所有関係を表さず、そのカウントは Rc<T> インスタンスがいつクリーンアップされるかに影響しません。弱参照は参照サイクルの原因になりません。なぜなら、一部に弱参照を含むサイクルは、関係する値の強参照カウントが 0 になれば壊れるからです。
Rc::downgrade を呼び出すと、Weak<T> 型のスマートポインタが得られます。
Rc<T> インスタンスの strong_count を 1 増やす代わりに、
Rc::downgrade を呼び出すと weak_count が 1 増えます。Rc<T> 型は
strong_count と同様に、存在する Weak<T> 参照の数を追跡するために
weak_count を使用します。違いは、Rc<T> インスタンスがクリーンアップされるために
weak_count が 0 である必要はないという点です。
Weak<T> が参照している値はすでにドロップされている可能性があるため、
Weak<T> が指している値に対して何かを行うには、その値がまだ存在していることを
確認しなければなりません。これを行うには、Weak<T> インスタンスに対して
upgrade メソッドを呼び出します。これにより Option<Rc<T>> が返されます。
Rc<T> の値がまだドロップされていなければ結果は Some になり、
Rc<T> の値がドロップされていれば結果は None になります。upgrade は
Option<Rc<T>> を返すため、Rust は Some の場合と None の場合が処理されることを保証し、
無効なポインタは発生しません。
例として、各要素が次の要素しか知らないリストを使う代わりに、 各ノードが子ノード と 親ノードを把握している木を作成します。
ツリーデータ構造を作成する
まず、子ノードを把握しているノードを持つ木を構築します。
自身の i32 値と、その子である Node 値への参照を保持する、
Node という名前の構造体を作成します。
ファイル名: src/main.rs
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}
Node にその子を所有させたい一方で、その所有権を変数と共有して、
木の中の各 Node に直接アクセスできるようにもしたいと考えています。これを実現するために、
Vec<T> の要素を Rc<Node> 型の値として定義します。また、どのノードが別のノードの子であるかを
変更できるようにしたいので、children では Vec<Rc<Node>> を
RefCell<T> で包みます。
次に、この構造体定義を使って、値 3 と子を持たない leaf という名前の
Node インスタンスを 1 つ作成し、さらに値 5 を持ち、leaf をその子の
1 つとして持つ branch という名前の別のインスタンスを作成します。これは
リスト 15-27 に示されています。
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}
leaf 内の Rc<Node> をクローンして branch に格納します。つまり、
leaf 内の Node には現在 2 つの所有者がいます: leaf と branch です。
branch.children を通じて branch から leaf にたどることはできますが、
leaf から branch にたどる方法はありません。その理由は、leaf が branch への参照を持っておらず、
両者が関連していることを知らないからです。leaf に、branch がその親であることを
認識させたいので、次にそれを行います。
子から親への参照を追加する
子ノードが親を認識できるようにするには、Node 構造体定義に parent フィールドを
追加する必要があります。問題は、parent の型を何にすべきかを決めることです。
そこに Rc<T> を含められないことはわかっています。なぜなら、その場合
leaf.parent が branch を指し、branch.children が leaf を指す
参照サイクルが作られ、それによってそれらの strong_count の値が決して 0 にならないからです。
別の見方をすると、親ノードはその子を所有すべきです。親ノードがドロップされたら、 その子ノードも同様にドロップされるべきです。しかし、子は親を所有すべきではありません。 子ノードをドロップしても、親は依然として存在しているべきです。これは弱参照の出番です。
そのため、Rc<T> の代わりに、parent の型として Weak<T>、
具体的には RefCell<Weak<Node>> を使います。これで Node 構造体定義は
次のようになります。
ファイル名: src/main.rs
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
ノードは親ノードを参照できますが、その親を所有はしません。リスト 15-28 では、
leaf ノードがその親である branch を参照できるように、
この新しい定義を使うよう main を更新しています。
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
leaf ノードの作成は、parent フィールドを除けばリスト 15-27 と似ています。
leaf は最初は親を持たないため、新しい空の Weak<Node> 参照インスタンスを作成します。
この時点で、upgrade メソッドを使って leaf の親への参照を取得しようとすると、
None の値が得られます。これは最初の println! 文の出力で確認できます。
leaf parent = None
branch ノードを作成すると、その parent フィールドにも新しい Weak<Node>
参照が入ります。これは branch が親ノードを持たないためです。また、
leaf は引き続き branch の子の 1 つです。branch 内の Node
インスタンスを手に入れたら、leaf を変更して、親への Weak<Node> 参照を
持たせることができます。leaf の parent フィールドにある
RefCell<Weak<Node>> に対して borrow_mut メソッドを使い、
その後 Rc::downgrade 関数を使って、branch 内の Rc<Node> から
branch への Weak<Node> 参照を作成します。
再び leaf の親を出力すると、今度は branch を保持した Some バリアントが得られます。
これで leaf はその親にアクセスできるようになりました。また、leaf を出力するときにも、
リスト 15-26 にあったような、最終的にスタックオーバーフローに至るサイクルを回避できます。
Weak<Node> 参照は (Weak) として出力されます。
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })
無限に出力されないことから、このコードが参照サイクルを作成していないことがわかります。
これは Rc::strong_count と Rc::weak_count を呼び出して得られる値を見てもわかります。
strong_count と weak_count の変化を視覚化する
Rc<Node> インスタンスの strong_count と weak_count の値が
どのように変化するかを、新しい内側のスコープを作成し、branch の作成を
そのスコープ内に移すことで見てみましょう。そうすることで、branch が作成され、
スコープを抜けたときにドロップされると何が起こるのかを確認できます。
変更内容をリスト 15-29 に示します。
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
);
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
leaf が作成された後、その Rc<Node> の強参照カウントは 1、弱参照カウントは 0 です。内側のスコープでは、branch を作成してそれを leaf に関連付けます。この時点でカウントを出力すると、branch 内の Rc<Node> は強参照カウントが 1、弱参照カウントが 1 になります(leaf.parent が Weak<Node> で branch を指しているためです)。leaf のカウントを出力すると、branch.children に格納された leaf の Rc<Node> のクローンを branch が持つようになったため、強参照カウントが 2 になっていることがわかりますが、弱参照カウントは引き続き 0 のままです。
内側のスコープが終了すると、branch はスコープ外に出て、Rc<Node> の強参照カウントは 0 に減少するため、その Node は破棄されます。leaf.parent からの 1 つの弱参照カウントは Node が破棄されるかどうかには影響しないので、メモリリークは発生しません!
スコープの終了後に leaf の親へアクセスしようとすると、再び None が得られます。プログラムの最後では、leaf 内の Rc<Node> の強参照カウントは 1、弱参照カウントは 0 です。これは、変数 leaf が再びその Rc<Node> への唯一の参照になっているためです。
カウントの管理と値の破棄に関するロジックは、すべて Rc<T> と Weak<T>、およびそれらの Drop トレイトの実装に組み込まれています。Node の定義において、子から親への関係が Weak<T> 参照であると指定することで、親ノードが子ノードを指し、かつその逆も可能でありながら、参照サイクルやメモリリークを発生させずに済みます。
まとめ
この章では、通常の参照に対して Rust がデフォルトで提供するものとは異なる保証やトレードオフを実現するために、スマートポインタをどのように使うかを扱いました。Box<T> 型はサイズが既知で、ヒープに確保されたデータを指します。Rc<T> 型はヒープ上のデータへの参照数を追跡し、そのデータが複数の所有者を持てるようにします。RefCell<T> 型は、その内部可変性によって、不変な型が必要でありながらその型の内部の値を変更する必要がある場合に使える型を提供します。また、借用規則をコンパイル時ではなく実行時に強制します。
さらに、スマートポインタの多くの機能を可能にする Deref トレイトと Drop トレイトについても説明しました。また、メモリリークを引き起こしうる参照サイクルを取り上げ、Weak<T> を使ってそれを防ぐ方法を見てきました。
この章を読んで興味を持ち、自分自身のスマートポインタを実装してみたいと思ったなら、さらに役立つ情報については “The Rustonomicon” を参照してください。
次は、Rust における並行性について話します。いくつかの新しいスマートポインタについても学ぶことになります。
恐れ知らずの並行性
並行プログラミングを安全かつ効率的に扱うことは、Rust のもう 1 つの 主要な目標です。並行プログラミング では、プログラムの異なる部分が 独立して実行され、並列プログラミング では、プログラムの異なる部分が 同時に実行されます。より多くのコンピュータが複数のプロセッサを活用する ようになるにつれて、これらはますます重要になっています。歴史的に、 こうした状況でのプログラミングは難しく、エラーが起こりやすいものでした。 Rust はこれを変えようとしています。
当初、Rust チームは、メモリ安全性を確保することと並行性の問題を防ぐことは 異なる方法で解決すべき 2 つの別個の課題だと考えていました。時間がたつに つれて、チームは、所有権システムと型システムが、メモリ安全性 と 並行性の問題の両方を管理するのに役立つ強力な道具立てであることを 発見しました! 所有権と型チェックを活用することで、多くの並行性エラーは、 Rust では実行時エラーではなくコンパイル時エラーになります。したがって、 実行時の並行性バグが発生する正確な状況を再現しようとして多くの時間を 費やす代わりに、誤ったコードはコンパイルされず、その問題を説明する エラーが示されます。その結果、コードが本番環境にリリースされたあとでは なく、作業中の段階で修正できます。Rust のこの側面を 恐れ知らずの 並行性 と呼んでいます。恐れ知らずの並行性によって、気づきにくいバグの ないコードを書けるようになり、新たなバグを持ち込むことなく簡単に リファクタリングできます。
注: 簡単にするため、多くの問題について、より正確に 並行および/または並列 と言う代わりに 並行 と呼びます。この章では、 並行 という語を使うたびに、頭の中で 並行および/または並列 と 読み替えてください。次章では、この違いがより重要になるため、より具体的に 述べます。
多くの言語は、並行性の問題を扱うために提供する解決策について教条的です。 たとえば、Erlang にはメッセージパッシングによる並行性のための洗練された 機能がありますが、スレッド間で状態を共有する方法はわかりにくいものしか ありません。可能な解決策の一部だけをサポートするのは、高水準言語に とって妥当な戦略です。なぜなら、高水準言語は、抽象化を得るために ある程度の制御を手放す代わりに恩恵をもたらすからです。しかし、低水準 言語には、どのような状況でも最良の性能を発揮する解決策を提供し、 ハードウェアに対する抽象化をより少なくすることが期待されます。したがって、 Rust は、あなたの状況と要件に適した方法で問題をモデル化するための さまざまなツールを提供しています。
この章で扱うトピックは次のとおりです。
- 複数のコード片を同時に実行するためのスレッドを作成する方法
- チャネルがスレッド間でメッセージを送信する メッセージパッシング による 並行性
- 複数のスレッドがあるデータにアクセスできる 共有状態 による並行性
- Rust の並行性に関する保証を、ユーザー定義型だけでなく標準ライブラリが
提供する型にも拡張する
SyncとSendトレイト
スレッドを使ってコードを同時に実行する
コードを同時に実行するためのスレッドの利用
現在のほとんどのオペレーティングシステムでは、実行されるプログラムのコードは プロセス 内で実行され、オペレーティングシステムは複数のプロセスを同時に 管理します。プログラムの内部では、同時に実行される独立した部分を持つことも できます。こうした独立した部分を実行する機能は、スレッド と呼ばれます。たと えば、Webサーバーは複数のスレッドを持つことで、同時に複数のリクエストに 応答できます。
プログラムの計算を複数のスレッドに分割して、複数のタスクを同時に実行する ことで、パフォーマンスが向上する場合がありますが、その一方で複雑さも増し ます。スレッドは同時に実行されうるため、異なるスレッド上のコードの各部分が どの順序で実行されるかについて、本質的な保証はありません。これにより、たと えば次のような問題が生じる可能性があります。
- 競合状態。スレッドがデータやリソースに不整合な順序で アクセスすること
- デッドロック。2つのスレッドが互いを待ち、どちらの スレッドも先に進めなくなること
- 特定の状況でしか発生せず、確実に再現して修正するのが難しい バグ
Rustはスレッド利用の悪影響を軽減しようとしていますが、それでも マルチスレッド環境でのプログラミングには慎重な検討が必要であり、 コード構造も単一スレッドで動くプログラムとは異なるものが求められます。
プログラミング言語はスレッドをいくつかの異なる方法で実装しており、多くの オペレーティングシステムは、新しいスレッドを作成するためにプログラミング 言語から呼び出せるAPIを提供しています。Rustの標準ライブラリは、スレッド 実装に 1:1 モデルを採用しており、このモデルでは、プログラムは言語レベルの 1つのスレッドにつき、オペレーティングシステムの1つのスレッドを使用します。 1:1モデルとは異なるトレードオフを行う、別のスレッドモデルを実装したcrateも あります。(次の章で見るRustのasyncシステムも、並行性に対する別の アプローチを提供します。)
spawn を使って新しいスレッドを作成する
新しいスレッドを作成するには、thread::spawn 関数を呼び出し、新しい
スレッドで実行したいコードを含むクロージャ(クロージャについては第13章で
説明しました)を渡します。リスト16-1の例では、メインスレッドからある
テキストを出力し、新しいスレッドから別のテキストを出力します。
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
Rustプログラムのメインスレッドが完了すると、spawnされたすべてのスレッド は、実行を完了しているかどうかにかかわらず停止されることに注意して ください。このプログラムの出力は毎回少し異なるかもしれませんが、次の ようなものになります。
hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
thread::sleep の呼び出しは、スレッドに短時間その実行を停止させ、
別のスレッドが実行できるようにします。スレッドはおそらく交互に実行され
ますが、それは保証されていません。これは、オペレーティングシステムが
スレッドをどのようにスケジュールするかに依存します。この実行では、コード中
ではspawnされたスレッドの print 文のほうが先に現れているにもかかわらず、
メインスレッドが先に出力しました。また、spawnされたスレッドには i が 9
になるまで出力するよう指示しましたが、メインスレッドが終了する前に 5
までしか到達しませんでした。
このコードを実行して、メインスレッドからの出力しか見えない、またはまったく 重なりが見えない場合は、範囲内の数値を大きくして、オペレーティング システムがスレッド間を切り替える機会を増やしてみてください。
すべてのスレッドの終了を待つ
リスト16-1のコードでは、メインスレッドが終了するため、ほとんどの場合 spawnされたスレッドが途中で止められてしまうだけでなく、スレッドが実行 される順序にも保証がないため、spawnされたスレッドがそもそも実行される ことすら保証できません!
spawnされたスレッドが実行されない、または途中で終了してしまうという
問題は、thread::spawn の戻り値を変数に保存することで修正できます。
thread::spawn の戻り値の型は JoinHandle<T> です。JoinHandle<T> は
所有権を持つ値で、この値に対して join メソッドを呼び出すと、そのスレッド
が終了するまで待機します。リスト16-2は、リスト16-1で作成したスレッドの
JoinHandle<T> をどのように使うか、また、main が終了する前に
spawnされたスレッドが確実に完了するように join をどのように呼び出すか
を示しています。
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
ハンドルに対して join を呼び出すと、現在実行中のスレッドは、その
ハンドルが表すスレッドが終了するまでブロックされます。スレッドを
ブロック するとは、そのスレッドが処理を実行したり終了したりできない
ようにすることです。join の呼び出しをメインスレッドの for ループの
後に置いたので、リスト16-2を実行すると、次のような出力になるはずです。
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
2つのスレッドは引き続き交互に実行されますが、handle.join() の呼び
出しによりメインスレッドは待機するため、spawnされたスレッドが完了する
まで終了しません。
では、代わりに main の for ループの前に handle.join() を移動
するとどうなるかを見てみましょう。
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
handle.join().unwrap();
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
メインスレッドはspawnされたスレッドが終わるのを待ってから自分の for
ループを実行するので、ここに示すように、出力はもう入り混じらなくなります。
```text
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!
join をどこで呼び出すかのようなちょっとした詳細でも、スレッドが同時に
実行されるかどうかに影響を与えることがあります。
スレッドで move クロージャを使う
thread::spawn に渡すクロージャでは move キーワードをよく使います。
これは、そのクロージャが環境から使用する値の所有権を取得し、結果として
それらの値の所有権をあるスレッドから別のスレッドへ移すためです。
第 13 章の 「参照をキャプチャするか所有権をムーブするか」 では、クロージャの文脈で move を取り上げました。ここでは、
move と thread::spawn の相互作用により注目します。
リスト 16-1 では、thread::spawn に渡すクロージャが引数を取らないことに
注目してください。生成されたスレッドのコードでは、メインスレッドのデータを
何も使っていません。生成されたスレッドでメインスレッドのデータを使うには、
生成されたスレッドのクロージャが必要な値をキャプチャしなければなりません。
リスト 16-3 は、メインスレッドでベクタを作成し、それを生成された
スレッドで使おうとする試みを示しています。しかし、すぐにわかるように、
これはまだうまく動きません。
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();
}
このクロージャは v を使っているので、v をキャプチャして、そのクロージャの
環境の一部にします。thread::spawn はこのクロージャを新しいスレッドで実行するため、
その新しいスレッドの中で v にアクセスできるはずです。しかし、この例を
コンパイルすると、次のエラーが出ます。
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7 | println!("Here's a vector: {v:?}");
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {v:?}");
8 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` (bin "threads") due to 1 previous error
Rust は v をどのようにキャプチャするかを 推論 します。そして、
println! が必要とするのは v への参照だけなので、クロージャは v を
借用しようとします。しかし、ここには問題があります。Rust には、生成された
スレッドがどれくらい実行されるのか判断できないため、v への参照が常に
有効かどうかわからないのです。
リスト 16-4 は、v への参照が無効になる可能性がより高い状況を示しています。
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
drop(v); // oh no!
handle.join().unwrap();
}
もし Rust がこのコードの実行を許したなら、生成されたスレッドがまったく実行
されないまま直ちにバックグラウンドに回される可能性があります。生成された
スレッドの内部には v への参照がありますが、メインスレッドは第 15 章で
説明した drop 関数を使って、すぐに v をドロップします。すると、
生成されたスレッドの実行が始まったときには v はもはや有効ではなく、
それへの参照も無効になります。困りました!
リスト 16-3 のコンパイラエラーを修正するには、エラーメッセージの助言に 従えます。
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
クロージャの前に move キーワードを追加すると、そのクロージャが使用している
値の所有権を取得するように強制でき、Rust にそれらの値を借用すべきだと
推論させずに済みます。リスト 16-5 に示した、リスト 16-3 への修正は、
意図どおりにコンパイルされて実行されます。
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();
}
メインスレッドが drop を呼び出していたリスト 16-4 のコードも、move
クロージャを使って同じように修正したくなるかもしれません。しかし、この修正は
機能しません。なぜなら、リスト 16-4 がやろうとしていることは、別の理由で
許可されていないからです。クロージャに move を追加すると、v はクロージャの
環境へムーブされるため、メインスレッドでそれに対して drop を呼び出すことは
もうできません。代わりに、このコンパイラエラーが出ます。
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
--> src/main.rs:10:10
|
4 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
5 |
6 | let handle = thread::spawn(move || {
| ------- value moved into closure here
7 | println!("Here's a vector: {v:?}");
| - variable moved due to use in closure
...
10 | drop(v); // oh no!
| ^ value used here after move
|
help: consider cloning the value before moving it into the closure
|
6 ~ let value = v.clone();
7 ~ let handle = thread::spawn(move || {
8 ~ println!("Here's a vector: {value:?}");
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` (bin "threads") due to 1 previous error
Rust の所有権ルールが、またしても私たちを救ってくれました! リスト 16-3 の
コードでエラーが出たのは、Rust が慎重に振る舞ってスレッドに対して v を
借用するだけにしていたためであり、その結果、理論上はメインスレッドが
生成されたスレッドの参照を無効にできてしまうからです。Rust に v の
所有権を生成されたスレッドへムーブするよう伝えることで、メインスレッドが
もはや v を使わないことを Rust に保証しています。リスト 16-4 も同じように
変更すると、メインスレッドで v を使おうとした時点で所有権ルールに
違反することになります。move キーワードは、借用するという Rust の慎重な
デフォルトを上書きするものであり、所有権ルールに違反できるようにする
ものではありません。
スレッドとは何かと、thread API が提供するメソッドを見てきたので、次は スレッドを使用できるいくつかの状況を見ていきましょう。
メッセージ受け渡しでスレッド間でデータを転送する
メッセージパッシングでスレッド間のデータを受け渡す
安全な並行性を確保するためのアプローチとして、ますます人気が高まって いるのがメッセージパッシングです。これは、スレッドやアクターがデータを 含むメッセージを互いに送り合うことで通信する方式です。この考え方は、 Go 言語のドキュメント の次の標語によく表れています: 「メモリを共有して通信するな。代わりに、通信することでメモリを共有せよ。」
Rust の標準ライブラリは、メッセージ送信による並行性を実現するための チャネル実装を提供しています。チャネル とは、データをあるスレッドから 別のスレッドへ送るための一般的なプログラミング概念です。
プログラミングにおけるチャネルは、小川や川のように一方向に流れる水路の ようなものだと考えることができます。アヒルのおもちゃのようなものを川に 入れると、それは下流へ流れていき、水路の終点にたどり着きます。
チャネルは二つの部分から成ります。送信側と受信側です。送信側は、川の 上流でアヒルのおもちゃを川に入れる場所にあたり、受信側は、そのアヒルの おもちゃが下流で行き着く場所にあたります。コードの一方では、送信したい データとともに送信側のメソッドを呼び出し、もう一方では、受信側に届く メッセージを確認します。送信側または受信側のどちらかがドロップされると、 そのチャネルは 閉じられた と見なされます。
ここでは、値を生成してチャネルに送る 1 つのスレッドと、その値を受信して 表示する別のスレッドを持つプログラムを段階的に作っていきます。この機能を 説明するために、チャネルを使ってスレッド間で単純な値を送ります。この手法に 慣れたら、チャットシステムや、多数のスレッドが計算の一部を実行してその部分を 結果を集約する 1 つのスレッドに送るようなシステムなど、互いに通信する必要が あるあらゆるスレッドでチャネルを使えるようになります。
まず、リスト 16-6 ではチャネルを作成しますが、まだそれでは何もしません。 なお、現時点ではこれはまだコンパイルできません。Rust には、どの型の値を チャネルで送りたいのかが分からないためです。
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
mpsc::channel 関数を使って新しいチャネルを作成します。mpsc は
multiple producer, single consumer の略です。要するに、Rust の標準ライブラリ
におけるチャネル実装では、1 つのチャネルに、値を生成する複数の 送信 側を
持てますが、それらの値を消費する 受信 側は 1 つだけです。複数の小川が
合流して 1 本の大きな川になるところを想像してください。どの小川に流したものも、
最後には 1 本の川に流れ着きます。ここではまず 1 つのプロデューサーから
始めますが、この例が動くようになったら複数のプロデューサーを追加します。
mpsc::channel 関数はタプルを返します。その第 1 要素が送信側、つまり
トランスミッターであり、第 2 要素が受信側、つまりレシーバーです。略語
tx と rx は、多くの分野でそれぞれ transmitter と receiver を表すものとして
伝統的に使われているため、それぞれの端を示すように変数名をそのように
しています。ここでは、タプルを分解するパターンを伴う let 文を使っています。
let 文におけるパターンの使い方と分配束縛については第 19 章で説明します。
今は、このように let 文を使うことで、mpsc::channel が返すタプルの各要素を
便利に取り出せると知っておいてください。
次に、送信側を spawn したスレッドに移し、そのスレッドから 1 つの文字列を 送るようにして、生成したスレッドがメインスレッドと通信するようにしましょう。 これは、上流でアヒルのおもちゃを川に入れること、あるいはあるスレッドから 別のスレッドへチャットメッセージを送ることに似ています。
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
}
ここでも、新しいスレッドを作成するために thread::spawn を使い、続いて move
を使って tx をクロージャへ移動し、生成したスレッドが tx を所有するように
しています。生成したスレッドは、チャネルを通じてメッセージを送るために、
送信側を所有している必要があります。
送信側には、送信したい値を受け取る send メソッドがあります。send
メソッドは Result<T, E> 型を返すため、受信側がすでにドロップされていて
値の送り先がない場合、送信操作はエラーを返します。この例では、エラー時に
パニックするために unwrap を呼び出しています。しかし実際のアプリケーション
では、これを適切に処理することになります。適切なエラーハンドリングの戦略を
復習するには、第 9 章に戻ってください。
リスト 16-8 では、メインスレッドで受信側から値を取得します。これは、川の 終わりで水の中からアヒルのおもちゃを取り出すこと、あるいはチャット メッセージを受け取ることに似ています。
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}
受信側には、便利なメソッドが 2 つあります。recv と try_recv です。ここで
使っているのは receive の略である recv で、これはメインスレッドの実行を
ブロックし、チャネルに値が送られるまで待機します。値が送信されると、
recv はそれを Result<T, E> で返します。送信側が閉じられると、recv は、
それ以上値が来ないことを示すためにエラーを返します。
try_recv メソッドはブロックしません。その代わり、即座に Result<T, E> を
返します。利用可能なメッセージがあればそれを保持した Ok 値を返し、今回
メッセージがなければ Err 値を返します。try_recv は、このスレッドに、
メッセージを待っている間にほかにする仕事がある場合に便利です。一定間隔で
try_recv を呼び出し、メッセージがあればそれを処理し、そうでなければ
少しのあいだ別の仕事をしてから再び確認する、というループを書くことができます。
この例では単純化のために recv を使っています。メインスレッドには、
メッセージを待つ以外にする仕事がないため、メインスレッドをブロックするのは
適切です。
リスト 16-8 のコードを実行すると、メインスレッドから表示された次の値が 見えるはずです。
Got: hi
完璧です!
チャネルを通じて所有権を移動する
所有権のルールは、メッセージ送信において重要な役割を果たします。なぜなら、それによって安全な並行コードを書けるようになるからです。Rustプログラム全体を通して所有権を意識する利点は、並行プログラミングにおけるエラーを防げることです。チャネルと所有権がどのように連携して問題を防ぐのかを示すために、実験をしてみましょう。チャネルに送信した 後で、生成されたスレッド内で val の値を使おうとしてみます。このコードが許可されない理由を確認するために、リスト16-9のコードをコンパイルしてみてください。
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
println!("val is {val}");
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}
ここでは、tx.send を通じて val をチャネルに送信した後で、それを表示しようとしています。これを許可するのはよくありません。いったん値が別のスレッドへ送信されると、そのスレッドが、こちらが再びその値を使おうとする前に、それを変更したりドロップしたりできてしまうからです。場合によっては、他方のスレッドによる変更が、一貫性のないデータや存在しないデータを原因として、エラーや予期しない結果を引き起こす可能性があります。しかし、リスト16-9のコードをコンパイルしようとすると、Rustはエラーを出してくれます。
$ cargo run
Compiling message-passing v0.1.0 (file:///projects/message-passing)
error[E0382]: borrow of moved value: `val`
--> src/main.rs:10:27
|
8 | let val = String::from("hi");
| --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
9 | tx.send(val).unwrap();
| --- value moved here
10 | println!("val is {val}");
| ^^^ 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)
For more information about this error, try `rustc --explain E0382`.
error: could not compile `message-passing` (bin "message-passing") due to 1 previous error
この並行性に関するミスは、コンパイル時エラーを引き起こしました。send 関数はその引数の所有権を受け取り、値がムーブされると受信側がその所有権を取得します。これにより、送信後に誤ってその値を再び使うことが防がれます。所有権システムが、すべて問題ないことを検査してくれるのです。
複数の値を送信する
リスト16-8のコードはコンパイルも実行もできましたが、2つの別々のスレッドがチャネルを介して互いにやり取りしていることは、はっきりとは示していませんでした。
リスト16-10では、リスト16-8のコードが並行して動いていることを示すための変更をいくつか加えました。生成されたスレッドは今度は複数のメッセージを送信し、各メッセージの間で1秒停止します。
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {received}");
}
}
今回は、生成されたスレッドがメインスレッドへ送りたい文字列のベクタを持っています。それらを反復処理して、それぞれを個別に送信し、1秒の Duration 値を指定して thread::sleep 関数を呼び出すことで、各送信の間に一時停止を入れます。
メインスレッドでは、もはや recv 関数を明示的に呼び出していません。代わりに、rx をイテレータとして扱っています。受信した各値について、それを表示しています。チャネルが閉じられると、反復は終了します。
リスト16-10のコードを実行すると、各行の間に1秒の間隔を空けて、次のような出力が表示されるはずです。
Got: hi
Got: from
Got: the
Got: thread
メインスレッドの for ループには一時停止や遅延を行うコードがないので、メインスレッドが生成されたスレッドから値を受け取るのを待っていることがわかります。
複数のプロデューサを作成する
先ほど、mpsc は 複数のプロデューサ、単一のコンシューマ の頭字語だと述べました。では mpsc を実際に使って、リスト16-10のコードを拡張し、すべて同じ受信側に値を送る複数のスレッドを作成してみましょう。これは、リスト16-11に示すように送信側をクローンすることで実現できます。
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
// --snip--
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
thread::spawn(move || {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {received}");
}
// --snip--
}
今回は、最初の生成されたスレッドを作成する前に、送信側に対して clone を呼び出します。これにより、最初の生成されたスレッドに渡せる新しい送信側が得られます。元の送信側は2つ目の生成されたスレッドに渡します。これで、同じ1つの受信側にそれぞれ異なるメッセージを送る2つのスレッドができます。
このコードを実行すると、出力はおおよそ次のようになります。
Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you
システムによっては、値が別の順序で表示されるかもしれません。これこそが、並行性を興味深くもあり難しくもしている点です。thread::sleep にさまざまな値を与えて異なるスレッドで試してみると、実行ごとの非決定性はさらに高まり、毎回異なる出力が生まれます。
チャネルがどのように動作するかを見てきたので、次は別の並行性の手法を見ていきましょう。
共有状態の並行性
共有状態並行性
メッセージ受け渡しは並行性を扱うための優れた方法ですが、それだけが唯一の方法ではありません。 別の方法として、複数のスレッドが同じ共有データにアクセスするやり方があります。 ここで、Go言語のドキュメントにあるスローガンのこの部分をもう一度考えてみましょう: 「メモリを共有することで通信してはならない」。
メモリ共有による通信とは、どのようなものなのでしょうか。さらに、なぜ メッセージ受け渡しの支持者たちはメモリ共有を使わないよう注意を促すのでしょうか。
ある意味では、どのプログラミング言語においてもチャネルは単一所有権に似ています。 なぜなら、ある値をチャネルに送ったなら、その値はもはや使うべきではないからです。 共有メモリ並行性は、複数所有権のようなものです: 複数のスレッドが同時に同じメモリ位置にアクセスできます。第15章で、スマートポインタが複数所有権を可能にしたときに見たように、複数所有権は 管理すべき異なる所有者が存在するため、複雑さを増します。Rustの型システム と所有権ルールは、この管理を正しく行ううえで大いに役立ちます。例として、共有メモリにおけるより一般的な並行性プリミティブの1つであるミューテックスを見ていきましょう。
Mutexによるアクセス制御
Mutex は mutual exclusion の略で、ミューテックスはある時点で1つの スレッドだけがあるデータにアクセスできるようにします。ミューテックス内の データにアクセスするには、スレッドはまず、ミューテックスのロックを取得したい と要求することで、アクセスしたいことを知らせなければなりません。lock は ミューテックスの一部であるデータ構造で、現在誰がそのデータへの排他的アクセス権を持っているかを追跡します。したがって、ミューテックスはロック機構によって保持しているデータを guarding していると説明されます。
ミューテックスは使うのが難しいという評判があります。なぜなら、次の2つのルールを 覚えておかなければならないからです:
- データを使う前に、必ずロックの取得を試みなければならない。
- ミューテックスが保護しているデータの使用が終わったら、他のスレッドがロックを取得できるように、そのデータのロックを解除しなければならない。
ミューテックスの現実世界での比喩として、1本のマイクしかない会議での パネルディスカッションを想像してください。パネリストが話す前には、まず マイクを使いたいと求めるか、その意思を示さなければなりません。マイクを 受け取ったら、好きなだけ話し、その後で次に発言を求めているパネリストに マイクを渡します。もしパネリストが使い終わった後にマイクを渡し忘れると、 他の誰も話せなくなります。共有マイクの管理がうまくいかなければ、その パネルは計画どおりには進みません!
ミューテックスの管理を正しく行うのは非常に難しいことがあります。そのため、 多くの人がチャネルに熱心なのです。しかし、Rustの型システムと所有権ルールの おかげで、ロックとアンロックを誤ることはありません。
Mutex<T> のAPI
ミューテックスの使い方の例として、まずはリスト16-12に示すように、単一 スレッドの文脈でミューテックスを使うところから始めましょう。
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
}
println!("m = {m:?}");
}
多くの型と同様に、Mutex<T> は関連関数 new を使って作成します。
ミューテックスの内部のデータにアクセスするには、lock メソッドを使って
ロックを取得します。この呼び出しは、ロックを取得する順番が回ってくるまで、
現在のスレッドをブロックして何の作業もできないようにします。
ロックを保持していた別のスレッドがパニックした場合、lock の呼び出しは
失敗します。その場合、誰も二度とロックを取得できなくなるため、ここでは
その状況にあるならこのスレッドもパニックするように、unwrap を使うことにしました。
ロックを取得した後は、この場合 num という名前の戻り値を、内部データへの
可変参照として扱えます。型システムにより、m 内の値を使う前にロックを
取得することが保証されます。m の型は i32 ではなく Mutex<i32> なので、
i32 の値を使うには 必ず lock を呼び出さなければなりません。
忘れることはできません。そうしないと、型システムが内部の i32 への
アクセスを許可しないからです。
lock の呼び出しは MutexGuard という型を返します。これは
LockResult に包まれており、unwrap の呼び出しで処理しました。
MutexGuard 型は、内部データを指すために Deref を実装しています。
この型はさらに Drop も実装しており、MutexGuard がスコープを抜けたとき、
つまり内部スコープの終わりで、自動的にロックを解放します。その結果、
ロックの解放は自動的に行われるため、ロックを解放し忘れてミューテックスが
他のスレッドから使えなくなる危険はありません。
ロックがドロップされた後、ミューテックスの値を出力すると、内部の i32 を
6 に変更できたことがわかります。
Mutex<T> への共有アクセス
次に、Mutex<T> を使って複数のスレッド間で値を共有してみましょう。10個の
スレッドを起動し、それぞれがカウンタの値を1ずつ増やすようにするので、
カウンタは0から10になります。リスト16-13の例ではコンパイラエラーが発生し、
そのエラーを手がかりに Mutex<T> の使い方と、それをRustがどのように正しく
使えるよう助けてくれるかを詳しく見ていきます。
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];
for _ in 0..10 {
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
counter 変数を作成し、リスト16-12で行ったように Mutex<T> の内部に
i32 を保持します。次に、数値の範囲を反復処理して10個のスレッドを作成します。
thread::spawn を使い、すべてのスレッドに同じクロージャを渡します。これは
カウンタをスレッドにムーブし、lock メソッドを呼び出して Mutex<T> の
ロックを取得し、その後ミューテックス内の値に1を加えるクロージャです。
スレッドがそのクロージャの実行を終えると、num はスコープを抜け、ロックを
解放するので、別のスレッドがそれを取得できるようになります。
メインスレッドでは、すべてのjoinハンドルを収集します。それから、リスト16-2で
行ったように、すべてのスレッドが終了することを確認するために各ハンドルに
対して join を呼び出します。その時点で、メインスレッドがロックを取得し、
このプログラムの結果を出力します。
この例はコンパイルできないと示唆していました。では、その理由を見てみましょう!
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: borrow of moved value: `counter`
--> src/main.rs:21:29
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `std::sync::Mutex<i32>`, which does not implement the `Copy` trait
...
8 | for _ in 0..10 {
| -------------- inside of this loop
9 | let handle = thread::spawn(move || {
| ------- value moved into closure here, in previous iteration of loop
...
21 | println!("Result: {}", *counter.lock().unwrap());
| ^^^^^^^ value borrowed here after move
|
help: consider moving the expression out of the loop so it is only moved once
|
8 ~ let mut value = counter.lock();
9 ~ for _ in 0..10 {
10 | let handle = thread::spawn(move || {
11 ~ let mut num = value.unwrap();
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
エラーメッセージは、counter の値がループの前回の反復ですでにムーブされたと
述べています。Rustは、ロック counter の所有権を複数のスレッドにムーブすることはできないと教えてくれています。第15章で説明した複数所有権の方法で、このコンパイラエラーを修正しましょう。
複数スレッドにおける複数所有権
第15章では、スマートポインタ Rc<T> を使って参照カウントされる値を作成する
ことで、ある値を複数の所有者に渡しました。ここでも同じことをして、何が起こるかを見てみましょう。リスト16-14では Mutex<T> を Rc<T> でラップし、スレッドに所有権をムーブする前に Rc<T> をクローンします。
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Rc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
もう一度コンパイルすると……今度は別のエラーが出ます! コンパイラは 多くのことを教えてくれています:
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ------------- ^------
| | |
| ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
| | |
| | required by a bound introduced by this call
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________^ `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
|
= help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc<std::sync::Mutex<i32>>`
note: required because it's used within this closure
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/std/src/thread/mod.rs:723:1
For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
これはずいぶん長いエラーメッセージです! ここで注目すべき重要な部分は
次のとおりです:
`Rc<Mutex<i32>>` cannot be sent between threads safely。コンパイラは、
その理由も教えてくれています: the trait `Send` is not implemented for `Rc<Mutex<i32>>`。Send については次の節で説明します。これは、
スレッドとともに使う型が並行な状況での使用に適していることを保証する
トレイトの 1 つです。
残念ながら、Rc<T> はスレッド間で共有しても安全ではありません。
Rc<T> は参照カウントを管理する際、clone が呼ばれるたびにカウントを
増やし、各クローンがドロップされるとカウントを減らします。しかし、
カウントの変更が別のスレッドによって中断されないことを保証するための
並行性プリミティブは一切使っていません。その結果、カウントが誤る
可能性があります。そうした微妙なバグは、メモリリークや、まだ使い終えて
いない値が早くドロップされてしまうことにつながりかねません。私たちに
必要なのは、Rc<T> とまったく同じようでありながら、参照カウントの
変更をスレッドセーフな方法で行う型です。
Arc<T> によるアトミックな参照カウント
幸い、Arc<T> は、並行な状況で安全に使える Rc<T> のような型です。
a は atomic を表し、つまり アトミックに参照カウントされる 型という
意味です。アトミックは、ここでは詳しく扱わない別種の並行性プリミティブ
です。詳しくは、標準ライブラリの std::sync::atomic
のドキュメントを参照してください。現時点では、アトミックはプリミティブ型
のように動作しつつ、スレッド間で安全に共有できることだけ知っていれば
十分です。
すると、なぜすべてのプリミティブ型がアトミックではなく、なぜ標準
ライブラリの型がデフォルトで Arc<T> を使うよう実装されていないのか、
と不思議に思うかもしれません。その理由は、スレッド安全性には性能上の
コストが伴い、そのコストは本当に必要なときにだけ払いたいからです。
単一スレッド内の値に対して操作しているだけなら、アトミックが提供する
保証を強制しなくてよいぶん、コードはより高速に動作できます。
例に戻りましょう。Arc<T> と Rc<T> は同じ API を持っているので、
use 行、new の呼び出し、clone の呼び出しを変更すれば、私たちの
プログラムを修正できます。リスト 16-15 のコードは、ついにコンパイルして
実行できるようになります。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
このコードは次のように出力します:
Result: 10
やりました! 0 から 10 まで数えられました。あまりすごいことには思えない
かもしれませんが、Mutex<T> とスレッド安全性について多くのことを学べ
ました。このプログラムの構造は、単にカウンタを増やすだけでなく、もっと
複雑な操作にも使えます。この戦略を使えば、計算を独立した部分に分割し、
その各部分をスレッドに振り分け、Mutex<T> を使って各スレッドが自分の
担当分で最終結果を更新するようにできます。
単純な数値演算をしているのであれば、標準ライブラリの
std::sync::atomic モジュール には Mutex<T> より
単純な型が用意されていることにも注意してください。これらの型は、
プリミティブ型に対する安全で並行なアトミックアクセスを提供します。
この例でプリミティブ型に対して Mutex<T> を使うことにしたのは、
Mutex<T> がどのように動くかに集中できるようにするためです。
RefCell<T>/Rc<T> と Mutex<T>/Arc<T> の比較
counter は不変なのに、その中の値への可変参照を取得できたことに気づいた
かもしれません。これは、Cell ファミリーがそうであるように、
Mutex<T> が内部可変性を提供していることを意味します。第 15 章で
Rc<T> の中身を変更できるように RefCell<T> を使ったのと同じように、
Arc<T> の中身を変更するために Mutex<T> を使います。
もう 1 つ注意すべき点は、Mutex<T> を使うとき、Rust はあらゆる種類の
ロジックエラーからあなたを守ってくれるわけではないということです。
第 15 章で、Rc<T> を使うと参照サイクルを作ってしまう危険があり、2 つの
Rc<T> の値が互いを参照し合うことでメモリリークが起きる可能性があると
学びました。同様に、Mutex<T> には デッドロック を引き起こす危険が
あります。これは、ある操作が 2 つのリソースをロックする必要があり、
2 つのスレッドがそれぞれ片方のロックを獲得した結果、互いを永遠に待ち
続けるようになったときに起こります。デッドロックに興味があるなら、
デッドロックを起こす Rust プログラムを作ってみてください。その後、
どんな言語でもよいのでミューテックスのデッドロック軽減戦略を調べ、
Rust で実装してみましょう。標準ライブラリの Mutex<T> と MutexGuard
の API ドキュメントにも、有用な情報があります。
この章の締めくくりとして、Send トレイトと Sync トレイト、そして
それらをカスタム型とともにどう使えるかについて説明します。
Send と Sync による拡張可能な並行性
Send と Sync による拡張可能な並行性
興味深いことに、この章でここまで扱ってきた並行性機能のほとんどは、 言語そのものではなく標準ライブラリの一部でした。並行性を扱うための 選択肢は、言語や標準ライブラリに限られません。独自の並行性機能を 書くこともできますし、ほかの人が書いたものを使うこともできます。
しかし、標準ライブラリではなく言語に埋め込まれている並行性の重要な
概念の中には、std::marker トレイトである Send と Sync が
あります。
スレッド間で所有権を転送する
Send マーカートレイトは、Send を実装する型の値の所有権を
スレッド間で転送できることを示します。ほとんどすべての Rust の型は
Send を実装していますが、いくつか例外があり、そのひとつが Rc<T>
です。これは Send を実装できません。なぜなら、Rc<T> の値を
クローンして、そのクローンの所有権を別のスレッドに転送しようとすると、
両方のスレッドが同時に参照カウントを更新してしまう可能性があるからです。
このため、Rc<T> は、スレッドセーフ性のための性能上のペナルティを
払いたくないシングルスレッドの状況で使うために実装されています。
したがって、Rust の型システムとトレイト境界により、Rc<T> の値を
安全でない形で誤ってスレッド間で送ってしまうことは決してありません。
これをリスト16-14で試したとき、the trait `Send` is not implemented for `Rc<Mutex<i32>>` というエラーが出ました。Send を実装している
Arc<T> に切り替えると、コードはコンパイルできました。
Send 型だけで完全に構成される任意の型も、自動的に Send として
マークされます。第20章で説明する生ポインタを除き、ほとんどすべての
プリミティブ型は Send です。
複数のスレッドからアクセスする
Sync マーカートレイトは、Sync を実装する型が複数のスレッドから
参照されても安全であることを示します。言い換えると、&T
(T へのイミュータブル参照)が Send を実装していれば、任意の型 T
は Sync を実装します。これは、その参照を別のスレッドへ安全に送れる
ことを意味します。Send と同様に、すべてのプリミティブ型は Sync を
実装しており、Sync を実装する型だけで完全に構成される型も Sync を
実装します。
スマートポインタ Rc<T> も、Send を実装しないのと同じ理由で
Sync を実装しません。RefCell<T> 型(第15章で扱いました)と、
それに関連する Cell<T> 型の一群も Sync を実装しません。
RefCell<T> が実行時に行う借用チェックの実装はスレッドセーフでは
ありません。スマートポインタ Mutex<T> は Sync を実装しており、
「Mutex<T> への共有アクセス」 で見たように、
複数のスレッド間でアクセスを共有するために使えます。
Send と Sync を手動で実装するのは unsafe である
Send トレイトと Sync トレイトを実装するほかの型だけで完全に
構成される型も、自動的に Send と Sync を実装するため、これらの
トレイトを手動で実装する必要はありません。マーカートレイトであるため、
実装すべきメソッドすらありません。これらは、並行性に関する不変条件を
強制するのに役立つだけです。
これらのトレイトを手動で実装するには、unsafe な Rust コードを実装する
必要があります。unsafe な Rust コードの使い方については第20章で
説明します。今のところ重要なのは、Send と Sync の部品から
構成されていない新しい並行型を作るには、安全性の保証を保つために
慎重な検討が必要だということです。『The Rustonomicon』 には、
これらの保証と、それをどのように維持するかについての詳しい情報が
あります。
まとめ
この本で並行性を見るのはこれで最後ではありません。次の章では async プログラミングに焦点を当て、第21章のプロジェクトでは、この章の概念を、 ここで扱った小さな例よりも現実的な状況で使います。
前にも触れたように、Rust が並行性をどのように扱うかのうち、言語そのものの 一部であるものはごくわずかなので、多くの並行性ソリューションはクレートとして 実装されています。これらは標準ライブラリよりも速く進化するため、 マルチスレッドの状況で使う最新の最先端クレートについては、必ずオンラインで 調べるようにしてください。
Rust の標準ライブラリは、メッセージ受け渡しのためのチャネルと、
Mutex<T> や Arc<T> のような、並行コンテキストで安全に使える
スマートポインタ型を提供しています。型システムと借用チェッカーにより、
これらのソリューションを使うコードがデータ競合や無効な参照を抱えることは
ありません。いったんコードがコンパイルできれば、他の言語でよくある、
追跡しづらい種類のバグなしに、それが複数のスレッドで問題なく動作すると
安心してよいでしょう。並行プログラミングは、もはや恐れるべき概念では
ありません。さあ、恐れることなくプログラムを並行にしましょう!
非同期プログラミングの基礎: async、await、Future、Stream
コンピューターに実行させる操作の多くは、完了までに時間がかかることがあります。そうした長時間実行される処理の完了を待つ間に、別のことができると便利です。現代のコンピューターでは、複数の操作を同時に扱うための手法として、並列性と並行性の 2 つがあります。しかし、私たちが書くプログラムのロジックは、たいてい直線的な形になっています。プログラムが実行すべき操作と、関数がいったん停止してその代わりにプログラムの別の部分を実行できる地点を記述でき、しかもコードの各部分をどの順序でどのように実行するかをあらかじめ正確に指定しなくて済むのが望ましいところです。非同期プログラミング は、停止し得る地点と最終的に得られる結果という観点でコードを表現できるようにし、その調整の詳細を私たちに代わって処理してくれる抽象化です。
この章では、第 16 章で扱った、スレッドを使って並列性と並行性を実現する方法を踏まえつつ、コードを書くための別のアプローチを導入します。すなわち、操作がどのように非同期になり得るかを表現する Rust の Future、Stream、および async と await の構文、そして非同期ランタイム、つまり非同期操作の実行を管理し調整するコードを実装するサードパーティ製クレートです。
例を考えてみましょう。家族のお祝いの様子を撮影した動画を書き出しているとします。この操作は、完了までに数分から数時間かかることがあります。動画の書き出しは、利用可能な限り CPU と GPU の処理能力を使います。CPU コアが 1 つしかなく、オペレーティングシステムがその書き出しを完了まで中断しない、つまり 同期的に 実行するとしたら、そのタスクの実行中はコンピューターで他のことは何もできません。それはかなりストレスのたまる体験でしょう。幸い、コンピューターのオペレーティングシステムは、その書き出しを見えない形で十分な頻度で中断して、同時に別の作業を進められるようにしてくれます。
では次に、別の誰かが共有した動画をダウンロードしているとしましょう。これも時間がかかることがありますが、それほど多くの CPU 時間は使いません。この場合、CPU はネットワークからデータが届くのを待たなければなりません。データが届き始めたら読み始めることはできますが、全体がそろうまでには時間がかかるかもしれません。データがすべてそろってからでも、動画がかなり大きければ、それをすべて読み込むのに少なくとも 1、2 秒かかることがあります。それほど長くは聞こえないかもしれませんが、毎秒何十億もの操作を実行できる現代のプロセッサにとっては、非常に長い時間です。ここでも、オペレーティングシステムはプログラムを見えない形で中断し、ネットワーク呼び出しの完了を待つ間に CPU が別の処理を行えるようにします。
動画の書き出しは、CPU バウンド または 計算バウンド な操作の一例です。これは、CPU や GPU 内でのコンピューターの潜在的なデータ処理速度と、その速度のうちどれだけをその操作に割り当てられるかによって制限されます。動画のダウンロードは I/O バウンド な操作の一例です。これはコンピューターの 入出力 の速度によって制限され、ネットワーク越しにデータを送れる速さまでしか進めないからです。
どちらの例でも、オペレーティングシステムによる見えない割り込みが、並行性の一形態を提供しています。ただし、その並行性が起きるのはプログラム全体のレベルに限られます。オペレーティングシステムはあるプログラムを中断して、ほかのプログラムに処理を進めさせるのです。多くの場合、私たちはオペレーティングシステムよりもはるかに細かい粒度で自分たちのプログラムを理解しているため、オペレーティングシステムには見えない並行実行の機会を見つけられます。
たとえば、ファイルのダウンロードを管理するツールを作っているなら、1 つのダウンロードを開始しても UI が固まらないようにプログラムを書けるべきですし、ユーザーは同時に複数のダウンロードを開始できるべきです。ところが、ネットワークを扱うための多くのオペレーティングシステム API は ブロッキング です。つまり、処理対象のデータが完全に準備できるまで、プログラムの進行を止めてしまいます。
注: 考えてみれば、これは ほとんどの 関数呼び出しがそうであるのと同じです。ただし、blocking という用語は通常、ファイル、ネットワーク、またはコンピューター上のほかのリソースとやり取りする関数呼び出しに対して使われます。そうした場合こそ、個々のプログラムがその操作から _非_ブロッキングであることの恩恵を受けられるからです。
各ファイルのダウンロード用に専用スレッドを生成すれば、メインスレッドがブロックされるのは避けられます。しかし、それらのスレッドが使うシステムリソースのオーバーヘッドは、やがて問題になります。そもそも呼び出し自体がブロックしないのが望ましく、その代わりに、プログラムに完了させたい複数のタスクを定義し、それらをどの順序でどのように実行するのが最適かをランタイムに選ばせられるとよいでしょう。
それこそが、Rust の async(asynchronous の略)という抽象化が私たちに与えてくれるものです。この章では、次のトピックを通して async について詳しく学びます。
- Rust の
asyncおよびawait構文の使い方と、ランタイムを使って非同期関数を実行する方法 - async モデルを使って、第 16 章で見たものと同種のいくつかの課題を解決する方法
- マルチスレッディングと async がどのように相補的な解決策を提供し、多くの場合に組み合わせて使えるか
ただし、async が実際にどのように機能するかを見る前に、少し寄り道をして並列性と並行性の違いを説明する必要があります。
並列性と並行性
これまでは、並列性と並行性をほぼ同じ意味のものとして扱ってきました。ここから作業を始めるにあたっては、その違いが表れてくるので、より正確に区別する必要があります。
ソフトウェアプロジェクトでチームが作業を分担するさまざまな方法を考えてみましょう。1 人のメンバーに複数のタスクを割り当てることもできますし、各メンバーに 1 つずつタスクを割り当てることもできますし、その 2 つを組み合わせることもできます。
1 人の人が、どのタスクも完了する前に複数の異なるタスクに取り組む場合、それは 並行性 です。並行性を実現する 1 つの方法は、コンピューター上に 2 つの異なるプロジェクトをチェックアウトしておき、片方のプロジェクトに飽きたり行き詰まったりしたら、もう片方に切り替えるのに似ています。自分は 1 人しかいないので、両方のタスクをまったく同時に進めることはできませんが、切り替えながら片方ずつ進めることで、マルチタスクとして前進できます(図 17-1 を参照)。
チームが、一連のタスクを各メンバーが 1 つずつ引き受けてそれぞれ単独で取り組む形で分担する場合、それは 並列性 です。チームの各人が、まったく同時に前進できます(図 17-2 を参照)。
これら2つのワークフローのどちらでも、異なるタスク間で調整が必要になる かもしれません。ある人に割り当てたタスクはほかの全員の作業から完全に 独立していると思っていたのに、実際にはチーム内の別の人が先に自分の タスクを終える必要がある、ということもあります。作業の一部は並列に 進められても、別の一部は実際には 直列 です。図17-3のように、 一連の流れとして、1つのタスクの後に別のタスクが続く形でしか進みません。
同様に、自分のタスクの1つが別の自分のタスクに依存していることに 気づくかもしれません。すると、自分の並行した作業も直列になります。
並列性と並行性は、互いに交わることもあります。あなたのタスクの1つを 終えるまで同僚の作業が止まっていると分かったら、その同僚の作業の “ブロックを解除”するために、そのタスクに全力を集中するでしょう。あなた と同僚はもはや並列に作業できず、自分自身のタスクも並行して進められません。
同じ基本的な力学は、ソフトウェアとハードウェアでも当てはまります。CPU コアが1つしかないマシンでは、CPUは一度に1つの操作しか実行できませんが、 それでも並行して動作できます。スレッド、プロセス、async などの 手段を使うことで、コンピュータはある処理を一時停止して別の処理に 切り替え、最終的には再び最初の処理へ戻ることができます。CPUコアが 複数あるマシンでは、並列に作業することもできます。あるコアが1つの タスクを実行している一方で、別のコアがまったく無関係な別のタスクを 実行でき、それらの操作は実際に同時に起こります。
Rust で async コードを実行すると、通常は並行に行われます。ハードウェア、 オペレーティングシステム、そして使用している async ランタイム (async ランタイムについてはこのあとすぐ詳しく説明します)によっては、 その並行性が内部的に並列性も利用している場合があります。
では、Rust の async プログラミングが実際にどのように動作するのかを見ていきましょう。
FutureとAsync構文
Futureと非同期構文
Rustにおける非同期プログラミングの主要な要素は、future と、Rustの async および await キーワードです。
future とは、今は準備できていないかもしれないものの、将来のある時点で準備できる値のことです。(この同じ概念は多くの言語に見られ、task や promise など別の名前で呼ばれることもあります。)Rustは、共通のインターフェイスを持ちながら異なるデータ構造でさまざまな非同期操作を実装できるようにするための構成要素として、Future トレイトを提供しています。Rustでは、futureとは Future トレイトを実装する型のことです。各futureは、これまでにどこまで進んだか、そして「ready」が何を意味するかについての独自の情報を保持しています。
ブロックや関数に async キーワードを適用すると、それらが中断および再開可能であることを指定できます。asyncブロックまたはasync関数の内部では、await キーワードを使って futureを待機する(つまり、それがreadyになるまで待つ)ことができます。asyncブロックまたは関数の中でfutureを待機する箇所はどこでも、そのブロックや関数が一時停止および再開する可能性のある地点です。futureに対して、その値がまだ利用可能かどうかを確認する処理は ポーリング と呼ばれます。
C#やJavaScriptなどの他の言語でも、非同期プログラミングのために async と await キーワードを使います。これらの言語に慣れているなら、Rustがこの構文を扱う方法にかなり大きな違いがあることに気づくかもしれません。それには、これから見るように、ちゃんとした理由があります。
非同期Rustを書くとき、私たちはほとんどの場合 async と await キーワードを使います。Rustは、それらを Future トレイトを使った等価なコードにコンパイルします。これは、for ループを Iterator トレイトを使った等価なコードにコンパイルするのと同じです。ただし、Rustは Future トレイトを提供しているので、必要に応じて自分自身のデータ型に対してそれを実装することもできます。この章を通して見ていく関数の多くは、独自の Future 実装を持つ型を返します。章の終わりにトレイトの定義へ戻り、その仕組みをさらに掘り下げますが、先に進むにはこの程度の詳細で十分です。
ここまでの話は少し抽象的に感じられるかもしれないので、最初の非同期プログラムを書いてみましょう。小さなWebスクレイパーです。コマンドラインから2つのURLを受け取り、その両方を並行して取得し、先に完了した方の結果を返します。この例にはかなり多くの新しい構文が出てきますが、心配はいりません。必要なことは進みながらすべて説明します。
最初の非同期プログラム
この章では、エコシステムのさまざまな要素をやりくりすることではなく、非同期を学ぶことに集中できるように、私たちは trpl クレート(trpl は “The Rust Programming Language” の略です)を作成しました。これは、主に futures クレートと tokio クレートから、この章で必要になる型、トレイト、関数をすべて再エクスポートしています。futures クレートは、Rustにおける非同期コード実験のための公式な場であり、実際に Future トレイトが最初に設計された場所でもあります。Tokioは、今日のRustで最も広く使われている非同期ランタイムで、とりわけWebアプリケーションでよく使われています。他にも優れたランタイムはありますし、用途によってはそちらの方が適しているかもしれません。trpl では内部的に tokio クレートを使っていますが、それは十分にテストされていて広く使われているからです。
場合によっては、trpl はこの章に関係する詳細に集中できるよう、元のAPIに名前を付け直したりラップしたりもしています。このクレートが何をしているのかを理解したければ、そのソースコードを確認することをおすすめします。各再エクスポートがどのクレートから来ているのかを見ることができますし、このクレートが何をしているかを説明する豊富なコメントも残してあります。
hello-async という名前の新しいバイナリプロジェクトを作成し、依存関係として trpl クレートを追加してください。
$ cargo new hello-async
$ cd hello-async
$ cargo add trpl
これで、trpl が提供するさまざまな部品を使って最初の非同期プログラムを書けます。2つのWebページを取得し、それぞれから <title> 要素を取り出し、その一連の処理全体を先に終えたページのタイトルを表示する小さなコマンドラインツールを作ります。
page_title関数の定義
まずは、1つのページURLを引数に取り、それにリクエストを行い、<title> 要素のテキストを返す関数を書いてみましょう(リスト17-1を参照)。
extern crate trpl; // required for mdbook test
fn main() {
// TODO: we'll add this next!
}
use trpl::Html;
async fn page_title(url: &str) -> Option<String> {
let response = trpl::get(url).await;
let response_text = response.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
まず、page_title という名前の関数を定義し、それに async キーワードを付けます。次に、trpl::get 関数を使って渡されたURLを取得し、await キーワードを付けてレスポンスを待機します。response のテキストを取得するために、その text メソッドを呼び出し、もう一度 await キーワードで待機します。これら2つのステップはいずれも非同期です。get 関数では、サーバーがレスポンスの最初の部分を送り返してくるのを待たなければなりません。この部分にはHTTPヘッダーやクッキーなどが含まれており、レスポンスボディとは別に届くことがあります。特にボディが非常に大きい場合には、すべてが到着するまでに時間がかかることがあります。レスポンスの 全体 が届くまで待つ必要があるため、text メソッドもasyncです。
これら2つのfutureはどちらも明示的に待機しなければなりません。なぜなら、Rustのfutureは 遅延的 だからです。await キーワードで求めるまで、何もしません。(実際、futureを使わないと、Rustはコンパイラ警告を表示します。)これは、第13章の「イテレータで一連の要素を処理する」節でのイテレータの説明を思い出させるかもしれません。イテレータは、next メソッドを呼び出さない限り何もしません。直接呼び出す場合でも、内部で next を使う for ループや map のようなメソッドを通じて呼び出す場合でも同じです。同様に、futureも明示的に求めない限り何もしません。この遅延性によって、Rustは実際に必要になるまで非同期コードを実行しないで済みます。
注: これは、第16章の「spawnで新しいスレッドを作る」 節で
thread::spawnを使ったときに見た挙動とは異なります。そこでは、 別のスレッドに渡したクロージャがすぐに実行を開始しました。また、 多くの他の言語における非同期へのアプローチとも異なります。しかし、 イテレータの場合と同じく、Rustがその性能保証を提供できるようにする ためには、これが重要なのです。response_textを取得したら、Html::parseを使ってそれをHtml型のインスタンスにパースできます。これで、生の文字列ではなく、より豊かな データ構造として HTML を扱うためのデータ型が手に入ります。特に、select_firstメソッドを使うと、指定した CSS セレクタに一致する最初の 要素を見つけられます。文字列"title"を渡すと、ドキュメント内に存在す れば最初の<title>要素を取得できます。一致する要素が存在しない可能性が あるため、select_firstはOption<ElementRef>を返します。最後に、Option::mapメソッドを使います。これにより、Optionに値が存在するとき はその項目を処理し、存在しないときは何もしないようにできます。(ここではmatch式を使うこともできますが、mapの方がより慣用的です。)mapに 渡す関数の本体では、titleに対してinner_htmlを呼び出してその内容を 取得します。これはStringです。最終的に得られるのはOption<String>です。
Rust の await キーワードは、待機する式の 後ろ に置かれ、前には置かれ
ません。つまり、これは 後置 のキーワードです。他の言語で async を使っ
たことがあるなら、これは慣れているものと異なるかもしれませんが、Rust で
はこのおかげでメソッドチェーンがずっと扱いやすくなります。その結果、
リスト17-2に示すように、page_title の本体を変更して、trpl::get と
text の呼び出しを、その間に await を挟みながら連結できます。
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
// TODO: we'll add this next!
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
これで、最初の async 関数をうまく書くことができました! main にそれを呼
び出すコードを追加する前に、ここまでに書いたものと、それが何を意味するの
かについてもう少し見ていきましょう。
Rust は async キーワードの付いた ブロック を見ると、それを Future
トレイトを実装する固有の無名データ型にコンパイルします。Rust は async
の付いた 関数 を見ると、その本体が async ブロックである非 async 関数に
コンパイルします。async 関数の戻り値の型は、コンパイラがその async ブロッ
クのために生成する無名データ型の型です。
したがって、async fn と書くことは、戻り値型に対応する future を返す
関数を書くことと等価です。コンパイラから見ると、リスト17-1の
async fn page_title のような関数定義は、おおよそ次のような非 async 関
数定義と等価です。
#![allow(unused)]
fn main() {
extern crate trpl; // mdbook のテストに必要
use std::future::Future;
use trpl::Html;
fn page_title(url: &str) -> impl Future<Output = Option<String>> {
async move {
let text = trpl::get(url).await.text().await;
Html::parse(&text)
.select_first("title")
.map(|title| title.inner_html())
}
}
}
変換後のバージョンの各部分を順に見ていきましょう。
- 第10章の 「パラメータとしてのトレイト」
節で説明した
impl Trait構文を使っています。 - 返される値は
Futureトレイトを実装しており、その関連型はOutputで す。Output型がOption<String>であることに注目してください。これ は、page_titleのasync fnバージョンにおける元の戻り値型と同じで す。 - 元の関数本体で呼び出されていたコードはすべて、
async moveブロックで 包まれています。ブロックは式であることを思い出してください。このブロッ ク全体が、その関数から返される式です。 - この async ブロックは、先ほど説明したとおり
Option<String>型の値を 生成します。その値は、戻り値型にあるOutput型と一致します。これは、 これまでに見てきた他のブロックと同じです。 - 新しい関数本体が
async moveブロックになっているのは、urlパラメー タの使い方によるものです。(asyncとasync moveの違いについては、 この章の後の方でさらに詳しく扱います。)
これで、main から page_title を呼び出せるようになります。
ランタイムで async 関数を実行する
まずは、リスト17-3に示すように、1つのページのタイトルを取得します。残念 ながら、このコードはまだコンパイルできません。
extern crate trpl; // required for mdbook test
use trpl::Html;
async fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
コマンドライン引数を取得するために、第12章の
「コマンドライン引数を受け取る」 節で使ったの
と同じパターンに従います。次に、その URL 引数を page_title に渡し、結
果を await します。future が生成する値は Option<String> なので、その
ページに <title> があったかどうかに応じて異なるメッセージを表示するた
めに、match 式を使います。
await キーワードを使える場所は async 関数または async ブロックの中だけ
であり、Rust では特別な main 関数を async としてマークできません。
error[E0752]: `main` function is not allowed to be `async`
--> src/main.rs:6:1
|
6 | async fn main() {
| ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`
main を async としてマークできない理由は、async コードには
runtime、つまり非同期コード実行の詳細を管理する Rust クレートが必要だ
からです。プログラムの main 関数はランタイムを 初期化 することはでき
ますが、それ自体がランタイム そのもの ではありません。(なぜそうなのか
については、少し後でさらに見ていきます。)async コードを実行する Rust プ
ログラムにはどれにも、future を実行するランタイムをセットアップする場所が
少なくとも1つあります。
async をサポートするほとんどの言語はランタイムを同梱していますが、Rust はそうではありません。その代わりに、さまざまな async ランタイムが利用で き、それぞれが対象とするユースケースに適した異なるトレードオフを持ってい ます。たとえば、多数の CPU コアと大量の RAM を備えた高スループットの Web サーバーは、単一コアで RAM が少なく、ヒープ確保もできないマイクロコント ローラとはまったく異なる要件を持っています。そうしたランタイムを提供する クレートは、ファイル I/O やネットワーク I/O のような一般的機能の async 版もよく提供しています。
ここでは、この章の残りを通して trpl クレートの block_on 関数を使いま
す。この関数は future を引数に取り、その future が完了まで実行されるまで
現在のスレッドをブロックします。内部では、block_on を呼び出すと、渡さ
れた future を実行するために tokio クレートを使ったランタイムがセット
アップされます(trpl クレートの block_on の振る舞いは、他のランタイ
ムクレートの block_on 関数と似ています)。future が完了すると、
block_on はその future が生成した値をそのまま返します。
page_title が返す Future を block_on に直接渡し、
それが完了したら、リスト17-3でやろうとしたように、結果として得られる
Option<String> に対して match することもできます。しかし、この章の
ほとんどの例では(そして実際の世界のほとんどの async コードでも)、
単なる 1 回の async 関数呼び出し以上のことを行うので、代わりに
async ブロックを渡し、リスト17-4のように page_title 呼び出しの
結果を明示的に await します。
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::block_on(async {
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
})
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
このコードを実行すると、最初に期待していた動作が得られます。
$ cargo run -- "https://www.rust-lang.org"
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/async_await 'https://www.rust-lang.org'`
The title for https://www.rust-lang.org was
Rust Programming Language
ふう、ようやく動く async コードができました! しかし、2 つのサイトを 競争させるコードを追加する前に、Future がどのように動くのかに少しだけ 注意を戻しましょう。
各 await ポイント、つまりコードが await
キーワードを使うすべての場所は、制御がランタイムに返される地点を表します。
これを可能にするために、Rust は async ブロックに関わる状態を追跡しておく
必要があります。そうすることで、ランタイムは別の作業を開始し、最初の処理を
もう一度進められる準備ができたときにそこへ戻ってこられます。これは、
各 await ポイントで現在の状態を保存するために、次のような enum を
書いたかのような、目に見えない状態機械です。
#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test
enum PageTitleFuture<'a> {
Initial { url: &'a str },
GetAwaitPoint { url: &'a str },
TextAwaitPoint { response: trpl::Response },
}
}
しかし、各状態の間を遷移させるコードを手で書くのは面倒で、 間違いも起こりやすくなります。特に、あとでコードにさらに多くの機能や 状態を追加する必要がある場合はなおさらです。幸い、Rust コンパイラは async コード用の状態機械データ構造を自動的に作成して管理してくれます。 データ構造に関する通常の借用規則と所有権規則は引き続きすべて適用されますが、 うれしいことに、コンパイラはそれらのチェックも私たちの代わりに行い、 役に立つエラーメッセージも提供してくれます。この章の後半で、 そのいくつかを見ていきます。
最終的には、この状態機械を実行する何かが必要であり、それが ランタイムです。(ランタイムについて調べていると エグゼキュータ という 言葉を目にすることがあるのはこのためです。エグゼキュータは、async コードを 実行する責任を持つランタイムの一部です。)
これで、リスト17-3でコンパイラが main 自体を async 関数にすることを
止めた理由がわかるでしょう。もし main が async 関数だったら、
main が返す Future が何であれ、その状態機械を別の何かが管理する必要が
あります。しかし main はプログラムの開始地点です! その代わりに、
main の中で trpl::block_on 関数を呼び出してランタイムを設定し、
async ブロックが返す Future を完了するまで実行しました。
注: 一部のランタイムはマクロを提供しているので、async
main関数を実際に書くこともできます。そうしたマクロはasync fn main() { ... }を通常のfn mainに書き換えます。これは、リスト17-4で手作業で行ったのと 同じことです。つまり、trpl::block_onのように Future を完了まで実行する 関数を呼び出します。
では、これらの要素をまとめて、どのように並行コードを書けるかを見ていきましょう。
2つの URL を並行に競争させる
リスト17-5では、コマンドラインから渡された 2 つの異なる URL に対して
page_title を呼び出し、先に完了した方の Future を選ぶことで
それらを競争させます。
extern crate trpl; // required for mdbook test
use trpl::{Either, Html};
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::block_on(async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);
let (url, maybe_title) =
match trpl::select(title_fut_1, title_fut_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};
println!("{url} returned first");
match maybe_title {
Some(title) => println!("Its page title was: '{title}'"),
None => println!("It had no title."),
}
})
}
async fn page_title(url: &str) -> (&str, Option<String>) {
let response_text = trpl::get(url).await.text().await;
let title = Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html());
(url, title)
}
まず、ユーザーから渡された各 URL に対して page_title を呼び出します。
その結果得られる Future を title_fut_1 と title_fut_2 として保存します。
覚えておいてください。Future は遅延評価であり、まだ await していないので、
この時点ではまだ何も行いません。次に、それらの Future を trpl::select に
渡します。これは、渡された Future のうちどれが最初に完了したかを示す値を
返します。
注: 内部的には、
trpl::selectはfuturesクレートで定義されている、 より汎用的なselect関数の上に構築されています。futuresクレートのselect関数はtrpl::select関数にはできない多くのことを行えますが、 今は飛ばしてよい追加の複雑さもいくつかあります。
どちらの Future が「勝って」もまったく自然なので、Result を返すのは
適切ではありません。代わりに、trpl::select はまだ見たことのない型
trpl::Either を返します。Either 型は、2 つのケースを持つという点で
Result に少し似ています。しかし、Result とは違って、Either には
成功や失敗という概念は組み込まれていません。代わりに、Left と Right を
使って「どちらか一方」を示します。
#![allow(unused)]
fn main() {
enum Either<A, B> {
Left(A),
Right(B),
}
}
select 関数は、最初の引数が勝った場合にはその Future の出力とともに
Left を返し、そちら が勝った場合には 2 番目の Future 引数の出力とともに
Right を返します。これは、関数を呼び出すときに引数が現れる順序と
一致しています。最初の引数は、2 番目の引数の左側にあります。
また、page_title を更新して、渡されたのと同じ URL を返すようにします。
そうすれば、先に返ってきたページに取得できる <title> がなかったとしても、
意味のあるメッセージを表示できます。その情報が使えるようになったので、
最後に println! の出力を更新し、どの URL が最初に完了したのかと、
その URL の Web ページにどのような <title> があるのか(あれば)を
示すようにします。
これで、小さくても動く Web スクレイパーができました! いくつか URL を選んで、 このコマンドラインツールを実行してみてください。あるサイトは他のサイトよりも 一貫して速いこともあれば、別の場合には、どのサイトが速いかが実行のたびに 変わることもあるでしょう。さらに重要なのは、Future を扱うための基本を 学んだので、これで async で何ができるのかをさらに深く掘り下げられることです。
Asyncで並行性を実現する
async を使った並行性の適用
この節では、第16章でスレッドを使って取り組んだものと同じ並行性の課題のいくつかに async を適用します。そこで多くの重要な考え方についてはすでに説明したので、 この節ではスレッドと future の違いに焦点を当てます。
多くの場合、async を使って並行性を扱うための API は、スレッドを使うための API と 非常によく似ています。別の場合には、かなり異なるものになります。スレッドと async の間で API が似て 見える 場合であっても、振る舞いが異なることが多く、 そしてほとんどの場合、パフォーマンス特性は異なります。
spawn_task で新しいタスクを作成する
第16章の 「spawn で新しいスレッドを作る」節で
最初に扱った操作は、2 つの別々のスレッドで数を数え上げることでした。同じことを
async を使ってやってみましょう。trpl クレートは、thread::spawn API と
非常によく似た spawn_task 関数と、thread::sleep API の async 版である
sleep 関数を提供しています。これらを組み合わせて、リスト 17-6 に示すように
カウントの例を実装できます。
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
trpl::spawn_task(async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
}
出発点として、トップレベルの関数を async にできるように、main 関数を
trpl::block_on で設定します。
注: この章ではここから先、どの例でも
mainにまったく同じtrpl::block_onのラップ用コードを含めます。そのため、mainの場合と同じように、 しばしばそれを省略します。自分のコードには必ず含めるようにしてください!
次に、そのブロック内に 2 つのループを書きます。どちらにも trpl::sleep の
呼び出しが含まれており、次のメッセージを送る前に 0.5 秒(500 ミリ秒)待機します。
一方のループを trpl::spawn_task の本体に入れ、もう一方をトップレベルの for
ループに入れます。また、sleep の呼び出しの後に await も追加します。
このコードの振る舞いはスレッドベースの実装と似ています。これを実行したときに、 自分のターミナルではメッセージの表示順が異なる可能性がある点も同じです。
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
この版は、メインの async ブロック本体にある for ループが終了するとすぐに止まります。
これは、spawn_task で生成したタスクが、main 関数の終了時に停止されるからです。
タスクが最後まで完了するまで実行させたい場合は、join ハンドルを使って最初のタスクの
完了を待つ必要があります。スレッドでは、スレッドの実行が終わるまで「ブロック」するために
join メソッドを使いました。リスト 17-7 では、タスクハンドル自体が future なので、
同じことを await で行えます。その Output 型は Result なので、await した後で
それに対して unwrap も行います。
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let handle = trpl::spawn_task(async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
handle.await.unwrap();
});
}
この更新版では、両方の ループが終了するまで実行されます。
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
ここまでを見ると、async とスレッドは、構文が違うだけで似た結果をもたらすように見えます。
つまり、join ハンドルに対して join を呼び出す代わりに await を使い、
sleep の呼び出しも await します。
より大きな違いは、これを行うために別のオペレーティングシステムのスレッドを
生成する必要がなかったことです。実際、ここではタスクを spawn する必要すらありません。
async ブロックは無名の future にコンパイルされるため、それぞれのループを
async ブロックに入れ、ランタイムに trpl::join 関数を使って両方を完了まで
実行させることができます。
第16章の 「すべてのスレッドの終了を待つ」節では、
std::thread::spawn を呼び出したときに返される JoinHandle 型に対して
join メソッドを使う方法を示しました。trpl::join 関数はこれに似ていますが、
future 用です。これに 2 つの future を渡すと、両方が完了した時点で、渡した各 future の
出力を含むタプルを出力とする 1 つの新しい future を生成します。したがって、
リスト 17-8 では trpl::join を使って fut1 と fut2 の両方が終わるのを待ちます。
fut1 と fut2 を await するのではなく、trpl::join が生成する新しい future を
await します。出力は 2 つの unit 値を含むタプルにすぎないので、これを無視します。
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let fut1 = async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
};
let fut2 = async {
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
};
trpl::join(fut1, fut2).await;
});
}
これを実行すると、両方の future が完了まで実行されることがわかります。
hi number 1 from the first task!
hi number 1 from the second task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
これで、毎回まったく同じ順序になることがわかるでしょう。これは、
スレッドやリスト17-7の trpl::spawn_task で見たものとは大きく異なり
ます。これは、trpl::join 関数が_公平_だからです。つまり、各フュー
チャーを同じ頻度で確認し、それらを交互に切り替え、一方の準備ができて
いるなら他方だけが先に進みすぎることはありません。スレッドでは、どの
スレッドを確認し、どれだけ長く実行させるかをオペレーティングシステム
が決定します。async Rust では、どのタスクを確認するかをランタイムが決
定します。(実際には、asyncランタイムは並行性を管理する方法の一部とし
て内部でオペレーティングシステムのスレッドを使うことがあるため、詳細
は複雑になります。そのため、公平性を保証するのはランタイムにとってよ
り手間のかかる作業になる場合があります――それでも可能です!)ランタ
イムは、任意の操作について公平性を保証しなければならないわけではなく、
公平性が必要かどうかを選べるように、異なるAPIを提供していることもよ
くあります。
フューチャーを await する方法を次のように変えて、どうなるか試してみて ください。
- どちらか一方、または両方のループの周囲からasyncブロックを取り除く。
- 各asyncブロックを定義した直後に await する。
- 最初のループだけをasyncブロックで囲み、2番目のループ本体の後で、そ の結果のフューチャーを await する。
追加の挑戦として、コードを実行する_前に_、それぞれの場合の出力がどう なるか考えてみてください!
メッセージパッシングを使って2つのタスク間でデータを送る
フューチャー間でデータを共有する方法も、見覚えがあるでしょう。再び メッセージパッシングを使いますが、今回は型と関数のasync版を使います。 スレッドベースの並行性とフューチャーベースの並行性の重要な違いをいく つか示すため、16章の「メッセージパッシングでスレッド間でデータを転送 する」節でたどったのとは少し 異なる道筋をたどります。リスト17-9では、単一のasyncブロックだけから始 めます。別のスレッドを生成したときのように、別のタスクを_生成しませ ん_。
extern crate trpl; // required for mdbook test
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let val = String::from("hi");
tx.send(val).unwrap();
let received = rx.recv().await.unwrap();
println!("received '{received}'");
});
}
ここでは、16章でスレッドとともに使ったマルチプロデューサー・シングル
コンシューマーのチャネルAPIのasync版である trpl::channel を使いま
す。APIのasync版はスレッドベース版と少しだけ異なります。つまり、不変
ではなく可変のレシーバー rx を使い、recv メソッドは値を直接生成す
る代わりに、await が必要なフューチャーを生成します。これで、送信側か
ら受信側へメッセージを送れます。別のスレッドはもちろん、タスクさえ生
成する必要がないことに注意してください。必要なのは rx.recv の呼び出
しを await することだけです。
std::mpsc::channel の同期版 Receiver::recv メソッドは、メッセージ
を受信するまでブロックします。trpl::Receiver::recv メソッドはasync
なのでそうではありません。ブロックする代わりに、メッセージを受信する
か、チャネルの送信側が閉じられるまで、制御をランタイムに返します。対
照的に、send の呼び出しはブロックしないので await しません。送信先
のチャネルは非有界なので、ブロックする必要がないのです。
注: このasyncコード全体は
trpl::block_on呼び出し内のasyncブロッ クで実行されるため、その内部のすべてはブロックを避けられます。しか し、その_外側_のコードは、block_on関数が戻るまでブロックされま す。これこそがtrpl::block_on関数の要点です。つまり、ある一連の asyncコードのどこでブロックするか、ひいては同期コードとasyncコード のどこで切り替えるかを_選べる_ようにしてくれるのです。
この例について2つ注目してください。第一に、メッセージはすぐに到着しま す。第二に、ここではフューチャーを使っているにもかかわらず、まだ並行 性はありません。リスト内のすべては、フューチャーがまったく関係ない場 合と同じように、順番に起こります。
このうち最初の点に対処するため、リスト17-10に示すように、一連のメッ セージを送信し、その合間にスリープを入れてみましょう。
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
});
}
メッセージを送るだけでなく、受け取る必要もあります。この場合は、何件
のメッセージが来るか分かっているので、rx.recv().await を4回呼び出し
て手作業で受信することもできます。しかし現実には、通常は_未知の_数の
メッセージを待つことになるため、もうメッセージがないと判断できるまで
待ち続ける必要があります。
リスト16-10では、同期チャネルから受信したすべての要素を処理するため
に for ループを使いました。しかし、Rustにはまだ、_非同期に生成され
る_一連の要素に対して for ループを使う方法がありません。そこで、ま
だ見ていないループ、while let 条件付きループを使う必要があります。
これは、6章の[「if let と let...else を使った簡潔な制御フロー」]
if-let節で見た if let 構文のループ版です。このループ
は、指定したパターンが値にマッチし続ける限り実行を続けます。
rx.recv の呼び出しはフューチャーを生成し、それを await します。ラン
タイムは、それが準備できるまでそのフューチャーを一時停止します。メッ
セージが到着すると、そのたびにフューチャーは Some(message) に解決さ
れます。チャネルが閉じられると、_一度でも_メッセージが到着したかどう
かにかかわらず、そのフューチャーは代わりに None に解決されます。こ
れは、もう値がなく、したがってポーリング、つまり await をやめるべきで
あることを示します。
この while let ループが、これらすべてをまとめます。rx.recv().await
を呼び出した結果が Some(message) であれば、メッセージにアクセスで
き、if let と同じようにループ本体でそれを使えます。結果が None な
ら、ループは終了します。ループが1回完了するたびに、再び await ポイン
トに到達するので、別のメッセージが来るまでランタイムは再びそれを一時
停止します。
これでコードは、すべてのメッセージを正常に送受信できるようになりまし た。残念ながら、まだいくつか問題があります。ひとつには、メッセージは 0.5秒間隔では到着しません。プログラム開始から2秒(2,000ミリ秒)後に、 まとめて一度に到着します。もうひとつには、このプログラムはいつまでも 終了しません! その代わり、新しいメッセージを永久に待ち続けます。 ctrl-C を使って停止する必要があります。
1つのasyncブロック内のコードは直列に実行される
まず、各メッセージの間に遅延を挟んで届くのではなく、なぜ完全な遅延の
後にまとめて届くのかを見ていきましょう。あるasyncブロックの中では、
コード内で await キーワードが現れる順序が、そのままプログラム実行時
にそれらが実行される順序でもあります。
リスト17-10には async ブロックが1つしかないので、その中のすべては順番に実行されます。まだ並行性はありません。すべての tx.send 呼び出しが、すべての trpl::sleep 呼び出しとそれに対応する await ポイントに挟まれながら実行されます。その後になって初めて、while let ループが recv 呼び出し上の await ポイントを通過できるようになります。
私たちが欲しい振る舞い、つまり各メッセージの間に sleep の遅延が発生するようにするには、リスト17-11に示すように、tx と rx の操作をそれぞれ独自の async ブロックに入れる必要があります。そうすれば、リスト17-8と同様に、ランタイムは trpl::join を使ってそれぞれを別個に実行できます。ここでも、個々の Future ではなく、trpl::join の呼び出し結果を await します。個々の Future を順番に await してしまうと、単に逐次的なフローに戻ってしまいます。これはまさに、私たちが したくない ことです。
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx_fut = async {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
trpl::join(tx_fut, rx_fut).await;
});
}
リスト17-11の更新後のコードでは、メッセージは2秒後にまとめて一気に表示されるのではなく、500ミリ秒間隔で表示されます。
所有権を async ブロックに移動する
ただし、このプログラムは依然として終了しません。これは、while let ループと trpl::join の相互作用によるものです。
trpl::joinから返される Future は、それに渡された 両方の Future が完了したときにのみ完了します。tx_futFuture は、vals内の最後のメッセージを送信した後の sleep を終えると完了します。rx_futFuture は、while letループが終了するまで完了しません。while letループは、rx.recvをawaitした結果がNoneになるまで終了しません。rx.recvをawaitすると、チャネルの反対側が閉じられたときにのみNoneが返ります。- チャネルが閉じるのは、
rx.closeを呼び出すか、送信側であるtxがドロップされたときだけです。 rx.closeはどこでも呼び出しておらず、txはtrpl::block_onに渡した最も外側のasyncブロックが終わるまでドロップされません。- そのブロックは
trpl::joinの完了待ちでブロックされているため終われず、その結果、このリストの先頭に戻ることになります。
現時点では、メッセージを送信する async ブロックは tx を 借用 しているだけです。というのも、メッセージの送信に所有権は必要ないからです。しかし、もし tx をその async ブロックへ move できれば、そのブロックが終了した時点で tx はドロップされます。第13章の 「参照をキャプチャするか、所有権を移動する」
節では、クロージャで move キーワードを使う方法を学びました。また、第16章の 「スレッドで move クロージャを使う」 節で説明したように、スレッドを扱うときには、データをクロージャへ移動する必要がしばしばあります。同じ基本的な力学が async ブロックにも当てはまるため、move キーワードはクロージャと同じように async ブロックでも機能します。
リスト17-12では、メッセージ送信に使っているブロックを async から async move に変更します。
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx_fut = async move {
// --snip--
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
trpl::join(tx_fut, rx_fut).await;
});
}
この バージョンのコードを実行すると、最後のメッセージが送信され受信された後に正常に終了します。次に、複数の Future からデータを送信するには何を変更する必要があるかを見てみましょう。
join! マクロで複数の Future を結合する
この非同期チャネルはマルチプロデューサチャネルでもあるので、リスト17-13に示すように、複数の Future からメッセージを送信したい場合は tx に対して clone を呼び出せます。
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(1500)).await;
}
};
trpl::join!(tx1_fut, tx_fut, rx_fut);
});
}
まず、tx をクローンして、最初の async ブロックの外側で tx1 を作成します。そして、先ほど tx で行ったのと同じように、tx1 をそのブロックに move します。その後、元の tx を 新しい async ブロックに move し、そこで少し長めの遅延でさらにメッセージを送信します。この新しい async ブロックはたまたまメッセージ受信用の async ブロックの後に置いていますが、同じように前に置くこともできます。重要なのは、Future が作成される順序ではなく、それらが await される順序です。
メッセージ送信用の2つの async ブロックは、どちらも async move ブロックである必要があります。そうすることで、それらのブロックが終了したときに tx と tx1 の両方がドロップされます。そうしないと、結局また最初と同じ無限ループに戻ってしまいます。
最後に、追加の Future を扱うために trpl::join から trpl::join! に切り替えます。join! マクロは、コンパイル時に Future の数が分かっている場合に、任意個の Future を await できます。数が不明な Future のコレクションを await する方法については、この章の後半で説明します。
これで、2つの送信 Future からのすべてのメッセージが見えるようになります。また、送信 Future は送信後に少しずつ異なる遅延を使っているため、メッセージもその異なる間隔で受信されます。
received 'hi'
received 'more'
received 'from'
received 'the'
received 'messages'
received 'future'
received 'for'
received 'you'
Future 間でデータを送るためにメッセージパッシングを使う方法、async ブロック内のコードがどのように逐次実行されるか、所有権を async ブロックへ移動する方法、そして複数の Future を結合する方法を見てきました。次は、ランタイムに別のタスクへ切り替えてよいことをどのように、そしてなぜ伝えるのかを説明しましょう。
任意の数のFutureを扱う
ランタイムに制御を譲る
「最初の async プログラム」 節で見たように、各 await ポイントで、await されている future の準備ができて いない場合、Rust はランタイムにそのタスクを一時停止して別のタスクへ切り替える 機会を与えます。逆もまた真です。Rust が async ブロックを一時停止し、 ランタイムに制御を返すのは await ポイントでのみ です。await ポイントの 間にあるものはすべて同期的です。
つまり、await ポイントなしに async ブロックの中で大量の作業をすると、 その future はほかの future が進行するのを妨げます。これを、ある future が ほかの future を 飢餓状態にする と表現するのを耳にすることもあるでしょう。 場合によっては、それは大した問題ではないかもしれません。しかし、何らかの コストの高い初期化や長時間にわたる作業をしている場合、あるいは特定のタスクを 際限なく実行し続ける future がある場合には、いつどこでランタイムに 制御を返すかを考える必要があります。
この飢餓の問題を示すために、長時間実行される操作をシミュレートしてから、
それをどう解決するかを見ていきましょう。リスト 17-14 では slow 関数を
導入します。
extern crate trpl; // required for mdbook test
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
// We will call `slow` here later
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
このコードでは trpl::sleep ではなく std::thread::sleep を使っているので、
slow を呼び出すと、現在のスレッドが一定のミリ秒数だけブロックされます。
slow は、長時間実行され、しかもブロッキングでもある現実世界の操作の
代わりとして使えます。
リスト 17-15 では、2 つの future でこの種の CPU バウンドな作業を行う様子を
slow を使って再現します。
extern crate trpl; // required for mdbook test
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
let a = async {
println!("'a' started.");
slow("a", 30);
slow("a", 10);
slow("a", 20);
trpl::sleep(Duration::from_millis(50)).await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
slow("b", 10);
slow("b", 15);
slow("b", 350);
trpl::sleep(Duration::from_millis(50)).await;
println!("'b' finished.");
};
trpl::select(a, b).await;
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
各 future は、時間のかかる操作をいくつも実行した 後で初めて ランタイムに制御を返します。このコードを実行すると、次のような出力になります。
'a' started.
'a' ran for 30ms
'a' ran for 10ms
'a' ran for 20ms
'b' started.
'b' ran for 75ms
'b' ran for 10ms
'b' ran for 15ms
'b' ran for 350ms
'a' finished.
2 つの URL を取得する future を競わせるために trpl::select を使った
リスト 17-5 と同様に、select は a が終わるとすぐに完了します。ただし、
2 つの future での slow の呼び出しはインターリーブされません。a future は
trpl::sleep の呼び出しが await されるまでの作業をすべて行い、その後で b
future が自身の trpl::sleep の呼び出しが await されるまでの作業をすべて行い、
最後に a future が完了します。両方の future が時間のかかるタスクの合間にも
進行できるようにするには、ランタイムに制御を返せる await ポイントが必要です。
つまり、await できる何かが必要なのです!
リスト 17-15 でも、この種の制御の受け渡しが起きていることはすでにわかります。
a future の末尾にある trpl::sleep を取り除くと、b future が まったく
実行されないまま完了してしまうからです。操作どうしが進行を交代できるように
するための出発点として、リスト 17-16 に示すように trpl::sleep
関数を使ってみましょう。
extern crate trpl; // required for mdbook test
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
let one_ms = Duration::from_millis(1);
let a = async {
println!("'a' started.");
slow("a", 30);
trpl::sleep(one_ms).await;
slow("a", 10);
trpl::sleep(one_ms).await;
slow("a", 20);
trpl::sleep(one_ms).await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
trpl::sleep(one_ms).await;
slow("b", 10);
trpl::sleep(one_ms).await;
slow("b", 15);
trpl::sleep(one_ms).await;
slow("b", 350);
trpl::sleep(one_ms).await;
println!("'b' finished.");
};
trpl::select(a, b).await;
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
slow の各呼び出しの間に、await ポイントを伴う trpl::sleep の呼び出しを
追加しました。これで 2 つの future の作業はインターリーブされます。
'a' started.
'a' ran for 30ms
'b' started.
'b' ran for 75ms
'a' ran for 10ms
'b' ran for 10ms
'a' ran for 20ms
'b' ran for 15ms
'a' finished.
a future は trpl::sleep を呼び出す前に slow を呼び出しているため、
制御を b に渡す前にまだ少しの間は動き続けます。しかしその後は、どちらかが
await ポイントに達するたびに、future は前後に切り替わります。この場合は、
slow の呼び出しごとにそうしていますが、作業は自分たちにとって最も理にかなう
やり方で分割できます。
ただし、ここで本当にしたいのは スリープ することではありません。できるだけ
速く処理を進めたいのです。必要なのは、ランタイムに制御を返すことだけです。
それは trpl::yield_now 関数を使って直接行えます。リスト 17-17 では、
それらの trpl::sleep 呼び出しをすべて trpl::yield_now に置き換えます。
extern crate trpl; // required for mdbook test
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
let a = async {
println!("'a' started.");
slow("a", 30);
trpl::yield_now().await;
slow("a", 10);
trpl::yield_now().await;
slow("a", 20);
trpl::yield_now().await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
trpl::yield_now().await;
slow("b", 10);
trpl::yield_now().await;
slow("b", 15);
trpl::yield_now().await;
slow("b", 350);
trpl::yield_now().await;
println!("'b' finished.");
};
trpl::select(a, b).await;
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
このコードは、実際の意図をより明確に表しているうえ、sleep を使うよりも
かなり速くなる可能性があります。というのも、sleep が使うようなタイマーには
どこまで細かい粒度で動けるかの制限がしばしばあるからです。たとえば、ここで
使っている sleep は、1 ナノ秒の Duration を渡したとしても、常に少なくとも
1 ミリ秒はスリープします。繰り返しますが、現代のコンピューターは 高速 です。
1 ミリ秒の間にもたくさんのことができます!
つまり、プログラムがほかに何をしているかによっては、計算集約的なタスクに 対しても async は有用になりえます。なぜなら、それはプログラムの異なる部分 どうしの関係を構造化するための便利な道具を提供するからです(ただし、 async 状態機械のオーバーヘッドというコストはあります)。これは 協調的マルチタスク の一種で、各 future は、await ポイントを通じていつ 制御を渡すかを自分で決める力を持っています。したがって各 future には、 長くブロックしすぎない責任もあります。Rust ベースの組み込みオペレーティング システムの中には、これが 唯一の マルチタスク方式であるものもあります!
もちろん、実際のコードでは、1 行ごとに関数呼び出しと await ポイントを 交互に並べるようなことは普通しません。このように制御を譲ることは比較的 低コストですが、ただではありません。多くの場合、計算集約的なタスクを分割しようと すると、かえってかなり遅くなることがあります。そのため、全体的な パフォーマンスのためには、処理を短時間ブロックさせたほうがよいこともあります。 コードの実際の性能ボトルネックが何なのかは、必ず測定して確認してください。 ただし、同時に起こるはずだと思っていた大量の処理が実際には直列に進んでいるのを 実際に 目にしているのであれば、この背後にある力学を念頭に置いておくことは 重要です!
独自の async 抽象化を構築する
future を組み合わせて、新しいパターンを作ることもできます。たとえば、
すでに持っている async の構成要素を使って timeout 関数を構築できます。
そうしてできあがるものは、さらに別の async 抽象化を作るために使える、
また別の構成要素になります。
リスト 17-18 は、この timeout が時間のかかる future に対してどのように
動作することを期待するかを示しています。
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"Finally finished"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
これを実装してみましょう! まずは、timeout の API について考えてみましょう。
- これを
awaitできるようにするため、timeout自体も async 関数である必要があります。 - 1 つ目のパラメータは実行する future であるべきです。任意の future で動作できるように、これをジェネリックにできます。
- 2 つ目のパラメータは待機する最大時間です。
Durationを使えば、それをtrpl::sleepに簡単に渡せます。 - 戻り値は
Resultであるべきです。future が正常に完了した場合、Resultは future が生成した値を持つOkになります。先にタイムアウトが経過した場合、Resultはタイムアウトが待機した duration を持つErrになります。
リスト 17-19 にこの宣言を示します。
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"Finally finished"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
async fn timeout<F: Future>(
future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
// Here is where our implementation will go!
}
これで型に関する目標は満たせました。では次に、必要な 振る舞い について考えてみましょう。渡された future と duration を競争させたいのです。trpl::sleep を使って duration からタイマー future を作り、trpl::select を使ってそのタイマーと、呼び出し元が渡した future を一緒に実行できます。
リスト 17-20 では、trpl::select を await した結果に対してマッチすることで timeout を実装します。
extern crate trpl; // required for mdbook test
use std::time::Duration;
use trpl::Either;
// --snip--
fn main() {
trpl::block_on(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"Finally finished"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
async fn timeout<F: Future>(
future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
match trpl::select(future_to_try, trpl::sleep(max_time)).await {
Either::Left(output) => Ok(output),
Either::Right(_) => Err(max_time),
}
}
trpl::select の実装は公平ではありません。常に、渡された順序で引数を poll します(他の select 実装では、どの引数を最初に poll するかをランダムに選ぶものもあります)。そのため、max_time が非常に短い duration であっても future_to_try に完了の機会を与えられるように、future_to_try を先に select に渡します。future_to_try が先に完了した場合、select は future_to_try からの出力を持つ Left を返します。timer が先に完了した場合、select はタイマーの出力である () を持つ Right を返します。
future_to_try が成功して Left(output) を受け取った場合は、Ok(output) を返します。代わりに sleep タイマーが経過して Right(()) を受け取った場合は、_ で () を無視し、代わりに Err(max_time) を返します。
これで、他の 2 つの async ヘルパーを組み合わせて動作する timeout ができました。コードを実行すると、タイムアウト後の失敗モードが出力されます。
2 秒後に失敗しました
future は他の future と合成できるため、より小さな async の構成要素を使って非常に強力なツールを構築できます。たとえば、同じアプローチを使ってタイムアウトとリトライを組み合わせ、さらにそれらをネットワーク呼び出しのような操作(リスト 17-5 にあるものなど)で使うことができます。
実際には、通常は async と await を直接使い、次に select のような関数や join! マクロのようなマクロを使って、最も外側の future がどのように実行されるかを制御します。
ここまでで、複数の future を同時に扱うさまざまな方法を見てきました。次は、stream を使って、時間の経過に沿った順序の中で複数の future を扱う方法を見ていきます。
Stream: 順に並んだFuture
ストリーム:順に現れる Future
この章の前の方にある
「メッセージ受け渡し」 節で、async
チャネルのレシーバーをどのように使ったかを思い出してください。非同期の
recv メソッドは、時間の経過とともに一連のアイテムを生成します。これは、
ストリーム として知られる、はるかに一般的なパターンの一例です。多くの概念は自然に
ストリームとして表現できます。たとえば、キュー内で利用可能になるアイテム、
完全なデータセットがコンピューターのメモリに収まりきらないときに
ファイルシステムから段階的に取り出されるデータのチャンク、あるいは
時間の経過とともにネットワーク越しに到着するデータです。
ストリームは Future なので、ほかの種類の Future と一緒に使い、
興味深い方法で組み合わせることができます。たとえば、イベントをまとめて
ネットワーク呼び出しが多くなりすぎるのを避けたり、長時間実行される一連の
操作にタイムアウトを設定したり、不要な処理を避けるために
ユーザーインターフェイスのイベントをスロットリングしたりできます。
第 13 章で、「Iterator トレイトと next メソッド」 節で Iterator
トレイトを見たときにも、一連のアイテムを扱いました。ただし、イテレータと
async チャネルのレシーバーの間には 2 つの違いがあります。1 つ目の違いは時間です。
イテレータは同期的ですが、チャネルのレシーバーは非同期です。2 つ目の違い
は API です。Iterator を直接扱うときは、その同期的な
next メソッドを呼び出します。特に trpl::Receiver ストリームでは、代わりに
非同期の recv メソッドを呼び出しました。そのほかの点では、これらの API は非常によく似ており、
その類似性は偶然ではありません。ストリームは、反復処理の非同期版の
ようなものです。ただし、trpl::Receiver は具体的にはメッセージを受信するのを
待ちますが、汎用のストリーム API はそれよりずっと広範です。
Iterator と同じように次のアイテムを提供しますが、それを非同期に行います。
Rust におけるイテレータとストリームの類似性は、実際には
任意のイテレータからストリームを作成できることを意味します。イテレータと同様に、
ストリームでもその next メソッドを呼び出し、その出力を await して扱えます。これはリスト
17-21 に示すとおりですが、このコードはまだコンパイルできません。
extern crate trpl; // required for mdbook test
fn main() {
trpl::block_on(async {
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);
while let Some(value) = stream.next().await {
println!("The value was: {value}");
}
});
}
まず数値の配列から始め、それをイテレータに変換してから
map を呼び出し、すべての値を 2 倍にします。次に、
trpl::stream_from_iter 関数を使って、そのイテレータを
ストリームに変換します。その後、while let ループを使って、
到着した順にストリーム内のアイテムを処理します。
残念ながら、このコードを実行しようとするとコンパイルできず、
代わりに利用可能な next メソッドがないと報告されます。
error[E0599]: no method named `next` found for struct `tokio_stream::iter::Iter` in the current scope
--> src/main.rs:10:40
|
10 | while let Some(value) = stream.next().await {
| ^^^^
|
= help: items from traits can only be used if the trait is in scope
help: the following traits which provide `next` are implemented but not in scope; perhaps you want to import one of them
|
1 + use crate::trpl::StreamExt;
|
1 + use futures_util::stream::stream::StreamExt;
|
1 + use std::iter::Iterator;
|
1 + use std::str::pattern::Searcher;
|
help: there is a method `try_next` with a similar name
|
10 | while let Some(value) = stream.try_next().await {
| ~~~~~~~~
この出力が説明しているように、このコンパイラエラーの原因は、next
メソッドを使えるようにするために適切なトレイトをスコープに入れる必要があることです。
ここまでの説明から、そのトレイトは Stream だと考えるのがもっともですが、
実際には StreamExt です。extension の略である Ext は、
あるトレイトを別のトレイトで拡張することを表す、
Rust コミュニティで一般的なパターンです。
Stream トレイトは、実質的に Iterator トレイトと Future
トレイトを組み合わせた低レベルのインターフェイスを定義します。
StreamExt は Stream の上に、より高レベルな API 群を提供します。
これには next メソッドに加えて、Iterator
トレイトが提供するものに似たほかのユーティリティメソッドも含まれます。Stream と
StreamExt はまだ Rust の標準ライブラリの一部ではありませんが、
エコシステム内のほとんどのクレートでは、これと似た定義を使っています。
このコンパイラエラーを修正するには、リスト 17-22 のように
trpl::StreamExt に対する use 文を追加します。
extern crate trpl; // required for mdbook test
use trpl::StreamExt;
fn main() {
trpl::block_on(async {
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// --snip--
let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);
while let Some(value) = stream.next().await {
println!("The value was: {value}");
}
});
}
これらの要素をすべて組み合わせると、このコードは期待どおりに動作します。さらに、
StreamExt がスコープに入ったので、イテレータのときと同じように、
そのすべてのユーティリティメソッドを使えます。
Asyncのためのトレイトを詳しく見る
Asyncのトレイトを詳しく見る
この章を通して、Future、Stream、StreamExt
トレイトをさまざまな形で使ってきました。ただしこれまでは、それらがどのように動作するのか、あるいはどのように組み合わさるのかという詳細には、あまり深く立ち入りませんでした。日々の Rust の作業では、たいていそれで十分です。しかし場合によっては、これらのトレイトについてもう少し詳しく理解する必要がある場面に出会います。その際には、Pin 型や Unpin トレイトもあわせて理解する必要があります。この節では、そのような場面で役立つだけの内容を掘り下げます。ただし、本当に深い説明はほかのドキュメントに譲ることにします。
Future トレイト
まずは、Future トレイトがどのように動作するのかを、もう少し詳しく見ていきましょう。Rust では次のように定義されています。
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}
このトレイト定義には、新しい型がいくつも登場し、これまで見たことのない構文も含まれています。そこで、この定義をひとつずつ見ていきましょう。
まず、Future の関連型 Output は、その future が最終的に何に解決されるかを示します。これは、Iterator トレイトにおける関連型 Item に相当します。
次に、Future には poll メソッドがあります。このメソッドは、self パラメータとして特別な Pin 参照と、Context 型への可変参照を受け取り、Poll<Self::Output> を返します。Pin と Context については、すぐ後で詳しく説明します。ひとまず今は、このメソッドが返す Poll 型に注目しましょう。
#![allow(unused)]
fn main() {
pub enum Poll<T> {
Ready(T),
Pending,
}
}
この Poll 型は Option に似ています。値を持つバリアント Ready(T) と、値を持たない Pending があります。ただし、Poll の意味は Option とはかなり異なります。Pending バリアントは、その future にまだやるべき仕事が残っていることを示しており、そのため呼び出し側はあとで再度確認する必要があります。一方 Ready バリアントは、その Future が作業を完了し、T の値が利用可能になったことを示します。
注:
pollを直接呼び出す必要があることはめったにありませんが、もし必要になった場合は、ほとんどの future ではReadyを返したあとに呼び出し側が再度pollを呼んではならない、という点に注意してください。多くの future は、準備完了後に再度 poll されると panic します。再度 poll しても安全な futures については、そのことがドキュメントに明示されています。これはIterator::nextの振る舞いに似ています。
await を使うコードを見るとき、Rust はその裏側で、それを poll を呼び出すコードにコンパイルしています。1 つの URL に対して解決後にページタイトルを出力したリスト 17-4 を振り返ると、Rust はそれをだいたい次のようなコードにコンパイルします(ただし正確に同じではありません)。
match page_title(url).poll() {
Ready(page_title) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// しかし、ここには何が入るのでしょうか?
}
}
future がまだ Pending のとき、私たちは何をすべきでしょうか。future が最終的に ready になるまで、何度も何度も何度も試せる何らかの方法が必要です。言い換えれば、ループが必要です。
let mut page_title_fut = page_title(url);
loop {
match page_title_fut.poll() {
Ready(value) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// 続ける
}
}
}
しかし、もし Rust が本当にそのコードそのままにコンパイルしていたなら、すべての await はブロッキングになってしまいます。これは、私たちが目指していたものとまさに逆です。そこで Rust は、このループが制御を何か別のものに渡せるようにします。その何かは、この future の作業を一時停止して別の future に取り組み、その後でまたこの future を確認できます。これまで見てきたように、その「何か」とは async ランタイムであり、このスケジューリングと調停の処理はその主要な仕事の 1 つです。
「メッセージパッシングを使って2つのタスク間でデータを送信する」節では、
rx.recv を待機することについて説明しました。recv 呼び出しは future を返し、その future を await すると poll が行われます。チャネルが閉じたとき、Some(message) または None のどちらかで準備が整うまで、ランタイムがその future を一時停止することを確認しました。Future トレイト、特に Future::poll についてより深く理解すると、それがどのように動くのかが分かります。その future が Poll::Pending を返したとき、ランタイムはまだ ready ではないことを認識します。逆に、poll が Poll::Ready(Some(message)) または
Poll::Ready(None) を返したとき、ランタイムはその future が ready になったことを認識し、処理を先へ進めます。
ランタイムがそれをどのように実現しているかという正確な詳細は、この本の範囲を超えます。しかし重要なのは、future の基本的な仕組みを理解することです。つまり、ランタイムは自分が担当する各 future を poll し、まだ ready でなければその future を再び休止状態に戻します。
Pin 型と Unpin トレイト
リスト 17-13 では、trpl::join! マクロを使って 3 つの
future を await しました。しかし、実行時まで数が分からない複数の futures を含む、ベクタのようなコレクションを持つことはよくあります。そこで、リスト
17-13 を、3 つの futures をベクタに入れ、代わりに trpl::join_all 関数を呼び出すリスト 17-23 のコードに変更してみましょう。ただし、これはまだコンパイルできません。
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
// --snip--
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let futures: Vec<Box<dyn Future<Output = ()>>> =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;
});
}
各 future を Box に入れて トレイトオブジェクト にしています。これは第 12 章の「run からエラーを返す」節で行ったのと同じです。(トレイトオブジェクトについては第 18 章で詳しく扱います。)トレイトオブジェクトを使うことで、これらの型が生成するそれぞれの無名の future を同じ型として扱えます。なぜなら、それらはすべて Future トレイトを実装しているからです。
これは意外に思えるかもしれません。というのも、どの async ブロックも何も返さないので、それぞれは Future<Output = ()> を生成するからです。ただし、Future はトレイトであること、そしてコンパイラは、出力型が同じであっても async
ブロックごとに固有の enum を生成することを思い出してください。手で書いた 2 つの異なる構造体を 1 つの Vec に入れられないのと同じように、コンパイラ生成の enum どうしを混在させることもできません。
その後、future のコレクションを trpl::join_all 関数に渡して、その結果を await します。しかし、これはコンパイルできません。以下がエラーメッセージの関連部分です。
error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
--> src/main.rs:48:33
|
48 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`
このエラーメッセージの注記は、値を ピン留め するために pin! マクロを使うべきだと伝えています。これは、値をメモリ内で移動しないことを保証する Pin 型の中に入れる、という意味です。エラーメッセージがピン留めが必要だと言っているのは、dyn Future<Output = ()> が Unpin トレイトを実装する必要があるのに、現時点では実装していないためです。
trpl::join_all 関数は JoinAll という構造体を返します。この構造体は型 F に対してジェネリックであり、その F には Future トレイトを実装するという制約があります。await を使って future を直接 await すると、その future は暗黙的にピン留めされます。これが、future を await したいあらゆる場面で pin! を使う必要がない理由です。
しかし、ここでは future を直接 await しているわけではありません。代わりに、future のコレクションを join_all 関数に渡して、新しい future である JoinAll を構築しています。join_all のシグネチャでは、コレクション内の各要素の型がすべて Future トレイトを実装していることが必要であり、Box<T> が Future を実装するのは、それが包む T が Unpin トレイトを実装する future である場合に限られます。
これは一度に理解するにはなかなか多いですね! 本当に理解するために、Future トレイトが実際にどのように動作しているのか、特にピン留めまわりについて、もう少し踏み込んで見ていきましょう。もう一度 Future トレイトの定義を見てください。
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
// 必須メソッド
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}
cx パラメータとその Context 型は、ランタイムが遅延性を保ちながら、いつ任意の future を確認すべきかを実際に知る仕組みの鍵です。繰り返しますが、その仕組みの詳細はこの章の範囲を超えており、通常は独自の Future 実装を書くときにだけ考えれば十分です。ここでは代わりに self の型に注目します。self に型注釈が付いたメソッドを見るのはこれが初めてです。self に対する型注釈は、ほかの関数パラメータに対する型注釈と似たように機能しますが、2つの重要な違いがあります。
- そのメソッドを呼び出すために
selfがどの型でなければならないかを Rust に伝えます。 - ただし、任意の型にできるわけではありません。そのメソッドが実装されている型、その型への参照またはスマートポインタ、あるいはその型への参照を包んだ
Pinに制限されます。
この構文については 第18章 でさらに見ていきます。今のところは、future を poll してそれが Pending か Ready(Output) かを確認したいなら、その型への Pin で包まれた可変参照が必要だと知っておけば十分です。
Pin は、&、&mut、Box、Rc のようなポインタ風の型を包むラッパーです。(厳密には、Pin は Deref または DerefMut トレイトを実装する型に対して機能しますが、これは実質的には参照とスマートポインタだけを扱うのと同じです。)Pin 自体はポインタではなく、Rc や Arc が参照カウントに関して持つような独自の振る舞いもありません。これは純粋に、コンパイラがポインタの使い方に関する制約を強制するために使える道具です。
await が poll の呼び出しによって実装されていることを思い出すと、先ほど見たエラーメッセージの説明が少し見えてきますが、あのメッセージで言及されていたのは Pin ではなく Unpin でした。では、Pin は Unpin と正確にはどう関係していて、なぜ Future は poll を呼び出すために self が Pin 型に入っている必要があるのでしょうか。
この章の前の方で見たように、future の中にある一連の await ポイントは状態機械にコンパイルされ、コンパイラはその状態機械が借用や所有権を含む Rust の通常の安全性ルールすべてに従うようにします。それを実現するために、Rust はある await ポイントと次の await ポイント、あるいは async ブロックの終わりとの間でどのデータが必要になるかを調べます。そして、コンパイル後の状態機械に対応するバリアントを作成します。各バリアントは、ソースコードのその区間で使われるデータに必要なアクセス権を受け取ります。それは、そのデータの所有権を取得する場合もあれば、そのデータへの可変参照または不変参照を取得する場合もあります。
ここまでは順調です。ある async ブロックにおける所有権や参照について何か間違えれば、借用チェッカーが教えてくれます。そのブロックに対応する future を移動したくなると――たとえば、それを Vec に移動して join_all に渡すような場合には――話はもっと厄介になります。
future を移動するということは――join_all でイテレータとして使うためにデータ構造に push する場合でも、関数からそれを返す場合でも――実際には Rust が私たちのために作った状態機械を移動するということです。そして、Rust におけるほとんどのほかの型とは異なり、Rust が async ブロックのために作る future では、図17-4 の簡略図に示すように、各バリアントのフィールドの中に自分自身への参照が含まれることがあります。
ただし、デフォルトでは、自分自身への参照を持つオブジェクトはどれも移動すると安全ではありません。参照は常に、それが指す対象の実際のメモリアドレスを指すからです(図17-5 を参照)。データ構造そのものを移動すると、それらの内部参照は古い場所を指したままになります。しかし、そのメモリ位置はもはや無効です。まず第一に、データ構造に変更を加えても、その値は更新されません。さらに、もっと重要なこととして、コンピュータはそのメモリをほかの目的のために自由に再利用できるようになります! その結果、あとでまったく無関係なデータを読んでしまうおそれがあります。
Pin はこれを土台にして、私たちが必要とするまさにその保証を与えて
くれます。値へのポインタを Pin でラップしてその値を ピン留め すると、
その値はもはや移動できません。したがって、Pin<Box<SomeType>> がある
場合、実際にピン留めされるのは SomeType の値であり、Box ポインタ
ではありません。図 17-6 はこの過程を示しています。
実際には、Box ポインタ自体はなお自由に動かせます。思い出して
ください。重要なのは、最終的に参照されているデータがその場にとどまる
ようにすることです。図 17-7 のように、ポインタが動いても、
それが指しているデータ が同じ場所にあるなら、問題が起こる可能性は
ありません。(練習として、これらの型のドキュメントと std::pin
モジュールのドキュメントを読み、Box をラップする Pin でこれを
どう実現するか考えてみてください。)重要なのは、自己参照型そのものは
依然としてピン留めされているため移動できない、という点です。
しかし、たとえ Pin ポインタの背後にあったとしても、ほとんどの型は
移動してまったく安全です。ピン留めについて考える必要があるのは、値が
内部参照を持つ場合だけです。数値や真偽値のようなプリミティブ値は安全
です。明らかに内部参照を持たないからです。
Rust で通常扱うほとんどの型も同様です。たとえば Vec は、心配せずに
移動できます。ここまで見てきたことを踏まえると、Pin<Vec<String>> が
ある場合、Vec<String> にはほかの参照がなければ常に安全に移動できる
にもかかわらず、Pin が提供する安全ではあるものの制約の強い API を
通してすべてを行わなければなりません。このような場合には値を移動しても
よいとコンパイラに伝える方法が必要であり、そこで Unpin の出番です。
Unpin は、16 章で見た Send トレイトや Sync トレイトと同様の
マーカートレイトであり、それ自体には何の機能もありません。マーカー
トレイトは、あるトレイトを実装した型を特定の文脈で安全に使えることを
コンパイラに伝えるためだけに存在します。Unpin は、ある型について、
その値を安全に移動できるかどうかに関して特別な保証を維持する必要が
ない ことをコンパイラに知らせます。
Send や Sync の場合と同様に、コンパイラは、安全だと証明できる
すべての型に対して自動的に Unpin を実装します。ここでも Send や
Sync と同じく、型に Unpin が 実装されない 場合が特別なケース
です。その表記は impl !Unpin for SomeType であり、
ここで SomeType は、その型へのポインタが Pin で
使われるときに安全であるために、それらの保証を 守る必要がある 型の
名前です。
言い換えると、Pin と Unpin の関係について覚えておくべきことは
2 つあります。1 つ目は、Unpin が「通常」のケースであり、!Unpin が
特別なケースだということです。2 つ目は、型が Unpin と !Unpin の
どちらを実装しているかが意味を持つのは、Pin<&mut
SomeType> のような、その型へのピン留めされたポインタを
使うとき だけ だということです。
これを具体的にするために、String を考えてみましょう。String には
長さと、それを構成する Unicode 文字があります。図 17-8 にあるように、
String を Pin でラップできます。しかし、String は Rust の
ほとんどのほかの型と同様に、自動的に Unpin を実装します。
その結果、String が代わりに !Unpin を実装していたなら不正になる
ようなこと、たとえば図 17-9 のようにメモリ内のまったく同じ場所で
1 つの文字列を別の文字列に置き換えることができます。これは Pin の
契約に違反しません。String には、移動することを安全でなくする内部
参照がないからです。まさにそれが、!Unpin ではなく Unpin を実装
している理由です。
これで、先ほどリスト17-23にあったその join_all 呼び出しで報告された
エラーを理解するのに十分な知識が身に付きました。もともと、async ブロックが生成した
future を Vec<Box<dyn Future<Output = ()>>> に移動しようとしましたが、これまでに見たように、
それらの future は内部参照を持つ可能性があるため、自動的には
Unpin を実装しません。いったんそれらを pin すれば、生成された Pin 型を
Vec に渡せます。そうすれば、future の基になるデータは 移動されない と
確信できます。リスト17-24は、3つの future のそれぞれを定義している箇所で
pin! マクロを呼び出し、トレイトオブジェクト型を調整することで、コードをどのように修正するかを示しています。
extern crate trpl; // required for mdbook test
use std::pin::{Pin, pin};
// --snip--
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = pin!(async move {
// --snip--
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
});
let rx_fut = pin!(async {
// --snip--
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
});
let tx_fut = pin!(async move {
// --snip--
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
});
let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
vec![tx1_fut, rx_fut, tx_fut];
trpl::join_all(futures).await;
});
}
この例はこれでコンパイルも実行もでき、実行時にベクタへ future を追加したり削除したりして、それらをすべて join できます。
Pin と Unpin が主に重要になるのは、日常的な Rust コードを書くときというより、
より低レベルなライブラリを構築するときや、ランタイムそのものを実装するときです。
とはいえ、エラーメッセージの中でこれらのトレイトを見かけたときには、これで
自分のコードをどう修正すればよいか、よりよく分かるようになったはずです。
注:
PinとUnpinのこの組み合わせにより、そうでなければ 自己参照であるために実装が難しい、複雑な型の一群を Rust で安全に 実装できるようになります。今日ではPinを必要とする型は async Rust で最もよく見られますが、ときどき他の文脈で目にすることもあります。
PinとUnpinがどのように動作するかの詳細や、それらが守ることを求められる 規則については、std::pinの API ドキュメントで広範に扱われているので、 さらに学びたいならそこから始めるのがよいでしょう。さらに詳しく内部でどのように動作しているのかを理解したい場合は、 Asynchronous Programming in Rust の第2章と 第4章 を参照してください。
Stream トレイト
これで Future、Pin、Unpin の各トレイトをより深く理解できたので、
次は Stream トレイトに目を向けましょう。この章の前の方で学んだように、
ストリームは非同期イテレータに似ています。ただし、Iterator と
Future と違って、本稿執筆時点では Stream は標準ライブラリには定義されていません。しかし、
エコシステム全体で使われている futures クレート由来の非常に一般的な定義が
存在します。
Stream トレイトがそれらをどのように組み合わせるのかを見る前に、
Iterator と Future の各トレイトの定義を確認しましょう。Iterator からは、
シーケンスという考え方を得ます。つまり、その next メソッドは
Option<Self::Item> を返します。Future からは、時間の経過に伴う準備完了という考え方を得ます。
つまり、その poll メソッドは Poll<Self::Output> を返します。時間の経過とともに
準備が整う要素のシーケンスを表現するために、これらの性質を組み合わせた
Stream トレイトを次のように定義します。
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
trait Stream {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>
) -> Poll<Option<Self::Item>>;
}
}
Stream トレイトは、ストリームが生成する要素の型として Item という
関連型を定義します。これは Iterator に似ており、要素は 0 個の場合もあれば多数ある場合もあります。これに対して Future では、たとえ単位型 () であっても、Output は常に 1 つだけです。
Stream は、それらの要素を取得するためのメソッドも定義します。これを poll_next と
呼ぶのは、Future::poll と同じように poll し、Iterator::next と同じように
要素のシーケンスを生成することを明確にするためです。その戻り値の型は
Poll と Option を組み合わせたものです。外側の型が Poll なのは、
future と同じく、準備ができているかを確認する必要があるからです。内側の型が Option
なのは、イテレータと同じく、まだ要素があるかどうかを示す必要が
あるからです。
この定義に非常に近いものが、いずれ Rust の 標準ライブラリの一部になる可能性が高いでしょう。それまでは、これはほとんどのランタイムの ツールキットの一部なので、これを前提にしてかまいませんし、これから説明する内容は一般に当てはまるはずです。
ただし、「ストリーム: 連なった Future」 節で見た例では、poll_next も Stream も使わず、
代わりに next と StreamExt を使いました。もちろん、
自分たちで Stream の状態機械を手書きすれば、poll_next API を
直接使って処理することもできます。ちょうど、future をその poll
メソッド経由で直接扱うこともできるのと同じです。しかし、
await を使う方がずっと扱いやすく、StreamExt トレイトはそのための next
メソッドを提供してくれます。
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
trait Stream {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>>;
}
trait StreamExt: Stream {
async fn next(&mut self) -> Option<Self::Item>
where
Self: Unpin;
// other methods...
}
}
注: この章の前の方で実際に使用した定義は、これとは少し 異なっています。というのも、トレイトで async 関数を使うことをまだ サポートしていなかった Rust のバージョンにも対応しているからです。その結果、 次のようになっています。
fn next(&mut self) -> Next<'_, Self> where Self: Unpin;この
Next型はFutureを実装するstructで、selfへの参照の ライフタイムをNext<'_, Self>で表せるようにするため、このメソッドでawaitが機能します。
StreamExt トレイトには、ストリームで使える興味深いメソッドがすべて
含まれています。Stream を実装するすべての型に対して StreamExt は
自動的に実装されますが、これらのトレイトが別々に定義されているのは、
基盤となるトレイトに影響を与えずにコミュニティが利便性 API を改善していけるようにするためです。
trpl クレートで使っている版の StreamExt では、このトレイトは next
メソッドを定義するだけでなく、Stream::poll_next の呼び出しに関わる詳細を
正しく処理する next のデフォルト実装も提供しています。これは、
自分でストリーミングデータ型を書く必要がある場合でも、実装しなければならないのは Stream だけでよく、
そのデータ型を使う人は誰でも自動的に StreamExt とそのメソッドを利用できる、
ということです。
これらのトレイトに関する低レベルな詳細については、ここまでにします。締めくくりとして、 future(ストリームを含む)、タスク、スレッドがそれぞれどのように 結び付くのかを考えてみましょう!
Future、タスク、スレッド
すべてをまとめる: Future、タスク、スレッド
第16章で見たように、スレッドは並行性への1つのアプローチを提供します。
この章では別のアプローチ、すなわち Future とストリームとともに async を
使う方法も見てきました。どちらの方法をいつ選ぶべきか気になるなら、その答えは
「場合による」です! そして多くの場合、選択肢はスレッド か async
ではなく、スレッド と async です。
多くのオペレーティングシステムは、すでに何十年にもわたってスレッドベースの 並行モデルを提供してきており、その結果として多くのプログラミング言語がそれを サポートしています。しかし、これらのモデルにもトレードオフがないわけでは ありません。多くのオペレーティングシステムでは、スレッドごとにかなりの量の メモリを使用します。また、スレッドが選択肢になるのは、オペレーティング システムとハードウェアがそれをサポートしている場合だけです。一般的な デスクトップやモバイルコンピューターとは異なり、一部の組み込みシステムには そもそも OS がないため、スレッドもありません。
async モデルは、異なる、そして最終的には相補的なトレードオフの組を
提供します。async モデルでは、並行な操作はそれぞれ専用のスレッドを必要と
しません。その代わり、ストリームの節で同期関数から処理を開始するために
trpl::spawn_task を使ったときのように、タスク上で実行できます。タスクは
スレッドに似ていますが、オペレーティングシステムに管理されるのではなく、
ライブラリレベルのコード、つまりランタイムに管理されます。
スレッドを生成する API とタスクを生成する API がこれほどよく似ているのには 理由があります。スレッドは同期操作の集合に対する境界として機能し、並行性は スレッド 間 で可能になります。タスクは 非同期 操作の集合に対する境界として 機能し、タスクは本体の中で Future を切り替えられるため、並行性はタスク 間 と タスク 内部 の両方で可能になります。最後に、Future は Rust における最も 粒度の細かい並行性の単位であり、それぞれの Future は別の Future の木を表す ことがあります。ランタイム、具体的にはそのエグゼキュータがタスクを管理し、 タスクが Future を管理します。その点で、タスクは軽量でランタイム管理の スレッドに似ていますが、オペレーティングシステムではなくランタイムによって 管理されることから生まれる追加の機能を備えています。
だからといって、async タスクが常にスレッドより優れている(あるいはその逆)
わけではありません。スレッドによる並行性は、ある意味では async による
並行性よりも単純なプログラミングモデルです。それは強みにも弱みにもなりえます。
スレッドはある程度「fire and forget」であり、Future に相当するネイティブな
仕組みを持たないため、オペレーティングシステム自身によって中断される場合を
除いて、単に完了まで実行されます。
そして実際のところ、スレッドとタスクはしばしば非常にうまく連携します。
なぜなら、タスクは(少なくとも一部のランタイムでは)スレッド間を移動できる
からです。実際、内部では、私たちが使ってきたランタイムは spawn_blocking と
spawn_task 関数を含め、デフォルトでマルチスレッドです! 多くのランタイムは、
システム全体のパフォーマンスを向上させるために、現在スレッドがどのように
利用されているかに基づいて、タスクをスレッド間で透過的に移動させる
ワークスティーリング と呼ばれるアプローチを使います。そのアプローチには
実際にはスレッド と タスク、したがって Future が必要です。
どの方法をいつ使うかを考えるときには、次の経験則を考慮してください。
- 作業が 非常に並列化しやすい(つまり、CPU バウンド)場合、たとえば それぞれの部分を別々に処理できる大量のデータを処理するようなケースでは、 スレッドのほうが適しています。
- 作業が 非常に並行的(つまり、I/O バウンド)である場合、たとえば異なる
間隔や異なる速度で届く可能性がある多数の異なるソースからのメッセージを
処理するようなケースでは、
asyncのほうが適しています。
また、並列性と並行性の両方が必要な場合でも、スレッドと async のどちらかを
選ばなければならないわけではありません。両者を自由に組み合わせて使い、
それぞれが最も得意な役割を担わせることができます。たとえば、リスト 17-25 は
実世界の Rust コードにおけるこの種の組み合わせの、かなり一般的な例を示して
います。
extern crate trpl; // for mdbook test
use std::{thread, time::Duration};
fn main() {
let (tx, mut rx) = trpl::channel();
thread::spawn(move || {
for i in 1..11 {
tx.send(i).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
trpl::block_on(async {
while let Some(message) = rx.recv().await {
println!("{message}");
}
});
}
まず async チャネルを作成し、それから move キーワードを使ってチャネルの
送信側の所有権を受け取るスレッドを生成します。スレッドの中では、1 から 10
までの数値を送信し、その間に毎回 1 秒ずつスリープします。最後に、この章を
通して行ってきたのと同じように、trpl::block_on に渡した async ブロックで
作成した Future を実行します。その Future では、これまで見てきたほかの
メッセージ受け渡しの例と同じように、それらのメッセージを待機します。
章の冒頭で取り上げたシナリオに戻ると、専用スレッドを使って一連の動画 エンコードタスクを実行し(動画エンコードは計算バウンドであるため)、 それらの操作が完了したことを async チャネルで UI に通知する場面を想像して みてください。この種の組み合わせの例は、実世界のユースケースに無数にあります。
まとめ
この本で並行性を扱うのはこれが最後ではありません。第21章 のプロジェクトでは、ここで扱ったより単純な例よりも現実的な状況でこれらの 概念を適用し、スレッド化とタスクおよび Future による問題解決をより直接的に 比較します。
これらのアプローチのどれを選ぶにしても、Rust は安全で高速な並行コードを 書くために必要なツールを提供してくれます。高スループットな Web サーバー向け であれ、組み込みオペレーティングシステム向けであれ、それは同じです。
次は、Rust プログラムが大きくなるにつれて、問題をモデル化し解決策を構造化 するための慣用的な方法について話します。さらに、Rust のイディオムが、 オブジェクト指向プログラミングで見慣れているかもしれないものとどのように 関係しているかも議論します。
オブジェクト指向プログラミングの機能
オブジェクト指向プログラミング(OOP)は、プログラムをモデル化する方法の 1つです。プログラム上の概念としてのオブジェクトは、1960年代に プログラミング言語 Simula で導入されました。これらのオブジェクトは、 オブジェクト同士が互いにメッセージを渡すという Alan Kay の プログラミングアーキテクチャに影響を与えました。彼はこの アーキテクチャを説明するために、1967年に オブジェクト指向プログラミング という用語を生み出しました。OOP が 何であるかを説明する定義は数多く競合しており、それらの定義の一部によれば Rust はオブジェクト指向ですが、別の定義によればそうではありません。この章 では、一般にオブジェクト指向と見なされるいくつかの特性と、それらの特性が Rust らしい書き方においてどのように表れるかを見ていきます。続いて、Rust でオブジェクト指向のデザインパターンを実装する方法を示し、そのようにする ことのトレードオフを、代わりに Rust の強みの一部を使って解決策を実装する 場合と比較しながら議論します。
オブジェクト指向言語の特徴
オブジェクト指向言語の特徴
ある言語がオブジェクト指向と見なされるためにどのような機能を 備えていなければならないかについては、プログラミングコミュニティの 中で合意がありません。Rust は OOP を含む多くのプログラミング パラダイムの影響を受けています。たとえば、第 13 章では関数型 プログラミングに由来する機能を見てきました。少なくとも、OOP 言語には いくつかの共通した特徴、すなわちオブジェクト、カプセル化、継承がある と言えるでしょう。それぞれの特徴が何を意味するのか、そして Rust が それをサポートしているかどうかを見ていきましょう。
オブジェクトはデータと振る舞いを含む
Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides による書籍 Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994)は、通称 The Gang of Four 本として知られており、 オブジェクト指向の設計パターンをまとめたカタログです。この本では OOP を 次のように定義しています。
オブジェクト指向プログラムはオブジェクトで構成されます。オブジェクトは、 データと、そのデータに対して動作する手続きをひとまとめにします。手続きは 通常、メソッドまたは操作と呼ばれます。
この定義に従えば、Rust はオブジェクト指向です。構造体と列挙型はデータを
持ち、impl ブロックは構造体や列挙型に対するメソッドを提供します。
メソッドを持つ構造体や列挙型はオブジェクトと 呼ばれる わけでは
ありませんが、Gang of Four によるオブジェクトの定義に従えば、同じ
機能を提供しています。
実装の詳細を隠すカプセル化
OOP と一般的に結び付けられるもう 1 つの側面は カプセル化 という考え方で、 これはオブジェクトの実装詳細に、そのオブジェクトを使うコードからは アクセスできないことを意味します。したがって、オブジェクトとやり取りする 唯一の方法はその公開 API を通すことです。オブジェクトを使うコードは、 オブジェクトの内部に立ち入ってデータや振る舞いを直接変更できるべきでは ありません。これにより、プログラマはそのオブジェクトを利用するコードを 変更せずに、オブジェクト内部を変更したりリファクタリングしたりできます。
第 7 章では、カプセル化をどのように制御するかを説明しました。pub
キーワードを使って、コード中のどのモジュール、型、関数、メソッドを公開
するかを決められ、デフォルトではそれ以外はすべて非公開になります。
たとえば、i32 値のベクタを保持するフィールドを持つ
AveragedCollection 構造体を定義できます。この構造体はさらに、ベクタ内の
値の平均を保持するフィールドも持てるため、誰かが必要とするたびに平均を
その場で計算する必要はありません。言い換えると、AveragedCollection は
計算済みの平均をキャッシュしてくれます。リスト 18-1 に
AveragedCollection 構造体の定義を示します。
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
この構造体には、他のコードから使えるように pub が付いていますが、
構造体の内部にあるフィールドは非公開のままです。この場合これが重要なのは、
リストに値が追加または削除されるたびに、平均も更新されることを保証したい
からです。これを実現するために、リスト 18-2 に示すように、この構造体に
add、remove、average メソッドを実装します。
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}
pub fn average(&self) -> f64 {
self.average
}
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
公開メソッド add、remove、average は、AveragedCollection の
インスタンス内のデータにアクセスしたり変更したりする唯一の手段です。
add メソッドを使って項目が list に追加されたり、remove メソッドを
使って削除されたりすると、それぞれの実装は非公開の update_average
メソッドを呼び出し、average フィールドの更新も行います。
list フィールドと average フィールドは非公開のままにしているため、
外部コードが list フィールドに対して直接項目を追加したり削除したりする
方法はありません。そうしないと、list が変更されたときに average
フィールドの値が同期しなくなる可能性があるからです。average メソッドは
average フィールドの値を返すので、外部コードは平均を読み取れますが、
それを変更することはできません。
AveragedCollection 構造体の実装詳細をカプセル化しているので、将来、
データ構造のような要素を簡単に変更できます。たとえば、list
フィールドには Vec<i32> の代わりに HashSet<i32> を使うこともできます。
公開メソッド add、remove、average のシグネチャが同じままである限り、
AveragedCollection を使うコードは変更する必要がありません。これに対して
list を公開にしていた場合は、必ずしもそうとは限りません。
HashSet<i32> と Vec<i32> では項目を追加・削除するためのメソッドが
異なるため、外部コードが list を直接変更していたなら、そのコードも
おそらく変更しなければならないでしょう。
ある言語がオブジェクト指向と見なされるためにカプセル化が必須の要素で
あるなら、Rust はその要件を満たしています。コードの各部分に対して pub
を使うかどうかを選べることにより、実装詳細のカプセル化が可能になります。
型システムとしての継承とコード共有としての継承
継承 とは、あるオブジェクトが別のオブジェクトの定義から要素を 受け継ぎ、それによって親オブジェクトのデータや振る舞いを、あらためて 定義しなくても獲得できる仕組みです。
もしある言語がオブジェクト指向であるために継承を備えていなければ ならないのなら、Rust はそのような言語ではありません。マクロを使わずに、 親構造体のフィールドとメソッド実装を継承する構造体を定義する方法は ありません。
とはいえ、プログラミングの道具箱の中に継承があることに慣れているなら、 そもそもなぜ継承を使いたいのかに応じて、Rust では別の解決策を使えます。
継承を選ぶ主な理由は 2 つあります。1 つはコードの再利用です。ある型に
特定の振る舞いを実装し、継承によってその実装を別の型でも再利用できます。
Rust のコードでは、デフォルトのトレイトメソッド実装を使うことで、これを
限定的に行えます。これは、リスト 10-14 で Summary トレイトに
summarize メソッドのデフォルト実装を追加したときに見たものです。
Summary トレイトを実装するあらゆる型は、追加のコードなしで
summarize メソッドを利用できます。これは、親クラスがメソッドの実装を
持ち、それを継承する子クラスもそのメソッド実装を持つことに似ています。
また、Summary トレイトを実装する際に summarize メソッドのデフォルト
実装をオーバーライドすることもできます。これは、子クラスが親クラスから
継承したメソッドの実装をオーバーライドすることに似ています。
継承を使うもう 1 つの理由は、型システムに関係しています。つまり、子型を 親型と同じ場所で使えるようにするためです。これは ポリモーフィズム とも 呼ばれ、複数のオブジェクトがある特性を共有していれば、実行時にそれらを 互いに置き換えて使えることを意味します。
ポリモーフィズム
多くの人にとって、ポリモーフィズムは継承と同義です。しかし、実際にはこれはより一般的な概念であり、 複数の型のデータを扱えるコードを指します。継承においては、それらの型は一般に サブクラスです。
その代わりに Rust は、ジェネリクスを使ってさまざまな可能性のある型を抽象化し、 トレイト境界を使ってそれらの型が提供しなければならないものに制約を課します。これは 境界付きパラメトリックポリモーフィズム と呼ばれることもあります。
Rust は、継承を提供しないことで、異なる一連のトレードオフを選択しています。 継承では、必要以上のコードを共有してしまう危険がしばしばあります。サブクラスは 親クラスのすべての特性を常に共有すべきとは限りませんが、継承ではそうなってしまいます。 これにより、プログラムの設計の柔軟性が低くなる可能性があります。また、 そのメソッドがサブクラスに適用されないために意味をなさなかったり、エラーを 引き起こしたりするメソッドをサブクラスに対して呼び出してしまう可能性も生じます。 さらに、一部の言語では 単一継承(つまり、サブクラスが継承できるのは 1 つのクラス からだけということ)しか許されず、プログラム設計の柔軟性がさらに制限されます。
これらの理由から、Rust は、実行時にポリモーフィズムを実現するために、 継承の代わりにトレイトオブジェクトを使用するという別のアプローチを取っています。 トレイトオブジェクトがどのように機能するのかを見ていきましょう。
トレイトオブジェクトを使って共有の振る舞いを抽象化する
共通の振る舞いを抽象化するためにトレイトオブジェクトを使う
第8章では、ベクタの制限の1つとして、格納できる要素が1つの型に
限られることに触れました。リスト 8-9 では、この制限に対する回避策として、
整数、浮動小数点数、テキストを保持するバリアントを持つ SpreadsheetCell
enum を定義しました。これにより、各セルに異なる型のデータを格納しつつ、
セルの1行を表すベクタを持つことができました。これは、相互に入れ替えて使う
項目が、コードのコンパイル時に分かっている固定の型の集合である場合には、
まったく問題のない解決策です。
しかし、状況によっては、ある特定の場面で有効な型の集合を、ライブラリの
利用者が拡張できるようにしたいことがあります。これをどのように実現できる
かを示すために、画面上に描画するために各項目に対して draw メソッドを
呼び出しながら項目のリストを反復処理する、グラフィカルユーザー
インターフェイス (GUI) ツールの例を作ります。これは GUI ツールで一般的な
手法です。gui という名前のライブラリクレートを作成し、その中に GUI
ライブラリの構造を含めます。このクレートには、Button や TextField の
ような、利用者が使えるいくつかの型が含まれるかもしれません。さらに、
gui の利用者は、自分で描画可能な型も作りたいと思うでしょう。たとえば、
あるプログラマは Image を追加し、別のプログラマは SelectBox を追加する
かもしれません。
ライブラリを書いている時点では、ほかのプログラマが作りたい型をすべて
知ることも、定義することもできません。しかし、gui がさまざまな型の多く
の値を追跡し、それらの異なる型の値それぞれに対して draw メソッドを
呼び出す必要があることは分かっています。draw メソッドを呼び出したときに
具体的に何が起こるかを正確に知る必要はなく、その値に、呼び出せるその
メソッドが用意されていることだけが必要です。
これを継承のある言語で行うなら、draw という名前のメソッドを持つ
Component というクラスを定義するかもしれません。Button、Image、
SelectBox などのほかのクラスは Component を継承し、その結果として
draw メソッドも継承します。それぞれが draw メソッドをオーバーライドして
独自の振る舞いを定義できますが、フレームワークはすべての型を Component
インスタンスであるかのように扱い、それらに対して draw を呼び出せます。
しかし Rust には継承がないため、ライブラリ利用者がライブラリと互換性のある
新しい型を作れるように gui ライブラリを構成する別の方法が必要です。
共通の振る舞いのためのトレイトを定義する
gui に持たせたい振る舞いを実装するために、draw という1つのメソッドを
持つ Draw という名前のトレイトを定義します。そうすれば、トレイト
オブジェクトを受け取るベクタを定義できます。トレイトオブジェクト は、
指定したトレイトを実装する型のインスタンスと、その型に対するトレイト
メソッドを実行時に参照するためのテーブルの両方を指します。トレイト
オブジェクトは、参照や Box<T> スマートポインタのような何らかのポインタ型、
続けて dyn キーワード、さらに対象となるトレイトを指定することで作成
します。(トレイトオブジェクトがポインタを使わなければならない理由については、
第20章の 「動的サイズ付き型と Sized トレイト」 で説明します。)
トレイトオブジェクトは、ジェネリック型や具体的な型の代わりに使えます。
トレイトオブジェクトを使う場所ではどこでも、その文脈で使われる値がその
トレイトオブジェクトのトレイトを実装していることを、Rust の型システムが
コンパイル時に保証します。その結果、コンパイル時に取り得る型をすべて
知っておく必要はありません。
Rust では、struct や enum をほかの言語のオブジェクトと区別するために、
それらを「オブジェクト」と呼ぶのは避ける、とこれまでに述べてきました。
struct や enum では、struct のフィールドにあるデータと impl ブロックに
ある振る舞いは分離されています。一方、ほかの言語では、データと振る舞いが
結び付いた1つの概念はしばしばオブジェクトと呼ばれます。トレイト
オブジェクトは、ほかの言語のオブジェクトとは異なり、トレイトオブジェクトに
データを追加することはできません。トレイトオブジェクトは、ほかの言語の
オブジェクトほど汎用的に有用ではありません。トレイトオブジェクトの具体的な
目的は、共通の振る舞いに対する抽象化を可能にすることです。
リスト 18-3 は、draw という1つのメソッドを持つ Draw という名前の
トレイトをどのように定義するかを示しています。
pub trait Draw {
fn draw(&self);
}
この構文は、第10章でトレイトをどのように定義するかを説明したときのものと
見覚えがあるはずです。次に新しい構文が出てきます。リスト 18-4 では、
components という名前のベクタを保持する Screen という名前の struct を
定義しています。このベクタの型は Box<dyn Draw> で、これはトレイト
オブジェクトです。つまり、Draw トレイトを実装する任意の型を Box の中に
入れたものの代わりになります。
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
Screen struct には、リスト 18-5 に示すように、それぞれの components に
対して draw メソッドを呼び出す run という名前のメソッドを定義します。
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
これは、トレイト境界を持つジェネリック型パラメータを使う struct を定義する
場合とは異なる動作をします。ジェネリック型パラメータは、一度に1つの具体的な
型でしか置き換えられませんが、トレイトオブジェクトでは、実行時に複数の
具体的な型をそのトレイトオブジェクトに当てはめることができます。たとえば、
リスト 18-6 のように、ジェネリック型とトレイト境界を使って Screen struct
を定義することもできました。
pub trait Draw {
fn draw(&self);
}
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
この方法では、Button 型だけ、あるいは TextField 型だけからなる
コンポーネントのリストを持つ Screen インスタンスに制限されます。常に
同種のコレクションしか扱わないのであれば、ジェネリクスとトレイト境界を
使うほうが望ましいです。なぜなら、定義はコンパイル時に具体的な型を使うよう
単相化されるからです。
一方、トレイトオブジェクトを使うメソッドでは、1つの Screen インスタンスが
Box<Button> と Box<TextField> の両方を含む Vec<T> を保持できます。
これがどのように機能するのかを見てから、実行時性能への影響について説明
しましょう。
トレイトを実装する
ここで、Draw トレイトを実装するいくつかの型を追加します。Button 型を
用意しましょう。繰り返しになりますが、実際に GUI ライブラリを実装することは
この本の範囲を超えているので、draw メソッドの本体には役に立つ実装は
含めません。実装がどのようなものになるかを想像するために言えば、Button
struct には、リスト 18-7 に示すように、width、height、label の
フィールドがあるかもしれません。
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// code to actually draw a button
}
}
Button の width、height、label フィールドは、ほかの
コンポーネントのフィールドとは異なります。たとえば、TextField
型はそれらと同じフィールドに加えて、placeholder
フィールドを持つかもしれません。画面上に描画したい各型は Draw
トレイトを実装しますが、その特定の型をどのように描画するかを定義する
ために、draw メソッドでは異なるコードを使います。ここで Button
がそうしているようにです(前述のとおり、実際のGUIコードは省いています)。
たとえば Button 型には、ユーザーがボタンをクリックしたときに何が
起こるかに関するメソッドを含む、追加の impl
ブロックがあるかもしれません。この種のメソッドは、TextField
のような型には当てはまりません。
私たちのライブラリを使う人が、width、height、options
フィールドを持つ SelectBox 構造体を実装すると決めた場合、
リスト18-8に示すように、SelectBox 型にも Draw
トレイトを実装することになります。
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
fn main() {}
これで、私たちのライブラリの利用者は Screen
インスタンスを作成する main 関数を書けます。各値を Box<T>
に入れてトレイトオブジェクトにすることで、Screen
インスタンスに SelectBox と Button を追加できます。そして、
Screen インスタンスに対して run
メソッドを呼び出せば、各コンポーネントに対して draw
が呼び出されます。リスト18-9はこの実装を示しています。
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
use gui::{Button, Screen};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
ライブラリを書いたときには、誰かが SelectBox
型を追加するかもしれないとはわかっていませんでした。しかし、
SelectBox は Draw トレイトを実装しており、つまり draw
メソッドを実装しているため、Screen
の実装はその新しい型に対しても動作し、それを描画できました。
この概念、つまり値の具体的な型ではなく、その値がどのメッセージに応答
するかだけを気にするという考え方は、動的型付け言語における
ダックタイピング の概念に似ています。アヒルのように歩き、
アヒルのように鳴くなら、それはアヒルに違いない、というわけです!
リスト18-5の Screen に対する run の実装では、run
は各コンポーネントの具体的な型が何であるかを知る必要がありません。
コンポーネントが Button のインスタンスか SelectBox
かを確認することはせず、ただそのコンポーネントに対して draw
メソッドを呼び出します。components ベクタ内の値の型として
Box<dyn Draw> を指定することで、Screen は draw
メソッドを呼び出せる値を必要とするように定義されています。
ダックタイピングを使うコードに似たコードを、トレイトオブジェクトと Rustの型システムを使って書く利点は、ある値が特定のメソッドを実装して いるかどうかを実行時に確認する必要がなく、また、値がそのメソッドを 実装していないのに呼び出してしまってエラーになる心配もないことです。 値がトレイトオブジェクトに必要なトレイトを実装していなければ、Rust はコードをコンパイルしません。
たとえば、リスト18-10は、コンポーネントとして String を持つ
Screen を作成しようとするとどうなるかを示しています。
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
String は Draw トレイトを実装していないため、このエラーが
表示されます。
$ cargo run
Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5 | components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
|
= help: the trait `Draw` is implemented for `Button`
= note: required for the cast from `Box<String>` to `Box<dyn Draw>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` (bin "gui") due to 1 previous error
このエラーによって、Screen
に渡すつもりのなかったものを渡してしまっているので別の型を渡すべき
なのか、あるいは Screen がそれに対して draw
を呼び出せるように、String に Draw
を実装すべきなのかがわかります。
動的ディスパッチを行う
第10章の「ジェネリクスを使うコードの パフォーマンス」で、 コンパイラがジェネリクスに対して行う単相化プロセスについて議論したことを 思い出してください。コンパイラは、ジェネリック型パラメータの代わりに 使う各具体的な型ごとに、関数やメソッドの非ジェネリックな実装を生成 します。単相化の結果として得られるコードは 静的ディスパッチ を行います。これは、コンパイラがコンパイル時にどのメソッドを呼び出して いるかを把握している場合のことです。これに対するのが 動的ディスパッチ で、コンパイラがコンパイル時にはどのメソッドを 呼び出しているかわからない場合のことです。動的ディスパッチの場合、 コンパイラは、実行時にどのメソッドを呼び出すべきかがわかるコードを 生成します。
トレイトオブジェクトを使うとき、Rust は動的ディスパッチを使わなければなりません。コンパイラは、トレイト オブジェクトを使うコードでどの型が使われる可能性があるかをすべて 把握していないため、どの型に実装されたどのメソッドを呼べばよいかを 知ることができません。代わりに、実行時にRustはトレイトオブジェクト 内部のポインタを使って、どのメソッドを呼ぶべきかを知ります。この 探索には、静的ディスパッチでは発生しない実行時コストがかかります。 また、動的ディスパッチはコンパイラがメソッドのコードをインライン化 することも妨げ、その結果として一部の最適化もできなくなります。さらに Rustには、動的ディスパッチを使える場所と使えない場所に関する、 dyn互換性 と呼ばれるいくつかのルールがあります。これらのルールは この議論の範囲を超えますが、詳しくはリファレンス を読んでください。しかしその一方で、リスト18-5で書いたコードには 追加の柔軟性が得られ、リスト18-9で示したようなことをサポート できました。したがって、これは検討すべきトレードオフです。
オブジェクト指向デザインパターンを実装する
オブジェクト指向の設計パターンを実装する
状態パターン は、オブジェクト指向の設計パターンです。この パターンの核心は、値が内部的に取りうる一連の状態を定義することにあります。その 状態は一連の 状態オブジェクト で表現され、値の振る舞いはその状態に応じて 変化します。ここでは、状態を保持するフィールドを持つブログ記事の構造体の例を 扱います。この状態は、「下書き」「レビュー」「公開済み」という一連の状態オブジェクトの いずれかになります。
状態オブジェクトは機能を共有します。もちろん Rust では、オブジェクトと継承ではなく、 構造体とトレイトを使います。各状態オブジェクトは、自身の振る舞いと、 いつ別の状態に変化すべきかを管理する責任を持ちます。状態オブジェクトを保持する 値は、状態ごとの異なる振る舞いや、いつ状態間を遷移すべきかについては 何も知りません。
状態パターンを使う利点は、プログラムのビジネス要件が変わったときにも、 状態を保持する値のコードや、その値を使うコードを変更する必要が ないことです。ルールを変更したり、場合によってはさらに状態オブジェクトを追加したり するには、状態オブジェクトのうち 1 つの内部コードを更新するだけで済みます。
まずは、より伝統的なオブジェクト指向のやり方で状態パターンを実装します。 その後、Rust ではもう少し自然なアプローチを使います。状態パターンを用いた ブログ記事のワークフローを、段階的に実装しながら見ていきましょう。
最終的な機能は次のようになります。
- ブログ記事は、空の下書きとして始まります。
- 下書きが完了したら、記事のレビューを依頼します。
- 記事が承認されると、公開されます。
- 公開済みのブログ記事だけが、表示するための内容を返します。これにより、未承認の記事が 誤って公開されることはありません。
記事に対してそのほかの変更を試みても、何の効果もないようにする必要があります。たとえば、 レビューを依頼する前に下書きのブログ記事を承認しようとしても、その記事は 未公開の下書きのままであるべきです。
伝統的なオブジェクト指向スタイルを試みる
同じ問題を解くためにコードを構成する方法は無数にあり、それぞれに 異なるトレードオフがあります。この節の実装は、より伝統的な オブジェクト指向スタイルのもので、Rust でも書くことは可能ですが、 Rust の強みの一部を活かしてはいません。後で、同じくオブジェクト指向の 設計パターンを使いながらも、オブジェクト指向の経験を持つプログラマには あまりなじみがないかもしれない構成の、別の解法を示します。 他の言語のコードとは異なる形で Rust のコードを設計することの トレードオフを体験するために、この 2 つの解法を比較してみましょう。
リスト 18-11 は、このワークフローをコードの形で示しています。これは、
blog という名前のライブラリクレートでこれから実装する API の使用例です。blog
クレートはまだ実装していないため、これはまだコンパイルできません。
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
ユーザーが Post::new で新しい下書きのブログ記事を作成できるようにしたいと考えています。
また、ブログ記事にテキストを追加できるようにもしたいです。承認前に
すぐ記事の内容を取得しようとしても、記事はまだ下書きなので、
テキストは何も得られないはずです。説明のために、コードには
assert_eq! を追加しています。これに対する優れたユニットテストは、
下書きのブログ記事が content メソッドから空文字列を返すことを
アサートすることですが、この例ではテストは書きません。
次に、記事のレビューを依頼できるようにしたいと考えています。そして、
レビュー待ちの間は content が空文字列を返すようにしたいです。記事が
承認を受けたら公開されるべきであり、その場合には content が
呼び出されたときに記事のテキストが返されます。
注目してほしいのは、このクレートからやり取りする型が Post
型だけだということです。この型は状態パターンを使用し、記事が取りうる
さまざまな状態、つまり下書き、レビュー、公開済みを表す 3 つの状態オブジェクトの
いずれかである値を保持します。ある状態から別の状態への変更は、
Post 型の内部で管理されます。状態は、ライブラリのユーザーが
Post インスタンスに対して呼び出すメソッドに応じて変化しますが、
ユーザー自身が直接状態遷移を管理する必要はありません。また、ユーザーが
レビュー前に記事を公開してしまう、といった状態に関する誤りを犯すこともできません。
Post を定義し、新しいインスタンスを作成する
ライブラリの実装を始めましょう。まず、何らかの内容を保持する公開の
Post 構造体が必要だとわかっています。そこで、リスト 18-12 に示すように、
構造体の定義と、Post のインスタンスを作成する関連する公開関数 new から始めます。
また、Post のすべての状態オブジェクトが備えるべき振る舞いを定義する、
非公開の State トレイトも作成します。
その後、Post は state という名前の非公開フィールドに、状態オブジェクトを保持するため
Option<T> の中に Box<dyn State> のトレイトオブジェクトを持ちます。
Option<T> が必要な理由は、少しするとわかります。
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
}
trait State {}
struct Draft {}
impl State for Draft {}
State トレイトは、異なる記事の状態が共有する振る舞いを定義します。状態オブジェクトは
Draft、PendingReview、Published で、これらはすべて State
トレイトを実装します。今のところ、このトレイトにはメソッドはありません。そして、
記事が最初に入る状態がそれだから、まずは Draft 状態だけを定義することから始めます。
新しい Post を作成するとき、その state フィールドには Box を保持する
Some 値を設定します。この Box は Draft 構造体の新しいインスタンスを
指します。これにより、新しい Post インスタンスを作成するたびに、
必ず下書きとして開始されることが保証されます。Post の state フィールドは
非公開なので、ほかの状態の Post を作成する方法はありません。Post::new
関数では、content フィールドを新しい空の String に設定します。
記事内容のテキストを格納する
リスト 18-11 では、add_text という名前のメソッドを呼び出して、
&str を渡し、それがブログ記事のテキスト内容として追加されるようにしたいことが
わかりました。これを content フィールドを pub として公開するのではなく
メソッドとして実装するのは、後で content フィールドのデータが
どのように読み取られるかを制御するメソッドを実装できるようにするためです。
add_text メソッドはかなり単純なので、リスト 18-13 の実装を impl Post ブロックに追加しましょう。
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
trait State {}
struct Draft {}
impl State for Draft {}
add_text メソッドが self への可変参照を受け取るのは、add_text を呼び出している Post インスタンスを変更しているからです。次に、content 内の String に対して push_str を呼び出し、text 引数を渡して保存された content に追加します。この振る舞いは、投稿がどの状態にあるかには依存しないため、ステートパターンの一部ではありません。add_text メソッドは state フィールドとまったくやり取りしませんが、サポートしたい振る舞いの一部です。
Draft 状態の Post の内容が空であることを保証する
add_text を呼び出して投稿にいくらかの内容を追加したあとでも、content メソッドには空の文字列スライスを返してほしいままです。これは、リスト 18-11 の最初の assert_eq! が示しているように、投稿がまだ draft 状態にあるためです。ひとまず、この要件を満たす最も単純な方法、つまり常に空の文字列スライスを返すように content メソッドを実装しましょう。これはあとで、投稿の状態を変更して公開できるようにする機能を実装したら変更します。ここまでのところ、投稿は draft 状態にしかなれないので、投稿内容は常に空であるべきです。リスト 18-14 はこのプレースホルダー実装を示しています。
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
}
trait State {}
struct Draft {}
impl State for Draft {}
この content メソッドを追加すると、リスト 18-11 の最初の assert_eq! までのすべてが意図どおりに動作します。
レビューを依頼すると Post の状態が変化する
次に、投稿のレビューを依頼する機能を追加する必要があります。これにより、状態が Draft から PendingReview に変わるはずです。リスト 18-15 はこのコードを示しています。
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
}
Post には、self への可変参照を受け取る request_review という名前の公開メソッドを追加します。次に、Post の現在の状態に対して内部の request_review メソッドを呼び出します。この 2 つ目の request_review メソッドは現在の状態を消費し、新しい状態を返します。
State トレイトに request_review メソッドを追加します。これにより、今後このトレイトを実装するすべての型は request_review メソッドを実装する必要があります。メソッドの最初の引数が self、&self、&mut self ではなく、self: Box<Self> になっていることに注目してください。この構文は、その型を保持している Box に対して呼び出された場合にのみ、このメソッドが有効であることを意味します。この構文は Box<Self> の所有権を取得し、古い状態を無効化することで、Post の状態値を新しい状態へ変換できるようにします。
古い状態を消費するために、request_review メソッドは状態値の所有権を取得する必要があります。ここで Post の state フィールドにある Option が役に立ちます。take メソッドを呼び出して state フィールドから Some の値を取り出し、その場所には None を残します。これは、Rust では構造体に値の入っていないフィールドを持たせることができないからです。これにより、Post から state の値を借用するのではなく、ムーブできるようになります。そして、その後で投稿の state の値をこの操作の結果に設定します。
state 値の所有権を取得するために、self.state = self.state.request_review(); のようなコードで直接設定するのではなく、state を一時的に None に設定する必要があります。これにより、Post は古い state 値を新しい状態に変換したあと、その古い state 値を使えないことが保証されます。
Draft に対する request_review メソッドは、レビュー待ちの状態を表す新しい PendingReview 構造体の、新しく box 化されたインスタンスを返します。PendingReview 構造体も request_review メソッドを実装しますが、変換は行いません。代わりに自分自身を返します。というのも、すでに PendingReview 状態にある投稿に対してレビューを依頼した場合でも、その投稿は PendingReview 状態のままであるべきだからです。
ここで、ステートパターンの利点が見え始めます。Post の request_review メソッドは、state の値が何であっても同じです。各状態はそれぞれ自分自身のルールに責任を持ちます。
Post の content メソッドはそのままにして、空の文字列スライスを返すようにしておきます。これで Post は Draft 状態だけでなく PendingReview 状態にもなれますが、PendingReview 状態でも同じ振る舞いを望みます。これで、リスト 18-11 は 2 つ目の assert_eq! 呼び出しまで動作するようになります。
approve を追加して content の振る舞いを変える
approve メソッドは request_review メソッドと似たものになります。リスト 18-16 に示されているように、その状態が承認されたときに現在の状態が持つべきだとする値に state を設定します。
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
State トレイトに approve メソッドを追加し、State を実装する新しい構造体である Published 状態も追加します。
PendingReview に対する request_review の動作と同様に、Draft に対して approve メソッドを呼び出しても効果はありません。なぜなら、approve は self を返すからです。PendingReview に対して approve を呼び出すと、新しく box 化された Published 構造体のインスタンスを返します。Published 構造体は State トレイトを実装しており、request_review メソッドと approve メソッドの両方について、自分自身を返します。これは、その場合に投稿が Published 状態のままであるべきだからです。
ここで、Post の content メソッドを更新する必要があります。content から返される値を Post の現在の状態に依存させたいので、リスト 18-17 に示すように、Post が自分の state に定義された content メソッドへ処理を委譲するようにします。
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
// --snip--
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
これらのルールをすべて State を実装する構造体の内部に保つことが目標なので、state 内の値に対して content メソッドを呼び出し、投稿インスタンス(つまり self)を引数として渡します。次に、state 値の content メソッドを使って返された値を返します。
Option の as_ref メソッドを呼び出すのは、値の所有権ではなく、Option の内側にある値への参照が欲しいからです。state は Option<Box<dyn State>> なので、as_ref を呼び出すと Option<&Box<dyn State>> が返されます。as_ref を呼び出さなければ、関数パラメータの借用された &self から state をムーブすることはできないため、エラーになります。
次に unwrap メソッドを呼び出します。これが panic しないことは確実です。なぜなら、Post のメソッドは、それらの処理が終わる時点で state が常に Some の値を含むことを保証していると分かっているからです。これは第9章の “When You Have More Information Than the Compiler” 節で説明したケースの1つで、コンパイラには理解できなくても、None の値が決してありえないと分かっている場合です。
この時点で、&Box<dyn State> に対して content を呼び出すと、& と Box に対してデリファレンス強制が働き、最終的に State トレイトを実装している型に対して content メソッドが呼び出されます。つまり、State トレイト定義に content を追加する必要があり、どの状態にあるかに応じてどの内容を返すかというロジックをそこに置くことになります。これはリスト18-18に示されています。
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
// --snip--
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
空の文字列スライスを返す content メソッドのデフォルト実装を追加します。つまり、Draft 構造体と PendingReview 構造体では content を実装する必要はありません。Published 構造体は content メソッドをオーバーライドして、post.content の値を返します。便利ではあるものの、Post の内容を State 上の content メソッドで決定するのは、State の責務と Post の責務の境界を曖昧にしています。
このメソッドにはライフタイム注釈が必要であることに注意してください。これは第10章で説明しました。引数として post への参照を受け取り、その post の一部への参照を返しているので、返される参照のライフタイムは post 引数のライフタイムに関連付けられます。
これで完了です。リスト18-11全体が動作するようになりました。ブログ記事のワークフローのルールとともに、状態パターンを実装できました。ルールに関するロジックは、Post 全体に散らばるのではなく、状態オブジェクトの中に存在します。
なぜ enum ではないのか?
なぜ異なる記事状態をバリアントとして持つ enum を使わなかったのか、不思議に思っていたかもしれません。もちろん、それも可能な解決策です。試してみて、どちらが好みか最終的な結果を比較してみてください! enum を使う欠点の1つは、enum の値をチェックするあらゆる場所で、すべての可能なバリアントを扱うために
match式、またはそれに類するものが必要になることです。これは、このトレイトオブジェクトによる解決策よりも繰り返しが多くなる可能性があります。
状態パターンの評価
Rust が、各状態において記事が持つべきさまざまな種類の振る舞いをカプセル化するために、オブジェクト指向の状態パターンを実装できることを示しました。Post 上のメソッドは、さまざまな振る舞いについて何も知りません。コードの構成方法のおかげで、公開済みの記事がどのように振る舞うかという異なる方法を知るために見る必要がある場所は1か所だけです。つまり、Published 構造体に対する State トレイトの実装です。
状態パターンを使わない別の実装を作るとしたら、代わりに Post 上のメソッド、あるいは記事の状態を確認してその場所で振る舞いを変える main コードの中で match 式を使うかもしれません。そうなると、記事が公開済み状態にあることのすべての含意を理解するために、複数の場所を見る必要が出てきます。
状態パターンでは、Post のメソッドや Post を使う場所では match 式は不要であり、新しい状態を追加するには、新しい構造体を追加して、その1つの構造体に対して1か所でトレイトメソッドを実装するだけで済みます。
状態パターンを使った実装は、さらに多くの機能を追加するために拡張しやすくなっています。状態パターンを使うコードの保守がどれほど単純かを確認するために、次の提案をいくつか試してみてください。
- 記事の状態を
PendingReviewからDraftに戻すrejectメソッドを追加する。 - 状態を
Publishedに変更できるようになる前に、approveを2回呼び出すことを必須にする。 - 記事が
Draft状態にあるときだけ、ユーザーがテキスト内容を追加できるようにする。 ヒント: 内容について何が変化しうるかは状態オブジェクトの責務としつつ、Postを変更する責務は持たせないようにします。
状態パターンの欠点の1つは、状態が状態間の遷移を実装しているため、状態同士が互いに結び付いてしまうことです。PendingReview と Published の間に Scheduled のような別の状態を追加すると、PendingReview のコードを変更して Scheduled へ遷移するようにしなければなりません。新しい状態の追加に伴って PendingReview を変更しなくて済めば作業は少なくなりますが、それは別のデザインパターンへ切り替えることを意味します。
もう1つの欠点は、いくつかのロジックを重複していることです。この重複をなくすために、State トレイト上の request_review および approve メソッドに、self を返すデフォルト実装を作ろうとするかもしれません。しかし、これはうまくいきません。State をトレイトオブジェクトとして使う場合、そのトレイトは具象的な self が正確には何になるかを知りません。そのため、戻り値の型はコンパイル時に分かりません。(これは先ほど触れた dyn 互換性ルールの1つです。)
そのほかの重複としては、Post 上の request_review メソッドと approve メソッドの実装が似ていることが挙げられます。どちらのメソッドも、Post の state フィールドに対して Option::take を使い、state が Some であれば、ラップされた値の同名メソッドの実装に委譲し、その結果を state フィールドの新しい値として設定します。このパターンに従うメソッドが Post に多数あるなら、繰り返しをなくすためにマクロの定義を検討するかもしれません(第20章の “Macros” 節を参照してください)。
状態パターンをオブジェクト指向言語向けに定義されているとおりに正確に実装することで、Rust の強みを十分には活かせていません。blog クレートに加えられるいくつかの変更を見て、無効な状態や遷移をコンパイル時エラーにできるようにしてみましょう。
状態と振る舞いを型としてエンコードする
異なるトレードオフの組を得るために、状態パターンをどう考え直せるかを示します。状態と遷移を完全にカプセル化して、外部のコードがそれらをまったく知らないようにする代わりに、状態を異なる型にエンコードします。その結果、Rust の型チェックシステムは、公開済み記事だけが許可される場所で下書きの記事を使おうとする試みを、コンパイラエラーを出すことで防いでくれます。
リスト18-11の main の最初の部分を考えてみましょう:
```rust,ignore
# use blog::Post;
#
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
#
# post.request_review();
# assert_eq!("", post.content());
#
# post.approve();
# assert_eq!("I ate a salad for lunch today", post.content());
}
Post::new を使って下書き状態の新しい投稿を作成できることと、投稿の内容にテキストを追加できることは、引き続き有効にします。しかし、空文字列を返す下書き投稿の content メソッドを用意する代わりに、下書き投稿には content メソッド自体を持たせないようにします。そうすれば、下書き投稿の内容を取得しようとしたとき、メソッドが存在しないことを示すコンパイルエラーが発生します。その結果、本番環境で誤って下書き投稿の内容を表示してしまうことは不可能になります。なぜなら、そのコードはそもそもコンパイルされないからです。リスト18-19は、Post 構造体と DraftPost 構造体の定義、およびそれぞれのメソッドを示しています。
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
Post 構造体と DraftPost 構造体はどちらも、ブログ投稿のテキストを保存する非公開の content フィールドを持っています。構造体はもはや state フィールドを持っていません。これは、状態の表現を構造体の型へ移すためです。Post 構造体は公開済みの投稿を表し、content を返す content メソッドを持っています。
Post::new 関数は引き続きありますが、Post のインスタンスを返す代わりに、DraftPost のインスタンスを返します。content は非公開であり、Post を返す関数も存在しないため、現時点では Post のインスタンスを作成することはできません。
DraftPost 構造体には add_text メソッドがあるので、以前と同じように content にテキストを追加できます。ただし、DraftPost には content メソッドが定義されていないことに注意してください。これにより、すべての投稿は下書き投稿として始まり、下書き投稿の内容は表示のために利用できないことがプログラムによって保証されます。こうした制約を回避しようとする試みは、どれもコンパイルエラーになります。
では、どうすれば公開済みの投稿を得られるのでしょうか。私たちは、下書き投稿は公開される前にレビューと承認を受けなければならない、というルールを強制したいと考えています。レビュー待ち状態の投稿も、やはり内容を表示してはいけません。これらの制約を実装するために、別の構造体 PendingReviewPost を追加し、DraftPost に PendingReviewPost を返す request_review メソッドを定義し、PendingReviewPost に Post を返す approve メソッドを定義します。これはリスト18-20に示されています。
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
// --snip--
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost {
content: self.content,
}
}
}
pub struct PendingReviewPost {
content: String,
}
impl PendingReviewPost {
pub fn approve(self) -> Post {
Post {
content: self.content,
}
}
}
request_review メソッドと approve メソッドは self の所有権を受け取るため、DraftPost と PendingReviewPost のインスタンスを消費し、それぞれ PendingReviewPost と公開済みの Post へと変換します。この方法により、それらに対して request_review を呼び出したあとに DraftPost インスタンスが残ることはありません。PendingReviewPost 構造体にも content メソッドは定義されていないため、DraftPost と同様に、その内容を読もうとするとコンパイルエラーになります。content メソッドを持つ公開済みの Post インスタンスを得る唯一の方法は、PendingReviewPost に対して approve メソッドを呼び出すことです。そして PendingReviewPost を得る唯一の方法は、DraftPost に対して request_review メソッドを呼び出すことです。これで、ブログ投稿のワークフローを型システムにエンコードしたことになります。
しかし、main にもいくつか小さな変更を加える必要があります。request_review メソッドと approve メソッドは、呼び出された構造体を変更するのではなく新しいインスタンスを返すため、返されたインスタンスを保存するために、さらに let post = によるシャドーイング代入を追加する必要があります。また、下書き投稿およびレビュー待ち投稿の内容が空文字列であることを確認するアサーションも置けなくなりますし、そもそもそれらは不要です。そうした状態の投稿の内容を使おうとするコードは、もはやコンパイルできないからです。更新後の main のコードをリスト18-21に示します。
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
let post = post.request_review();
let post = post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
main で post を再代入するために必要だった変更は、この実装がもはやオブジェクト指向の状態パターンに完全には従っていないことを意味します。状態間の変換が、もはや Post 実装の内部だけに完全にカプセル化されていないからです。しかし、その代わりに得られるのは、型システムとコンパイル時に行われる型チェックによって、不正な状態が不可能になることです。これにより、未公開の投稿内容を表示してしまうといった特定のバグは、本番環境に到達する前に発見されることが保証されます。
この節の冒頭で提案した課題を、リスト18-21後の blog クレートに対して試してみて、このバージョンのコードの設計についてどう思うか確かめてみてください。この設計では、いくつかの課題はすでに満たされているかもしれないことに注意してください。
Rust はオブジェクト指向の設計パターンを実装できますが、状態を型システムにエンコードするような別のパターンも Rust では利用できることを見てきました。これらのパターンには、それぞれ異なるトレードオフがあります。あなたはオブジェクト指向パターンに非常に慣れているかもしれませんが、Rust の機能を活用するように問題を捉え直すことで、コンパイル時に一部のバグを防げるなどの利点が得られます。所有権のように、オブジェクト指向言語にはない特定の機能があるため、オブジェクト指向パターンが Rust において常に最良の解決策になるとは限りません。
まとめ
この章を読んだあとで Rust がオブジェクト指向言語かどうかをどう考えるにせよ、trait object を使って Rust でいくつかのオブジェクト指向的機能を得られることは、もう分かったはずです。動的ディスパッチは、少しの実行時性能と引き換えに、コードにある程度の柔軟性を与えることができます。この柔軟性を使えば、コードの保守性に役立つオブジェクト指向パターンを実装できます。Rust には、オブジェクト指向言語にはない所有権のような他の機能もあります。オブジェクト指向パターンは、Rust の強みを活かすための最善の方法とは限りませんが、利用可能な選択肢のひとつです。
次は、Rust のもうひとつの機能であり、大きな柔軟性を可能にするパターンについて見ていきます。これまでも本書を通して少し触れてきましたが、その能力をまだ十分には見ていません。さあ、始めましょう!
パターンとマッチング
パターンはRustにおける特別な構文であり、複雑な型にも単純な型にも、その構造に対してマッチさせるために使います。パターンを match 式やその他の構文と組み合わせて使うことで、プログラムの制御フローをより細かく制御できます。パターンは、以下の要素をいくつか組み合わせて構成されます。
- リテラル
- 分解された配列、列挙型、構造体、またはタプル
- 変数
- ワイルドカード
- プレースホルダー
パターンの例としては x、(a, 3)、Some(Color::Red) などがあります。パターンが有効な文脈では、これらの構成要素はデータの形を表します。するとプログラムは値をパターンに照らしてマッチさせ、特定のコード片の実行を続けるために、正しいデータの形をしているかどうかを判定します。
パターンを使うには、それを何らかの値と比較します。パターンがその値にマッチした場合、値の各部分をコード内で使用します。第6章で、コイン仕分け機の例など、パターンを使った match 式を見たことを思い出してください。値がパターンの形に合っていれば、名前の付いた各部分を使えます。そうでなければ、そのパターンに対応するコードは実行されません。
この章では、パターンに関するあらゆる事柄を参照的に扱います。パターンを使用できる有効な場所、失敗しうるパターンと失敗しえないパターンの違い、そして目にするかもしれないさまざまな種類のパターン構文を取り上げます。この章を終える頃には、多くの概念を明快に表現するために、パターンをどう使えばよいかがわかるでしょう。
パターンが使えるあらゆる場所
パターンを使用できるあらゆる場所
Rustではさまざまな場所でパターンが登場し、実はあなたは気づかないうちにそれらをたくさん使ってきました! この節では、パターンが有効になるすべての場所について説明します。
match アーム
第6章で説明したように、match 式のアームではパターンを使います。形式的に言うと、match 式は、キーワード match、照合対象の値、そして1つ以上の match アームから構成されます。各アームは、あるパターンと、その値がそのアームのパターンに一致した場合に実行する式からなります。次のような形です。
match 値 {
パターン => 式,
パターン => 式,
パターン => 式,
}
たとえば、次はリスト6-5の match 式で、変数 x に入っている Option<i32> の値に対してマッチしています。
match x {
None => None,
Some(i) => Some(i + 1),
}
この match 式におけるパターンは、各矢印の左側にある None と Some(i) です。
match 式に対する1つの要件は、それが網羅的でなければならないということです。つまり、match 式の値に対するすべての可能性を考慮しなければなりません。あらゆる可能性をカバーしたことを保証する1つの方法は、最後のアームに残りすべてを受け止めるパターンを置くことです。たとえば、任意の値にマッチする変数名は決して失敗しないため、残っているすべてのケースをカバーします。
特別なパターン _ は何にでもマッチしますが、変数には決して束縛されないため、最後の match アームでよく使われます。たとえば、指定していない値をすべて無視したいときに、_ パターンは便利です。_ パターンについては、この章の後半にある 「パターン内の値を無視する」 でさらに詳しく説明します。
let 文
この章より前では、パターンの利用について match と if let でしか明示的に説明してきませんでした。しかし実際には、let 文を含め、ほかの場所でもパターンを使ってきました。たとえば、次のような単純な let による変数代入を考えてみましょう。
#![allow(unused)]
fn main() {
let x = 5;
}
このような let 文を使うたびに、あなたはパターンを使っていたのです。たとえそれに気づいていなかったとしても! より形式的に言うと、let 文は次のような形をしています。
let パターン = 式;
let x = 5; のように、パターンの位置に変数名がある文では、その変数名は単に特に単純な形のパターンです。Rustはその式をパターンと照合し、見つかった名前をすべて束縛します。したがって、let x = 5; の例では、x は「ここに一致したものを変数 x に束縛する」という意味のパターンです。名前 x がパターン全体であるため、このパターンは実質的に「値が何であっても、すべてを変数 x に束縛する」という意味になります。
let のパターンマッチの側面をより明確に見るために、タプルを分解するために let でパターンを使っているリスト19-1を見てみましょう。
fn main() {
let (x, y, z) = (1, 2, 3);
}
ここでは、タプルをパターンと照合しています。Rustは値 (1, 2, 3) をパターン (x, y, z) と比較し、値がそのパターンに一致すること、つまり両方の要素数が同じであることを確認します。するとRustは 1 を x に、2 を y に、3 を z に束縛します。このタプルパターンは、その内部に3つの個別の変数パターンが入れ子になっているものと考えられます。
パターン内の要素数がタプル内の要素数と一致しない場合、全体の型が一致せず、コンパイラエラーになります。たとえば、リスト19-2は、3要素のタプルを2つの変数に分解しようとする例を示しています。これはうまくいきません。
fn main() {
let (x, y) = (1, 2, 3);
}
このコードをコンパイルしようとすると、次の型エラーになります。
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
--> src/main.rs:2:9
|
2 | let (x, y) = (1, 2, 3);
| ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})`
| |
| expected a tuple with 3 elements, found one with 2 elements
|
= note: expected tuple `({integer}, {integer}, {integer})`
found tuple `(_, _)`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
このエラーを修正するには、「パターン内の値を無視する」 の節で見るように、_ や .. を使ってタプル内の1つ以上の値を無視できます。問題が、パターン内の変数が多すぎることにあるなら、解決策は変数を取り除いて、変数の数がタプルの要素数と等しくなるようにし、型を一致させることです。
条件付き if let 式
第6章では、if let 式の使い方について、主に1つのケースだけにマッチする match と同等のものをより短く書く方法として説明しました。必要に応じて、if let には、if let 内のパターンが一致しない場合に実行するコードを含む対応する else を付けることもできます。
リスト19-3は、if let、else if、else if let、そして else 式を組み合わせることもできることを示しています。こうすることで、パターンと比較する値を1つしか表現できない match 式よりも柔軟になります。またRustでは、if let、else if、else if let の一連のアームにある条件が、互いに関連している必要もありません。
リスト19-3のコードは、いくつかの条件を順にチェックして、背景を何色にするかを決定します。この例では、実際のプログラムならユーザー入力から受け取るかもしれない値を、ハードコードした変数として用意しています。
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
if let Some(color) = favorite_color {
println!("Using your favorite color, {color}, as the background");
} else if is_tuesday {
println!("Tuesday is green day!");
} else if let Ok(age) = age {
if age > 30 {
println!("Using purple as the background color");
} else {
println!("Using orange as the background color");
}
} else {
println!("Using blue as the background color");
}
}
ユーザーがお気に入りの色を指定していれば、その色が背景として使われます。お気に入りの色が指定されておらず、今日が火曜日なら、背景色は緑になります。それ以外の場合で、ユーザーが自分の年齢を文字列として指定しており、それを数値として正常にパースできれば、その数値の値に応じて色は紫またはオレンジになります。これらの条件のどれにも当てはまらない場合、背景色は青になります。
この条件構造により、複雑な要件に対応できます。ここでのハードコードされた値では、この例は Using purple as the background color と出力します。
if let も、match アームと同じように、既存の変数をシャドーイングする新しい変数を導入できることがわかります。if let Ok(age) = age という行では、Ok バリアントの内側の値を含む新しい age 変数が導入され、既存の age 変数をシャドーイングします。つまり、if age > 30 という条件はそのブロック内に配置する必要があります。この 2 つの条件を if let Ok(age) = age && age > 30 のように組み合わせることはできません。30 と比較したい新しい age は、波かっこによって新しいスコープが始まるまでは有効ではないからです。
if let 式を使う欠点は、match 式とは異なり、コンパイラが網羅性をチェックしないことです。最後の else ブロックを省略して、その結果いくつかのケースの処理を見落としていたとしても、コンパイラはその論理バグの可能性を警告してくれません。
while let 条件付きループ
構文は if let に似ていますが、while let 条件付きループでは、パターンがマッチし続ける限り while ループを実行できます。リスト 19-4 では、スレッド間で送信されるメッセージを待ち受ける while let ループを示していますが、この場合は Option ではなく Result をチェックしています。
fn main() {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
for val in [1, 2, 3] {
tx.send(val).unwrap();
}
});
while let Ok(value) = rx.recv() {
println!("{value}");
}
}
この例は 1、2、そして 3 を表示します。recv メソッドはチャネルの受信側から最初のメッセージを取り出し、Ok(value) を返します。第 16 章で recv を初めて見たときは、エラーを直接アンラップするか、for ループを使ってイテレータとして扱いました。しかし、リスト 19-4 が示すように、recv メソッドは送信側が存在する限りメッセージが到着するたびに Ok を返し、送信側が切断されると Err を生成するため、while let を使うこともできます。
for ループ
for ループでは、キーワード for の直後に続く値はパターンです。たとえば、for x in y では、x がパターンです。リスト 19-5 は、for ループの一部としてタプルを分割する、つまり分解するために、for ループでパターンを使う方法を示しています。
fn main() {
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{value} is at index {index}");
}
}
リスト 19-5 のコードは次のように表示します。
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2
enumerate メソッドを使ってイテレータを適応し、値とその値のインデックスを、タプルに格納した形で生成するようにしています。最初に生成される値はタプル (0, 'a') です。この値がパターン (index, value) にマッチすると、index は 0、value は 'a' になり、出力の最初の行が表示されます。
関数パラメータ
関数パラメータもパターンにできます。i32 型の x という名前の 1 つのパラメータを取る foo という関数を宣言しているリスト 19-6 のコードは、ここまでで見慣れたものになっているはずです。
fn foo(x: i32) {
// code goes here
}
fn main() {}
x の部分はパターンです! let で行ったのと同様に、関数の引数でタプルをパターンにマッチさせることができます。リスト 19-7 では、タプルの値を関数に渡す際に分割しています。
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({x}, {y})");
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
このコードは Current location: (3, 5) を表示します。値 &(3, 5) はパターン &(x, y) にマッチするため、x は値 3、y は値 5 です。
クロージャは関数に似ているため、第 13 章で説明したように、関数パラメータのリストと同じようにクロージャのパラメータリストでもパターンを使うことができます。
ここまでで、パターンを使ういくつかの方法を見てきましたが、パターンは使えるすべての場所で同じように機能するわけではありません。ある場所では、パターンは反駁不可能でなければなりません。別の状況では、反駁可能でもかまいません。次に、この 2 つの概念について説明します。
反駁可能性: パターンがマッチに失敗する可能性があるかどうか
反駁可能性: パターンがマッチに失敗する可能性があるかどうか
パターンには、反駁可能なものと反駁不可能なものの2種類があります。渡される可能性のあるどんな値にもマッチするパターンは、反駁不可能 です。例としては、文 let x = 5; における x が挙げられます。x はどんなものにもマッチするため、マッチに失敗することがありません。ある取りうる値に対してはマッチに失敗しうるパターンは、反駁可能 です。例としては、式 if let Some(x) = a_value における Some(x) が挙げられます。というのも、変数 a_value の値が Some ではなく None であれば、Some(x) パターンはマッチしないからです。
関数の引数、let 文、for ループは、反駁不可能なパターンしか受け取れません。というのも、値がマッチしないときに、プログラムは意味のあることを何もできないからです。if let 式、while let 式、および let...else 文は、反駁可能なパターンと反駁不可能なパターンの両方を受け取れますが、コンパイラは反駁不可能なパターンに対して警告を出します。なぜなら、定義上、それらは起こりうる失敗を扱うことを意図しているからです。条件分岐の機能は、成功か失敗かに応じて異なる動作を実行できることにあります。
一般に、反駁可能なパターンと反駁不可能なパターンの違いを気にする必要はありません。しかし、エラーメッセージで見かけたときに対応できるよう、この反駁可能性という概念には慣れておく必要があります。そのような場合には、コードの意図した振る舞いに応じて、パターンか、そのパターンと一緒に使っている構文のどちらかを変更する必要があります。
反駁可能なパターンが必要なのに Rust が反駁不可能なパターンを要求する場所で使おうとした場合、そしてその逆の場合に何が起こるのかを例で見てみましょう。リスト19-8では let 文を示していますが、パターンとして反駁可能なパターンである Some(x) を指定しています。予想どおり、このコードはコンパイルされません。
fn main() {
let some_option_value: Option<i32> = None;
let Some(x) = some_option_value;
}
もし some_option_value が None 値であれば、パターン Some(x) へのマッチは失敗します。つまり、このパターンは反駁可能です。しかし、let 文が受け取れるのは反駁不可能なパターンだけです。というのも、None 値に対してコードができる妥当なことは何もないからです。コンパイル時に、Rust は反駁不可能なパターンが必要な場所で反駁可能なパターンを使おうとしたことに対して文句を言います。
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding
--> src/main.rs:3:9
|
3 | let Some(x) = some_option_value;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
|
3 | let Some(x) = some_option_value else { todo!() };
| ++++++++++++++++
For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
Some(x) というパターンでは、(また、できることならすべての)有効な値を網羅していなかったため、Rust は正当にコンパイラエラーを出します。
反駁不可能なパターンが必要な場所で反駁可能なパターンを使っている場合、それを修正するには、そのパターンを使っているコードを変更できます。let を使う代わりに、let...else を使えるのです。そうすれば、パターンがマッチしなかったときに、中括弧内のコードがその値を処理します。リスト19-9は、リスト19-8のコードを修正する方法を示しています。
fn main() {
let some_option_value: Option<i32> = None;
let Some(x) = some_option_value else {
return;
};
}
これで、このコードには逃げ道が与えられました! このコードは完全に有効です。ただし、その代わり、警告を受けずに反駁不可能なパターンを使うことはできません。リスト19-10に示すように、常にマッチする x のようなパターンを let...else に与えると、コンパイラは警告を出します。
fn main() {
let x = 5 else {
return;
};
}
Rust は、反駁不可能なパターンと一緒に let...else を使うことには意味がないと指摘します。
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `let...else` pattern
--> src/main.rs:2:5
|
2 | let x = 5 else {
| ^^^^^^^^^
|
= note: this pattern will always match, so the `else` clause is useless
= help: consider removing the `else` clause
= note: `#[warn(irrefutable_let_patterns)]` on by default
warning: `patterns` (bin "patterns") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
Running `target/debug/patterns`
このため、match アームは、最後のアームを除いて、反駁可能なパターンを使わなければなりません。最後のアームは、反駁不可能なパターンで残っているすべての値にマッチするべきです。Rust では、アームが1つしかない match で反駁不可能なパターンを使うこともできますが、この構文は特に有用ではなく、より単純な let 文で置き換えられます。
これで、パターンをどこで使うのか、そして反駁可能なパターンと反駁不可能なパターンの違いがわかったので、パターンを作るために使えるすべての構文を見ていきましょう。
パターン構文
パターンの構文
この節では、パターンで有効な構文をすべて集め、それぞれをなぜ、そしてどのようなときに使いたくなるのかを説明します。
リテラルとのマッチング
第6章で見たように、パターンをリテラルに対して直接マッチさせることができます。次のコードはその例です。
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
このコードは、x の値が 1 なので one を出力します。特定の具体的な値を受け取った場合にコードに何らかの動作をさせたいとき、この構文は役に立ちます。
名前付き変数とのマッチング
名前付き変数はどんな値にもマッチする反駁不能なパターンであり、本書でも何度も使ってきました。しかし、名前付き変数を match、if let、while let 式で使う場合には、少し注意が必要です。これらの式はいずれも新しいスコープを開始するため、これらの式の内部でパターンの一部として宣言された変数は、ほかの変数と同様に、構文の外側にある同名の変数をシャドーイングします。リスト19-11では、Some(5) という値を持つ x という変数と、10 という値を持つ y という変数を宣言しています。次に、値 x に対する match 式を作成します。matchアーム内のパターンと最後の println! を見て、このコードを実行したり先を読んだりする前に、何が出力されるか考えてみてください。
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {y}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
match 式が実行されると何が起きるかを追ってみましょう。最初のmatchアームのパターンは、定義されている x の値にマッチしないため、コードは次に進みます。
2番目のmatchアームのパターンは、Some 値の内側にある任意の値にマッチする、新しい y という名前の変数を導入します。ここでは match 式の内側という新しいスコープにいるため、これは新しい y 変数であり、冒頭で値 10 として宣言した y ではありません。この新しい y への束縛は、Some の内側にあるどんな値にもマッチしますが、x がまさにそれです。そのため、この新しい y は x 内の Some の内側の値に束縛されます。その値は 5 なので、このアームの式が実行され、Matched, y = 5 が出力されます。
もし x が Some(5) ではなく None 値だったなら、最初の2つのアームのパターンはマッチしないので、その値はアンダースコアにマッチしたはずです。アンダースコアのアームのパターンでは x 変数を導入していないので、その式にある x は、依然としてシャドーイングされていない外側の x です。この仮定の場合、match は Default case, x = None を出力します。
match 式が終わると、そのスコープも終わり、内側の y のスコープも同様に終わります。最後の println! は at the end: x = Some(5), y = 10 を出力します。
既存の y 変数をシャドーイングする新しい変数を導入するのではなく、外側の x と y の値を比較する match 式を作るには、代わりに match ガード条件を使う必要があります。match ガードについては、後の 「match ガードで条件を追加する」 節で説明します。
複数のパターンとのマッチング
match 式では、パターンの or 演算子である | 構文を使って、複数のパターンにマッチさせることができます。たとえば、次のコードでは x の値をmatchアームに対してマッチさせていますが、その最初のアームには or の選択肢があります。これは、x の値がそのアームにあるどちらかの値にマッチすれば、そのアームのコードが実行されることを意味します。
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}
このコードは one or two を出力します。
..= を使った値の範囲とのマッチング
..= 構文を使うと、両端を含む値の範囲にマッチさせることができます。次のコードでは、パターンが与えられた範囲内のいずれかの値にマッチすると、そのアームが実行されます。
fn main() {
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
}
x が 1、2、3、4、5 のいずれかであれば、最初のアームにマッチします。この構文は、同じ考えを | 演算子で表現するよりも、複数のマッチ値に対して便利です。| を使うとしたら、1 | 2 | 3 | 4 | 5 と指定しなければなりません。範囲を指定するほうがずっと短く、とりわけ 1 から 1,000 までの任意の数にマッチさせたいような場合には便利です。
コンパイラはコンパイル時にその範囲が空でないことを検査します。また、Rust が範囲が空かどうかを判定できる型は char と数値だけなので、範囲は数値または char の値に対してのみ許可されます。
char 値の範囲を使う例を次に示します。
fn main() {
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}
Rust は 'c' が最初のパターンの範囲内にあると判断できるので、early ASCII letter を出力します。
デストラクチャリングで値を分解する
パターンを使って構造体、列挙型、タプルをデストラクチャリングし、それらの値のさまざまな部分を利用することもできます。それぞれの値について見ていきましょう。
構造体
リスト19-12は、x と y という2つのフィールドを持つ Point 構造体を示しています。これは、let 文でパターンを使って分解できます。
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}
このコードは、p 構造体の x フィールドと y フィールドの値にマッチする a と b という変数を作成します。この例は、パターン内の変数名が構造体のフィールド名と一致している必要はないことを示しています。しかし、どの変数がどのフィールドから来たのかを覚えやすくするために、変数名をフィールド名に合わせるのが一般的です。このような一般的な使い方があること、そして let Point { x: x, y: y } = p; と書くと重複が多いことから、Rust には構造体フィールドにマッチするパターンの短縮記法があります。構造体フィールドの名前だけを列挙すればよく、パターンから作られる変数は同じ名前になります。リスト19-13はリスト19-12のコードと同じように動作しますが、let パターンで作成される変数は a と b ではなく x と y です。
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
}
このコードは、変数 p の x フィールドと y フィールドにマッチする x と y という変数を作成します。その結果、変数 x と y には p 構造体の値が入ります。
構造体パターンの一部としてリテラル値を使って分解することもできます。こうすると、すべてのフィールドについて変数を作成するのではなく、一部のフィールドが特定の値を持つかどうかをテストしつつ、ほかのフィールドについては分解して変数を作成できます。
リスト 19-14 では、match 式を使って Point の値を 3 つのケースに分けています。x 軸上にある点(y = 0 のときに成り立つ)、y 軸上にある点(x = 0)、そしてどちらの軸上にもない点です。
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {x}"),
Point { x: 0, y } => println!("On the y axis at {y}"),
Point { x, y } => {
println!("On neither axis: ({x}, {y})");
}
}
}
最初のアームは、y フィールドがリテラル 0 にマッチすることを指定することで、x 軸上にある任意の点にマッチします。このパターンでは、引き続き x 変数も作成されるため、このアームのコード内でそれを使えます。
同様に、2 番目のアームは、x フィールドの値が 0 であることを指定することで、y 軸上の任意の点にマッチし、y フィールドの値に対する変数 y を作成します。3 番目のアームではリテラルをまったく指定していないため、ほかのあらゆる Point にマッチし、x フィールドと y フィールドの両方に対して変数を作成します。
この例では、p の値は x に 0 が入っているため、2 番目のアームにマッチします。したがって、このコードは On the y axis at 7 を出力します。
match 式は最初にマッチするパターンを見つけた時点でアームのチェックをやめることを思い出してください。そのため、Point { x: 0, y: 0 } は x 軸上でも y 軸上でもありますが、このコードが出力するのは On the x axis at 0 だけです。
列挙型
本書ではこれまでも列挙型を分解してきました(たとえば第 6 章のリスト 6-5)。しかし、列挙型を分解するためのパターンが、その列挙型の内部に格納されているデータの定義方法に対応していることは、まだ明示的には説明していませんでした。例として、リスト 19-15 ではリスト 6-2 の Message 列挙型を使い、それぞれの内部の値を分解するパターンを持つ match を書いています。
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.");
}
Message::Move { x, y } => {
println!("Move in the x direction {x} and in the y direction {y}");
}
Message::Write(text) => {
println!("Text message: {text}");
}
Message::ChangeColor(r, g, b) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
}
}
このコードは Change color to red 0, green 160, and blue 255 を出力します。msg の値を変更して、ほかのアームのコードが実行されるのを試してみてください。
Message::Quit のようにデータを持たない列挙型バリアントについては、それ以上値を分解することはできません。Message::Quit というリテラル値そのものにマッチさせることしかできず、そのパターンには変数はありません。
Message::Move のような構造体風の列挙型バリアントでは、構造体にマッチさせるときに指定するのと似たパターンを使えます。バリアント名の後に波かっこを置き、そこでフィールドを変数とともに列挙することで、各部分を分解し、このアームのコード内で使えるようにします。ここでは、リスト 19-13 で行ったのと同じく省略記法を使っています。
Message::Write のように 1 要素のタプルを保持するものや、Message::ChangeColor のように 3 要素のタプルを保持するものといったタプル風の列挙型バリアントでは、パターンはタプルにマッチさせるときに指定するパターンに似ています。パターン内の変数の数は、マッチ対象のバリアントの要素数と一致していなければなりません。
ネストした構造体と列挙型
これまでの例では、構造体または列挙型を 1 段階だけ分解してマッチさせてきましたが、マッチングはネストした項目にも使えます。たとえば、リスト 19-15 のコードをリファクタリングして、ChangeColor メッセージで RGB と HSV の色をサポートできるようにしたものが、リスト 19-16 です。
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to hue {h}, saturation {s}, value {v}");
}
_ => (),
}
}
match 式の最初のアームのパターンは、Color::Rgb バリアントを含む Message::ChangeColor 列挙型バリアントにマッチします。そして、そのパターンは内側の 3 つの i32 値に束縛します。2 番目のアームのパターンも Message::ChangeColor 列挙型バリアントにマッチしますが、内側の列挙型は代わりに Color::Hsv にマッチします。2 つの列挙型が関わっていても、こうした複雑な条件を 1 つの match 式で指定できます。
構造体とタプル
分解パターンは、さらに複雑な形で組み合わせたり、入れ子にしたりできます。次の例では、タプルの中に構造体とタプルをネストし、すべてのプリミティブ値を取り出す複雑な分解を示しています。
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
このコードにより、複雑な型をその構成要素に分解して、関心のある値を個別に使えるようになります。
パターンによる分解は、構造体の各フィールドの値のような、値の一部を互いに分けて利用するための便利な方法です。
パターン内で値を無視する
これまで見てきたように、パターン内では値を無視することが役立つ場合があります。たとえば、match の最後のアームでは、実際には何もしないものの、残っているすべての可能な値を考慮する包括的なパターンを得るために、値を無視できます。パターンの中で値全体または値の一部を無視する方法はいくつかあります。_ パターンを使う方法(これはすでに見ました)、別のパターンの中で _ パターンを使う方法、アンダースコアで始まる名前を使う方法、そして .. を使って値の残りの部分を無視する方法です。これらの各パターンをどのように、そしてなぜ使うのかを見ていきましょう。
_ による値全体の無視
アンダースコアは、どんな値にもマッチするが、その値には束縛しないワイルドカードパターンとして使ってきました。これは match 式の最後のアームで特に便利ですが、リスト 19-17 に示すように、関数の引数を含むあらゆるパターンで使えます。
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}
fn main() {
foo(3, 4);
}
このコードは、最初の引数として渡された値 3 を完全に無視し、This code only uses the y parameter: 4 を出力します。
ほとんどの場合、特定の関数引数が不要になったなら、その未使用の引数を含まないようにシグネチャを変更するでしょう。関数引数を無視することは、たとえばトレイトを実装していて、必要な型シグネチャはあるものの、実装内の関数本体では引数の 1 つが不要である場合に特に便利です。こうすると、名前を使った場合に出る未使用の関数引数に関するコンパイラ警告を避けられます。
ネストした _ による値の一部の無視
別のパターンの内部でも _ を使って、値の一部だけを無視することができます。たとえば、値の一部だけをテストしたい一方で、実行したい対応するコードではそのほかの部分を使わない場合です。リスト 19-18 は、設定の値を管理する役割を持つコードを示しています。ビジネス要件としては、ユーザーは設定に対する既存のカスタマイズを上書きしてはならない一方で、その設定が現在未設定であれば、設定を解除したり値を与えたりできるべきです。
fn main() {
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
(Some(_), Some(_)) => {
println!("Can't overwrite an existing customized value");
}
_ => {
setting_value = new_setting_value;
}
}
println!("setting is {setting_value:?}");
}
このコードは Can't overwrite an existing customized value を出力し、その後
setting is Some(5) を出力します。最初の match アームでは、どちらの Some
バリアントの内部の値についてもマッチしたり使ったりする必要はありませんが、
setting_value と new_setting_value が Some バリアントである場合はテストする必要があります。その場合、setting_value を変更しない理由を出力し、実際に変更もされません。
それ以外のすべての場合(setting_value または new_setting_value のいずれかが None の場合)では、2 番目のアームの _ パターンで表されているように、new_setting_value が setting_value になることを許可したいのです。
また、1 つのパターン内の複数の場所でアンダースコアを使って、特定の値を無視することもできます。リスト 19-19 は、5 要素のタプルのうち 2 番目と 4 番目の値を無視する例を示しています。
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}");
}
}
}
このコードは Some numbers: 2, 8, 32 を出力し、4 と 16 の値は無視されます。
名前を _ で始めることによる未使用変数
変数を作成してもどこでも使わない場合、Rust は通常、未使用の変数がバグである可能性があるため警告を出します。しかし、プロトタイプを作っているときやプロジェクトを始めたばかりのときのように、まだ使わない変数を作成できると便利なことがあります。このような状況では、変数名をアンダースコアで始めることで、その未使用変数について警告しないよう Rust に伝えることができます。リスト 19-20 では、2 つの未使用変数を作成していますが、このコードをコンパイルすると、そのうち 1 つについてだけ警告が出るはずです。
fn main() {
let _x = 5;
let y = 10;
}
ここでは、変数 y を使っていないことについては警告が出ますが、_x を使っていないことについては警告が出ません。
_ だけを使う場合と、アンダースコアで始まる名前を使う場合のあいだには、微妙な違いがあることに注意してください。構文 _x は依然として値をその変数に束縛しますが、_ はまったく束縛しません。この違いが重要になるケースを示すために、リスト 19-21 ではエラーが発生します。
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
_s に s の値が依然としてムーブされるため、s を再び使えなくなり、エラーを受け取ることになります。しかし、アンダースコア単体を使う場合は、値に束縛されることはまったくありません。リスト 19-22 は、s が _ にムーブされないため、エラーなしでコンパイルされます。
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
}
このコードがまったく問題なく動作するのは、s を何にも束縛していないからです。ムーブもされません。
.. による値の残りの部分
多くの部分を持つ値では、.. 構文を使って特定の部分を使い、残りを無視できます。これにより、無視する値ごとにアンダースコアを列挙する必要がなくなります。.. パターンは、そのパターンの残りの部分で明示的にマッチしていない値の部分をすべて無視します。リスト 19-23 では、3 次元空間内の座標を保持する Point 構造体があります。match 式では、x 座標に対してのみ処理を行い、y および z フィールドの値は無視したいと考えています。
fn main() {
struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {x}"),
}
}
x の値を列挙し、そのあとに .. パターンを含めるだけです。これは、特にフィールドの多い構造体を扱っていて、そのうち 1 つか 2 つのフィールドだけが関係する状況では、y: _ や z: _ を列挙するよりも手早く書けます。
構文 .. は、必要な数の値に展開されます。リスト 19-24 は、タプルで .. を使う方法を示しています。
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
このコードでは、最初と最後の値が first と last にマッチします。.. はその間にあるすべてをマッチさせて無視します。
ただし、.. の使用は曖昧であってはなりません。どの値をマッチさせ、どの値を無視する意図なのかが不明確な場合、Rust はエラーを出します。リスト 19-25 は、.. を曖昧に使っている例であるため、コンパイルできません。
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
この例をコンパイルすると、次のエラーが得られます。
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` (bin "patterns") due to 1 previous error
Rust には、second で値をマッチさせる前にタプル内のいくつの値を無視し、その後さらにいくつの値を無視するのかを判断することができません。このコードは、2 を無視して second を 4 に束縛し、その後 8、16、32 を無視したいという意味かもしれませんし、2 と 4 を無視して second を 8 に束縛し、その後 16 と 32 を無視したいという意味かもしれません。ほかにもさまざまな解釈がありえます。変数名 second には Rust にとって特別な意味はないため、このように 2 か所で .. を使うと曖昧になり、コンパイラエラーになります。
マッチガードで条件式を追加する
マッチガード は、match アーム内でパターンの後に指定する追加の if 条件であり、そのアームが選ばれるためにはその条件も満たされなければなりません。マッチガードは、パターンだけでは表現できないより複雑な考えを表すのに役立ちます。ただし、利用できるのは match 式だけであり、if let 式や while let 式では使えません。
この条件では、パターン内で作成された変数を使えます。リスト19-26は、最初のアームが Some(x) というパターンを持ち、さらに if x % 2 == 0 というマッチガードも持つ match を示しています(この条件は数値が偶数なら true になります)。
fn main() {
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("The number {x} is even"),
Some(x) => println!("The number {x} is odd"),
None => (),
}
}
この例は The number 4 is even を出力します。num が最初のアームのパターンと比較されると、Some(4) は Some(x) にマッチするので一致します。次に、マッチガードが x を 2 で割った余りが 0 に等しいかどうかを確認し、実際にそうなので、最初のアームが選択されます。
もし num が Some(5) だった場合、最初のアームのマッチガードは false になっていたでしょう。というのも、5 を 2 で割った余りは 1 であり、0 には等しくないからです。すると Rust は2つ目のアームに進みます。2つ目のアームにはマッチガードがなく、したがって任意の Some バリアントにマッチするので、こちらが一致します。
if x % 2 == 0 という条件をパターンの中で表現する方法はないため、マッチガードによってこのロジックを表現できるようになります。この表現力が増すことの欠点は、マッチガード式が関わる場合、コンパイラが網羅性を検査しようとしないことです。
リスト19-11を説明したとき、パターンのシャドーイング問題を解決するためにマッチガードを使えると述べました。match の外側にある変数を使う代わりに、match 式内のパターンの中で新しい変数を作成したことを思い出してください。その新しい変数のため、外側の変数の値に対してテストできませんでした。リスト19-27は、この問題を修正するためにマッチガードをどう使えるかを示しています。
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {n}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
このコードは今度は Default case, x = Some(5) を出力します。2つ目の match アームのパターンでは、外側の y をシャドーイングする新しい変数 y は導入していないため、マッチガードの中で外側の y を使えます。外側の y をシャドーイングしてしまう Some(y) というパターンを指定する代わりに、Some(n) を指定します。これにより、新しい変数 n が作られますが、match の外側には n という変数がないので、何もシャドーイングしません。
マッチガード if n == y はパターンではないので、新しい変数は導入しません。この y は、それをシャドーイングする新しい y ではなく、まさに 外側の y です。そして、n と y を比較することで、外側の y と同じ値を持つ値を探せます。
マッチガードでは or 演算子 | を使って複数のパターンを指定することもできます。マッチガードの条件は、それらすべてのパターンに適用されます。リスト19-28は、| を使うパターンとマッチガードを組み合わせたときの優先順位を示しています。この例で重要なのは、if y というマッチガードが 4、5、そして 6 に適用されることであり、一見すると if y が 6 にしか適用されないように見える点です。
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
このマッチ条件は、x の値が 4、5、または 6 に等しく、かつ y が true の場合にのみ、そのアームが一致することを表しています。このコードを実行すると、最初のアームのパターンは x が 4 なので一致しますが、マッチガード if y は false なので、最初のアームは選ばれません。コードは次のアームに進み、そちらは一致するため、このプログラムは no を出力します。その理由は、if 条件が最後の値 6 だけではなく、パターン全体 4 | 5 | 6 に適用されるからです。言い換えると、パターンに対するマッチガードの優先順位は次のように振る舞います。
(4 | 5 | 6) if y => ...
次のようになるのではありません。
4 | 5 | (6 if y) => ...
コードを実行すると、この優先順位の振る舞いは明らかです。もしマッチガードが | 演算子を使って指定された値の一覧の最後の値にしか適用されないのであれば、そのアームは一致し、プログラムは yes を出力していたでしょう。
@ バインディングを使う
at 演算子 @ を使うと、その値がパターンマッチするかどうかをテストすると同時に、その値を保持する変数を作成できます。リスト19-29では、Message::Hello の id フィールドが 3..=7 の範囲内にあることをテストしたいと考えています。また、その値を変数 id に束縛して、そのアームに対応するコードの中で使えるようにもしたいと考えています。
fn main() {
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id @ 3..=7 } => {
println!("Found an id in range: {id}")
}
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {id}"),
}
}
この例は Found an id in range: 5 を出力します。範囲 3..=7 の前に id @ を指定することで、その範囲にマッチした値が何であっても、それを id という名前の変数に束縛すると同時に、その値が範囲パターンにマッチしたこともテストしています。
2つ目のアームでは、パターン内に範囲しか指定していないため、そのアームに対応するコードには id フィールドの実際の値を含む変数がありません。id フィールドの値は 10、11、あるいは 12 だったかもしれませんが、そのパターンに対応するコードはどれなのかを知りません。id の値を変数に保存していないため、パターンのコードは id フィールドの値を使えません。
最後のアームでは、範囲なしで変数を指定しているため、id という名前の変数として、そのアームのコードで使える値があります。これは、構造体フィールドの省略記法を使っているからです。しかしこのアームでは、最初の2つのアームで行ったように、id フィールドの値に対して何のテストも適用していません。どんな値でもこのパターンにマッチします。
@ を使うと、1つのパターンの中で値をテストし、その値を変数に保存できます。
まとめ
Rust のパターンは、異なる種類のデータを区別するのに非常に役立ちます。match 式で使うと、Rust はパターンが取り得るすべての値を網羅していることを保証し、そうでなければプログラムはコンパイルされません。let 文や関数パラメータにおけるパターンは、そうした構文をより便利にし、値をより小さな部分に分解して、それらの部分を変数に代入できるようにします。必要に応じて、単純なパターンも複雑なパターンも作成できます。
次は、本書の最後から2番目の章として、Rust のさまざまな機能の高度な側面を見ていきます。
高度な機能
ここまでで、Rustプログラミング言語の最も一般的に使われる部分を学んできました。第21章でもう1つのプロジェクトに取り組む前に、たまに遭遇するかもしれないものの、日常的には使わないかもしれない言語のいくつかの側面を見ていきましょう。何かわからないものに出会ったときには、この章をリファレンスとして使えます。ここで扱う機能は、非常に特定の状況で役立ちます。頻繁に使うことはないかもしれませんが、Rustが提供するすべての機能をきちんと把握できるようにしておきたいと考えています。
この章では、次の内容を扱います。
- Unsafe Rust: Rustの保証の一部を無効にする方法と、それらの保証を手作業で維持する責任を引き受けること
- 高度なトレイト: 関連型、デフォルトの型パラメータ、完全修飾構文、スーパートレイト、およびトレイトに関連する newtype パターン
- 高度な型: newtype パターン、型エイリアス、never 型、動的サイズ付き型についてさらに詳しく
- 高度な関数とクロージャ: 関数ポインタとクロージャを返すこと
- マクロ: コンパイル時に、さらに多くのコードを定義するコードを定義する方法
まさに、誰にとっても何かしらある Rust 機能の大集合です! さっそく見ていきましょう!
Unsafe Rust
Unsafe Rust
これまでに説明してきたコードはすべて、Rust のメモリ安全性保証がコンパイル時に強制されるものでした。しかし、Rust には、こうしたメモリ安全性保証を強制しない、内部に隠れたもうひとつの言語があります。それが unsafe Rust で、通常の Rust と同じように動作しますが、追加のスーパーパワーを与えてくれます。
unsafe Rust が存在するのは、静的解析が本質的に保守的だからです。コンパイラがコードが保証を満たしているかどうかを判断しようとするとき、無効なプログラムをいくつか受け入れてしまうよりも、有効なプログラムをいくつか拒否してしまうほうが望ましいのです。コードは 問題ないかもしれません が、Rust コンパイラが確信を持つための十分な情報を持っていない場合、そのコードは拒否されます。このような場合には、unsafe コードを使ってコンパイラに「信じてください、自分が何をしているかは分かっています」と伝えられます。ただし、unsafe Rust の使用は自己責任であることに注意してください。unsafe コードを誤って使うと、null ポインタの逆参照のような、メモリ安全性の欠如による問題が発生する可能性があります。
Rust に unsafe というもうひとつの顔があるもうひとつの理由は、その土台となるコンピュータハードウェア自体が本質的に unsafe だからです。Rust が unsafe な操作を許可しなければ、特定の作業を行えません。Rust では、オペレーティングシステムと直接やり取りしたり、独自のオペレーティングシステムを書いたりするといった、低レベルのシステムプログラミングを可能にする必要があります。低レベルのシステムプログラミングを扱えることは、この言語の目標の 1 つです。unsafe Rust で何ができるのか、そしてそれをどう行うのかを見ていきましょう。
unsafeなスーパーパワーを使う
unsafe Rust に切り替えるには、unsafe キーワードを使い、その後に unsafe コードを含む新しいブロックを始めます。unsafe Rust では、safe Rust ではできない 5 つの操作が可能で、これらを unsafeなスーパーパワー と呼びます。これらのスーパーパワーには、次のことを行う能力が含まれます。
- 生ポインタを逆参照する。
- unsafe 関数またはメソッドを呼び出す。
- 可変な静的変数にアクセスまたは変更する。
- unsafe トレイトを実装する。
unionのフィールドにアクセスする。
理解しておくべき重要な点は、unsafe は借用チェッカを無効にしたり、Rust のほかの安全性チェックを無効にしたりするものではないということです。unsafe コードの中で参照を使えば、それは依然として検査されます。unsafe キーワードが与えるのは、コンパイラがメモリ安全性について検査しない、これら 5 つの機能へのアクセスだけです。unsafe ブロックの内部でも、ある程度の安全性は引き続き得られます。
さらに、unsafe は、そのブロック内のコードが必ずしも危険であるとか、確実にメモリ安全性の問題を抱えるという意味でもありません。意図としては、プログラマであるあなたが、unsafe ブロック内のコードが有効な方法でメモリにアクセスすることを保証する、ということです。
人は誤りを犯すものであり、ミスは起こります。しかし、これら 5 つの unsafe な操作を unsafe と注釈されたブロック内に置くことを要求することで、メモリ安全性に関するエラーは unsafe ブロック内にあるはずだと分かります。unsafe ブロックは小さく保ちましょう。後でメモリバグを調査するときに、そのありがたみが分かります。
unsafe コードをできるだけ隔離するためには、そのようなコードを安全な抽象化の中に閉じ込め、安全な API を提供するのが最善です。これについては、この章の後半で unsafe 関数とメソッドを調べるときに説明します。標準ライブラリの一部は、監査済みの unsafe コードの上に構築された安全な抽象化として実装されています。unsafe コードを安全な抽象化で包むと、あなたやその利用者が unsafe コードで実装された機能を使いたいあらゆる場所へ unsafe の使用が漏れ出すのを防げます。なぜなら、安全な抽象化を使うこと自体は安全だからです。
5 つの unsafe なスーパーパワーを順に見ていきましょう。また、unsafe コードに安全なインターフェースを提供するいくつかの抽象化も見ていきます。
生ポインタを逆参照する
第 4 章の [「ダングリング参照」][dangling-references] 節で、参照は常に有効であることをコンパイラが保証すると述べました。unsafe Rust には、参照に似た 生ポインタ と呼ばれる 2 つの新しい型があります。参照と同様に、生ポインタは不変にも可変にもでき、それぞれ *const T と *mut T と表記します。アスタリスクは逆参照演算子ではなく、型名の一部です。生ポインタの文脈では、不変 は、ポインタを逆参照してもその参照先に直接代入できないことを意味します。
参照やスマートポインタとは異なり、生ポインタは次の性質を持ちます。
- 借用規則を無視できるため、同じ場所に対して不変ポインタと 可変ポインタの両方、または複数の可変ポインタを持てる
- 有効なメモリを指していることが保証されない
- null であってもよい
- 自動的なクリーンアップを一切実装しない
Rust にこれらの保証を強制させないことを選ぶことで、保証された安全性を手放す代わりに、より高いパフォーマンスや、Rust の保証が適用されない他言語やハードウェアとのインターフェース能力を得られます。
リスト 20-1 は、不変の生ポインタと可変の生ポインタを作成する方法を示しています。
fn main() {
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;
}
このコードには unsafe キーワードが含まれていないことに注目してください。生ポインタは safe コード内でも作成できます。ただし、少し後で見るように、unsafe ブロックの外で生ポインタを逆参照することはできません。
生借用演算子を使って生ポインタを作成しました。&raw const num
は *const i32 の不変な生ポインタを作成し、&raw mut num は *mut i32 の可変な生ポインタを作成します。これらはローカル
変数から直接作成したので、この特定の生ポインタが有効であることは分かります。しかし、任意の生ポインタについてそのようには仮定できません。
これを示すために次は、raw borrow 演算子を使う代わりに、キーワード as を使って値をキャストすることで、有効性をそれほど確信できない生ポインタを作成します。リスト 20-2 は、メモリ上の任意の位置への生ポインタを作成する方法を示しています。任意のメモリを使おうとすることは未定義動作です。そのアドレスにデータがあるかもしれませんし、ないかもしれません。コンパイラがコードを最適化して、メモリアクセスがまったく行われないこともありますし、プログラムがセグメンテーションフォールトで終了することもあります。通常、このようなコードを書く正当な理由はありません。特に、代わりに生借用演算子を使える場合はなおさらです。しかし、そうすること自体は可能です。
fn main() {
let address = 0x012345usize;
let r = address as *const i32;
}
生ポインタは safe コード内で作成できますが、生ポインタを逆参照して、その指し示すデータを読み取ることはできないことを思い出してください。リスト 20-3 では、生ポインタに対して逆参照演算子 * を使用しており、そのために unsafe ブロックが必要になります。
fn main() {
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
}
ポインタを作成すること自体に害はありません。問題になり得るのは、そのポインタが指している値にアクセスしようとしたときであり、その際に無効な値を扱うことになる可能性があります。
また、リスト20-1および20-3では、どちらも num が格納されている同じメモリ位置を指す *const i32 と *mut i32 の生ポインタを作成したことにも注目してください。もし代わりに、num への不変参照と可変参照を作成しようとしていたら、そのコードはコンパイルされなかったでしょう。なぜなら、Rust の所有権ルールでは、不変参照が存在しているのと同時に可変参照を許可していないからです。生ポインタであれば、同じ位置への可変ポインタと不変ポインタを作成し、可変ポインタを通じてデータを変更できます。その結果、データ競合が発生する可能性があります。注意してください!
このような危険があるにもかかわらず、なぜ生ポインタを使うのでしょうか。大きなユースケースの 1 つは、次の節で見るように C コードとやり取りするときです。もう 1 つのケースは、借用チェッカーでは理解できない安全な抽象化を構築するときです。これから unsafe 関数を導入し、その後で unsafe コードを使う安全な抽象化の例を見ていきます。
unsafe 関数またはメソッドを呼び出す
unsafe ブロック内で実行できる 2 つ目の種類の操作は、unsafe 関数を呼び出すことです。unsafe 関数やメソッドは、通常の関数やメソッドとまったく同じ見た目ですが、定義の先頭に追加で unsafe が付きます。この文脈における unsafe キーワードは、その関数を呼び出す際に私たちが守らなければならない要件があることを示しています。Rust は、それらの要件が満たされていることを保証できないからです。unsafe 関数を unsafe ブロックの中で呼び出すことで、私たちはその関数のドキュメントを読み、その関数の契約を守る責任を引き受けることを表明しています。
以下は、本体では何もしない dangerous という名前の unsafe 関数です。
fn main() {
unsafe fn dangerous() {}
unsafe {
dangerous();
}
}
dangerous 関数は、別個の unsafe ブロックの中で呼び出さなければなりません。unsafe ブロックなしで dangerous を呼び出そうとすると、エラーになります。
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe block
--> src/main.rs:4:5
|
4 | dangerous();
| ^^^^^^^^^^^ call to unsafe function
|
= note: consult the function's documentation for information on how to avoid undefined behavior
For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
unsafe ブロックを使うことで、私たちは Rust に対して、その関数のドキュメントを読み、適切な使い方を理解し、その関数の契約を満たしていることを確認したと表明しています。
unsafe 関数の本体の中で unsafe な操作を実行するには、通常の関数内と同様に、やはり unsafe ブロックを使う必要があります。忘れるとコンパイラが警告します。これにより、unsafe な操作が関数本体全体にわたって必要とは限らないため、unsafe ブロックをできるだけ小さく保つことができます。
unsafe コードに対する安全な抽象化を作る
関数に unsafe コードが含まれているからといって、その関数全体を unsafe としてマークする必要があるわけではありません。実際、unsafe コードを安全な関数で包むことは一般的な抽象化です。例として、標準ライブラリの split_at_mut 関数を見てみましょう。この関数には一部 unsafe コードが必要です。これをどのように実装できるかを探っていきます。この安全なメソッドは可変スライスに対して定義されています。1 つのスライスを受け取り、引数として与えられたインデックスでそのスライスを分割して 2 つにします。リスト20-4は split_at_mut の使い方を示しています。
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];
let r = &mut v[..];
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
}
この関数は、安全な Rust だけでは実装できません。試みると、リスト20-5のようなものになるでしょうが、これはコンパイルされません。簡単のため、ここでは split_at_mut をメソッドではなく関数として実装し、またジェネリック型 T ではなく i32 型のスライスに対してのみ実装します。
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
assert!(mid <= len);
(&mut values[..mid], &mut values[mid..])
}
fn main() {
let mut vector = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut vector, 3);
}
この関数はまずスライスの全長を取得します。次に、パラメータとして与えられたインデックスがスライス内にあることを、長さ以下かどうかを確認することでアサートします。このアサーションは、スライスを分割するインデックスとして長さより大きい値を渡した場合、そのインデックスを使おうとする前に関数が panic することを意味します。
次に、タプルの中に 2 つの可変スライスを返します。1 つは元のスライスの先頭から mid インデックスまで、もう 1 つは mid からスライスの末尾までです。
リスト20-5のコードをコンパイルしようとすると、次のようなエラーになります。
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
--> src/main.rs:6:31
|
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
6 | (&mut values[..mid], &mut values[mid..])
| --------------------------^^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*values` is borrowed for `'1`
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
Rust の借用チェッカーは、私たちがスライスの異なる部分を借用していることを理解できません。借用チェッカーが知っているのは、同じスライスから 2 回借用しているということだけです。スライスの異なる部分を借用すること自体は、本質的には問題ありません。2 つのスライスは重なっていないからです。しかし、Rust はそれを理解できるほど賢くありません。コードが正しいと私たちには分かっているのに、Rust には分からない場合、それは unsafe コードの出番です。
リスト20-6は、unsafe ブロック、生ポインタ、そしていくつかの unsafe 関数の呼び出しを使って、split_at_mut の実装を機能させる方法を示しています。
use std::slice;
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut vector = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut vector, 3);
}
第4章の [「スライス型」][the-slice-type] 節で説明したように、スライスは何らかのデータへのポインタと、そのスライスの長さから成ります。len メソッドを使ってスライスの長さを取得し、as_mut_ptr
メソッドを使ってスライスの生ポインタにアクセスします。この場合、i32 値への可変スライスを持っているので、as_mut_ptr は型 *mut i32 の生ポインタを返し、これを変数 ptr に格納しています。
mid インデックスがスライス内にあることを確認するアサーションはそのまま維持します。次に unsafe コードに入ります。slice::from_raw_parts_mut 関数は、生ポインタと長さを受け取り、スライスを作成します。この関数を使って、ptr から始まり長さが mid 要素のスライスを作成します。次に、ptr に対して mid を引数として add メソッドを呼び出し、mid 位置から始まる生ポインタを取得し、そのポインタと mid 以降に残っている要素数を長さとして使ってスライスを作成します。
slice::from_raw_parts_mut 関数が unsafe なのは、生ポインタを受け取り、そのポインタが有効であることを信頼しなければならないからです。生ポインタに対する add メソッドも、オフセット先の位置がやはり有効なポインタであることを信頼しなければならないため unsafe です。そのため、これらを呼び出せるように、slice::from_raw_parts_mut と add の呼び出しを unsafe ブロックで囲む必要がありました。コードを見て、さらに mid が len 以下でなければならないというアサーションを追加することで、unsafe ブロック内で使われるすべての生ポインタが、スライス内のデータを指す有効なポインタであると判断できます。これは unsafe の許容できる適切な使い方です。
結果として得られる split_at_mut 関数を unsafe としてマークする必要は
なく、この関数は安全な Rust から呼び出せます。私たちは、この関数が使える
データから有効なポインタだけを生成するような、安全な方法で unsafe
コードを用いて関数を実装することで、unsafe なコードに対する安全な
抽象化を作り出しています。
対照的に、リスト 20-7 にある slice::from_raw_parts_mut の使用は、
そのスライスが使われたときにおそらくクラッシュするでしょう。このコードは
任意のメモリ位置を受け取り、長さ 10,000 のスライスを作成します。
fn main() {
use std::slice;
let address = 0x01234usize;
let r = address as *mut i32;
let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}
私たちはこの任意の位置にあるメモリを所有しておらず、このコードが作成
するスライスに有効な i32 値が含まれている保証もありません。values
を有効なスライスであるかのように使おうとすると、未定義動作になります。
extern 関数を使って外部コードを呼び出す
Rust のコードが、別の言語で書かれたコードとやり取りする必要が生じることも
あります。このために、Rust には extern キーワードがあり、これにより
外部関数インターフェイス (FFI) を作成して利用しやすくなっています。FFI
は、あるプログラミング言語が関数を定義し、別の(外部の)
プログラミング言語がそれらの関数を呼び出せるようにする仕組みです。
リスト 20-8 では、C 標準ライブラリの abs 関数との統合をセットアップする
方法を示します。extern ブロック内で宣言された関数は、一般に Rust
コードから呼び出すには安全ではないため、extern ブロックにも unsafe
を付けなければなりません。その理由は、他の言語は Rust の規則や保証を
強制せず、Rust もそれらを検査できないので、安全性を確保する責任が
プログラマに委ねられるからです。
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
unsafe extern "C" ブロックの中では、呼び出したい別言語の外部関数の名前
とシグネチャを列挙します。"C" の部分は、その外部関数がどの
アプリケーションバイナリインターフェイス (ABI) を使うかを定義します:
ABI は、アセンブリレベルで関数をどのように呼び出すかを定義します。"C"
ABI はもっとも一般的で、C プログラミング言語の ABI に従います。Rust が
サポートするすべての ABI に関する情報は [Rust Reference][ABI] にあります。
unsafe extern ブロック内で宣言された各項目は、暗黙的に unsafe です。
しかし、FFI 関数の中には、呼び出しても 安全な ものもあります。たとえば、
C の標準ライブラリにある abs 関数にはメモリ安全性に関する考慮事項がなく、
任意の i32 で呼び出せることがわかっています。このような場合には、
unsafe extern ブロック内にあるとしても、この特定の関数は安全に呼び出せる
と示すために safe キーワードを使えます。その変更を行えば、リスト 20-9
に示すように、それを呼び出すのに unsafe ブロックはもう不要になります。
unsafe extern "C" {
safe fn abs(input: i32) -> i32;
}
fn main() {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
関数を safe としてマークしても、それだけで本質的に安全になるわけでは
ありません! むしろ、それは「これが安全である」と Rust に約束している
ようなものです。その約束が守られるようにする責任は、依然としてあなたにあります!
他の言語から Rust 関数を呼び出す
extern を使って、他の言語から Rust 関数を呼び出せるインターフェイスを
作成することもできます。extern ブロック全体を作成する代わりに、対象の
関数に対する fn キーワードの直前に extern キーワードを追加し、
使用する ABI を指定します。また、この関数名を Rust コンパイラが
マングルしないように伝えるため、#[unsafe(no_mangle)] アノテーションも
追加する必要があります。マングリング とは、コンパイラが関数に与えた
名前を、コンパイル過程の他の部分が利用できるより多くの情報を含む一方で、
人間には読みにくい別の名前に変更することです。各プログラミング言語の
コンパイラは名前を少しずつ異なる形でマングルするため、Rust の関数を他の
言語から名前で参照できるようにするには、Rust コンパイラの名前の
マングリングを無効にしなければなりません。これは、組み込みのマングリングが
ないとライブラリ間で名前衝突が起こる可能性があるため unsafe であり、
そのため、選んだ名前をマングリングなしで安全に公開できるようにする責任は私たちにあります。
次の例では、call_from_c 関数を共有ライブラリにコンパイルして C から
リンクしたあと、C コードからアクセスできるようにします:
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
println!("C から Rust 関数が呼び出されました!");
}
この extern の使い方では、unsafe が必要なのは属性だけであり、
extern ブロックには不要です。
可変な静的変数へのアクセスまたは変更
この本ではまだグローバル変数について触れていませんが、Rust はこれを サポートしている一方で、Rust の所有権規則と組み合わせると問題になることが あります。2 つのスレッドが同じ可変なグローバル変数にアクセスすると、 データ競合を引き起こす可能性があります。
Rust では、グローバル変数は 静的 変数と呼ばれます。リスト 20-10 には、 文字列スライスを値として持つ静的変数の宣言と使用の 例が示されています。
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("value is: {HELLO_WORLD}");
}
静的変数は、定数に似ています。定数については、第 3 章の
[「定数の宣言」][constants] 節で説明しました。静的変数の
名前は、慣例として SCREAMING_SNAKE_CASE です。静的変数には
'static ライフタイムを持つ参照しか保存できません。つまり、Rust
コンパイラがそのライフタイムを判断できるので、私たちはそれを明示的に
注釈する必要がありません。不変な静的変数へのアクセスは安全です。
定数と不変な静的変数の微妙な違いの 1 つは、静的変数内の値はメモリ上の
固定アドレスを持つことです。その値を使うと、常に同じデータにアクセスする
ことになります。一方、定数は使われるたびにそのデータを複製してよいことに
なっています。もう 1 つの違いは、静的変数は可変にもできることです。
可変な静的変数へのアクセスと変更は unsafe です。リスト 20-11 では、
COUNTER という名前の可変な静的変数を宣言し、アクセスし、変更する
方法を示します。
static mut COUNTER: u32 = 0;
/// SAFETY: Calling this from more than a single thread at a time is undefined
/// behavior, so you *must* guarantee you only call it from a single thread at
/// a time.
unsafe fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
unsafe {
// SAFETY: This is only called from a single thread in `main`.
add_to_count(3);
println!("COUNTER: {}", *(&raw const COUNTER));
}
}
通常の変数と同様に、可変性は mut キーワードを使って指定します。COUNTER から読み取ったり COUNTER に書き込んだりするコードは、すべて unsafe ブロック内になければなりません。リスト 20-11 のコードはコンパイルされ、予想どおり COUNTER: 3 を出力します。これはシングルスレッドだからです。複数のスレッドから COUNTER にアクセスすると、おそらくデータ競合が発生するため、未定義動作になります。したがって、関数全体を unsafe としてマークし、安全性に関する制約を文書化しておく必要があります。そうすることで、この関数を呼び出す人は、何を安全に行えて、何を安全には行えないのかを把握できます。
unsafe 関数を書くときはいつでも、SAFETY で始まるコメントを書き、その関数を安全に呼び出すために呼び出し側が何をする必要があるのかを説明するのが慣例です。同様に、unsafe な操作を行うときもいつでも、SAFETY で始まるコメントを書き、安全性のルールがどのように守られているかを説明するのが慣例です。
さらに、コンパイラはコンパイラ lint によって、可変な静的変数への参照を作成しようとする試みをデフォルトで拒否します。その lint の保護を明示的に無効化するために #[allow(static_mut_refs)] アノテーションを追加するか、raw borrow 演算子のいずれかで作成した生ポインタを介して可変な静的変数にアクセスしなければなりません。これには、このコードリストの println! で使われているときのように、参照が見えない形で作成される場合も含まれます。静的な可変変数への参照を生ポインタ経由で作成するよう要求することで、それらを使う際の安全性要件がより明確になります。
グローバルにアクセス可能な可変データでは、データ競合がないことを保証するのが難しいため、Rust は可変な静的変数を unsafe だと見なします。可能であれば、異なるスレッドからのデータアクセスが安全に行われていることをコンパイラが検査できるように、第 16 章で説明した並行性のテクニックやスレッドセーフなスマートポインタを使うほうが望ましいです。
Unsafe トレイトを実装する
unsafe は unsafe トレイトの実装にも使えます。トレイトは、そのメソッドの少なくとも 1 つに、コンパイラが検証できない何らかの不変条件がある場合に unsafe になります。トレイトが unsafe であることは、trait の前に unsafe キーワードを追加して宣言し、リスト 20-12 に示すように、そのトレイトの実装も unsafe としてマークします。
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
fn main() {}
unsafe impl を使うことで、私たちはコンパイラが検証できない不変条件を自分たちが守ると約束していることになります。
例として、第 16 章の [「Send と Sync による拡張可能な並行性」][send-and-sync] 節で説明した Send と Sync のマーカートレイトを思い出してください。私たちの型が Send と Sync を実装したほかの型だけで構成されている場合、コンパイラはこれらのトレイトを自動的に実装します。生ポインタのように Send や Sync を実装していない型を含む型を実装し、その型を Send や Sync としてマークしたい場合は、unsafe を使わなければなりません。私たちの型が、スレッド間で安全に送信されたり、複数のスレッドからアクセスされたりできるという保証を満たしているかどうかを Rust は検証できません。そのため、それらの検査を手動で行い、そのことを unsafe で示す必要があります。
Union のフィールドにアクセスする
unsafe でしか行えない最後の操作は、union のフィールドにアクセスすることです。ユニオン は struct に似ていますが、特定のインスタンスでは、ある時点で使われるのは宣言されたフィールドのうち 1 つだけです。ユニオンは主に、C コード内の union とやり取りするために使われます。union のフィールドへのアクセスが unsafe なのは、union インスタンスに現在格納されているデータの型を Rust が保証できないためです。union については [Rust Reference][unions] でさらに学べます。
Miri を使って Unsafe コードを検査する
unsafe コードを書くときには、自分が書いたものが本当に安全で正しいかどうかを確認したいと思うかもしれません。そのための最良の方法の 1 つが、未定義動作を検出するための公式 Rust ツールである Miri を使うことです。borrow checker がコンパイル時に動作する 静的 ツールであるのに対し、Miri は実行時に動作する 動的 ツールです。Miri は、あなたのプログラムやそのテストスイートを実行し、Rust がどのように動作すべきかについて Miri が理解しているルールに違反したときにそれを検出することで、コードを検査します。
Miri を使うには Rust の nightly ビルドが必要です(これについては [付録 G: Rust の作られ方と「Nightly Rust」][nightly] でさらに説明します)。Rust の nightly 版と Miri ツールの両方は、rustup +nightly component add miri と入力することでインストールできます。これは、プロジェクトが使用する Rust のバージョンを変更するものではありません。必要なときに使えるように、単にそのツールをシステムに追加するだけです。プロジェクトに対して Miri を実行するには、cargo +nightly miri run または cargo +nightly miri test と入力します。
これがどれほど役立つかの例として、リスト 20-7 に対して実行したときに何が起こるかを見てみましょう。
$ cargo +nightly miri run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `file:///home/.rustup/toolchains/nightly/bin/cargo-miri runner target/miri/debug/unsafe-example`
warning: integer-to-pointer cast
--> src/main.rs:5:13
|
5 | let r = address as *mut i32;
| ^^^^^^^^^^^^^^^^^^^ integer-to-pointer cast
|
= help: this program is using integer-to-pointer casts or (equivalently) `ptr::with_exposed_provenance`, which means that Miri might miss pointer bugs in this program
= help: see https://doc.rust-lang.org/nightly/std/ptr/fn.with_exposed_provenance.html for more details on that operation
= help: to ensure that Miri does not miss bugs in your program, use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead
= help: you can then set `MIRIFLAGS=-Zmiri-strict-provenance` to ensure you are not relying on `with_exposed_provenance` semantics
= help: alternatively, `MIRIFLAGS=-Zmiri-permissive-provenance` disables this warning
= note: BACKTRACE:
= note: inside `main` at src/main.rs:5:13: 5:32
error: Undefined Behavior: pointer not dereferenceable: pointer must be dereferenceable for 40000 bytes, but got 0x1234[noalloc] which is a dangling pointer (it has no provenance)
--> src/main.rs:7:35
|
7 | let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: BACKTRACE:
= note: inside `main` at src/main.rs:7:35: 7:70
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error; 1 warning emitted
Miri は、整数をポインタにキャストしており、それが問題かもしれないことを正しく警告してくれますが、そのポインタがどのように生成されたかを知らないため、実際に問題があるかどうかまでは判断できません。続いて、私たちがダングリングポインタを持っているため、リスト 20-7 に未定義動作がある箇所で Miri はエラーを返します。Miri のおかげで、今では未定義動作のリスクがあることが分かり、コードをどうすれば安全にできるかを考えられます。場合によっては、Miri はエラーの修正方法について提案までしてくれます。
Miri は、unsafe コードを書くときに起こしうるあらゆる誤りを捕まえるわけではありません。Miri は動的解析ツールなので、実際に実行されたコードに関する問題しか検出しません。つまり、書いた unsafe コードに対する信頼を高めるには、Miri を適切なテスト手法と併用する必要があります。また、Miri はコードが不健全になりうるあらゆる可能な形を網羅しているわけでもありません。
別の言い方をすると、Miri が問題を 検出した なら、そこにバグがあることは分かりますが、Miri がバグを 検出しなかった からといって、問題がないとは限りません。それでも、Miri は非常に多くのことを検出できます。この章にあるほかの unsafe コードの例でも試してみて、何と言うかを見てみてください。
Miri については、[GitHub リポジトリ][miri] でさらに学べます。
Unsafe コードを正しく使う
先ほど説明した 5 つの超能力のいずれかを使うために unsafe を使うこと自体は、間違いでも忌避されることでもありません。しかし、コンパイラがメモリ安全性の維持を助けてくれないため、unsafe コードを正しく書くのはより難しくなります。unsafe コードを使う理由があるなら、そうしてかまいませんし、unsafe という明示的な注釈があることで、問題が発生したときにその原因を追跡しやすくなります。unsafe コードを書くときはいつでも、Miri を使うことで、自分が書いたコードが Rust のルールを守っているという確信をより高められます。
unsafe Rust を効果的に扱う方法をさらに深く掘り下げたいなら、Rust の unsafe に関する公式ガイドである [The Rustonomicon][nomicon] を読んでください。
[dangling-references]: ch04-02-references-and-borrowing.html#dangling-references
[ABI]: ../reference/items/external-blocks.html#abi
[constants]: ch03-01-variables-and-mutability.html#declaring-constants
[send-and-sync]: ch16-04-extensible-concurrency-sync-and-send.html
[the-slice-type]: ch04-03-slices.html#the-slice-type
[unions]: ../reference/items/unions.html
[miri]: https://github.com/rust-lang/miri
[editions]: appendix-05-editions.html
[nightly]: appendix-07-nightly-rust.html
[nomicon]: https://doc.rust-lang.org/nomicon/
高度なトレイト
高度なトレイト
トレイトについては、第10章の「トレイトによる共有動作の定義」節で最初に取り上げましたが、より高度な詳細については説明しませんでした。ここまでで Rust についてより多くを学んだので、今度は細かなところまで踏み込めます。
関連型を持つトレイトの定義
関連型 は、型のプレースホルダーをトレイトに結び付けることで、トレイトのメソッド定義がシグネチャの中でそれらのプレースホルダー型を使えるようにするものです。トレイトの実装者は、その実装においてプレースホルダー型の代わりに使う具体的な型を指定します。これにより、トレイトが実装されるまでそれらの型が正確には何であるかを知らなくても、いくつかの型を使うトレイトを定義できます。
この章で取り上げる高度な機能の大半は、必要になることがまれだと説明してきました。関連型はその中間にあります。つまり、本書の他の部分で説明した機能より使われる頻度は低い一方で、この章で扱う他の多くの機能よりは一般的に使われます。
関連型を持つトレイトの一例は、標準ライブラリが提供する Iterator トレイトです。関連型の名前は Item で、Iterator トレイトを実装する型が反復処理する値の型を表します。Iterator トレイトの定義は、リスト 20-13 に示すとおりです。
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
型 Item はプレースホルダーであり、next メソッドの定義は、それが Option<Self::Item> 型の値を返すことを示しています。Iterator トレイトの実装者は Item に対する具体的な型を指定し、next メソッドはその具体的な型の値を含む Option を返します。
関連型はジェネリクスと似た概念に見えるかもしれません。後者は、どの型を扱えるかを指定せずに関数を定義できるからです。この 2 つの概念の違いを確認するために、Item 型が u32 であることを指定した、Counter という名前の型に対する Iterator トレイトの実装を見てみましょう。
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
この構文は、ジェネリクスの構文と似ているように見えます。では、なぜリスト 20-14 に示すように、単に Iterator トレイトをジェネリクスで定義しないのでしょうか。
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
違いは、リスト 20-14 のようにジェネリクスを使う場合、各実装で型注釈を付けなければならないことです。Iterator<String> for Counter やそのほかの型についても実装できるため、Counter に対して Iterator の複数の実装を持てる可能性があります。言い換えると、トレイトがジェネリックパラメータを持つ場合、そのジェネリック型パラメータの具体的な型を毎回変えながら、1 つの型に対してそのトレイトを複数回実装できます。Counter に対して next メソッドを使うときは、どの Iterator の実装を使いたいのかを示すために型注釈を指定しなければなりません。
関連型を使う場合は、型注釈は必要ありません。というのも、1 つの型に対して同じトレイトを複数回実装することはできないからです。関連型を使う定義であるリスト 20-13 では、impl Iterator for Counter は 1 つしか存在できないため、Item の型として何を使うかを選べるのは 1 回だけです。Counter に対して next を呼び出すたびに、u32 値のイテレータが欲しいことを指定する必要はありません。
関連型は、トレイトの契約の一部にもなります。トレイトの実装者は、関連型プレースホルダーの代わりとなる型を提供しなければなりません。関連型には、その型がどのように使われるかを説明する名前が付いていることが多く、API ドキュメントでその関連型を説明するのはよい実践です。
デフォルトのジェネリック型パラメータと演算子のオーバーロードの使用
ジェネリック型パラメータを使うときは、そのジェネリック型に対してデフォルトの具体的な型を指定できます。これにより、デフォルトの型で問題ない場合、トレイトの実装者は具体的な型を指定する必要がなくなります。デフォルト型は、<PlaceholderType=ConcreteType> 構文を使ってジェネリック型を宣言するときに指定します。
この手法が役立つ状況の好例が、演算子のオーバーロード です。これは、特定の状況で演算子(+ など)の振る舞いをカスタマイズするものです。
Rust では、独自の演算子を作成したり、任意の演算子をオーバーロードしたりすることはできません。しかし、演算子に対応するトレイトを実装することで、std::ops に列挙されている演算と対応トレイトをオーバーロードできます。たとえばリスト 20-15 では、2 つの Point インスタンスを加算するように + 演算子をオーバーロードしています。これは、Point 構造体に対して Add トレイトを実装することで行います。
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
add メソッドは、2 つの Point インスタンスの x 値と y 値をそれぞれ加算して、新しい Point を生成します。Add トレイトには Output という関連型があり、add メソッドから返される型を決定します。
このコードにおけるデフォルトのジェネリック型は、Add トレイトの中にあります。定義は次のとおりです。
#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
}
このコードは、全体として見れば見慣れたもののはずです。1 つのメソッドと 1 つの関連型を持つトレイトです。新しい部分は Rhs=Self です。この構文は デフォルト型パラメータ と呼ばれます。Rhs ジェネリック型パラメータ(“right-hand side” の略)は、add メソッドの rhs パラメータの型を定義します。Add トレイトを実装するときに Rhs に具体的な型を指定しない場合、Rhs の型はデフォルトで Self になります。これは、Add を実装している対象の型です。
Point に対して Add を実装したとき、2 つの Point インスタンスを加算したかったので、Rhs にはデフォルト値を使いました。次に、デフォルトを使うのではなく Rhs 型をカスタマイズしたい場合の Add トレイト実装例を見てみましょう。
異なる単位の値を保持する 2 つの構造体 Millimeters と Meters があり
ます。既存の型を別の構造体で薄くラップするこの手法は newtype パター
ン と呼ばれ、これについては 「newtype パターンで外部トレイトを実装
する」 節でより詳しく説明します。ミリメート
ル単位の値とメートル単位の値を加算し、その際に Add の実装で正しく変
換が行われるようにしたいと考えています。リスト 20-16 に示すように、
Meters を Rhs として Millimeters に対して Add を実装できます。
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
Millimeters と Meters を加算するには、Self というデフォルトを使
う代わりに、Rhs 型パラメータの値を設定するために impl Add<Meters>
を指定します。
デフォルト型パラメータの主な使い方は 2 つあります。
- 既存のコードを壊さずに型を拡張するため
- ほとんどのユーザーには不要な特定のケースでカスタマイズを可能にするため
標準ライブラリの Add トレイトは、2 つ目の目的の例です。
通常は同じ種類の 2 つの型を加算しますが、Add トレイトはそれを超えた
カスタマイズを可能にします。Add トレイトの定義でデフォルト型パラメー
タを使うことで、ほとんどの場合に追加のパラメータを指定する必要がなくな
ります。つまり、少しばかりの実装用ボイラープレートが不要になり、その分
トレイトを使いやすくなります。
1 つ目の目的は 2 つ目と似ていますが逆方向です。既存のトレイトに型パラ メータを追加したい場合、それにデフォルト値を与えることで、既存の実装 コードを壊さずにトレイトの機能を拡張できます。
同名のメソッド間の曖昧さを解消する
Rust では、あるトレイトが別のトレイトのメソッドと同じ名前のメソッドを 持つことを妨げるものは何もありませんし、1 つの型に対して両方のトレイト を実装することも妨げられません。さらに、トレイトのメソッドと同じ名前の メソッドを、その型自体に直接実装することもできます。
同じ名前のメソッドを呼び出すときは、どれを使いたいのかを Rust に伝える
必要があります。リスト 20-17 のコードを考えてみましょう。ここでは、どち
らも fly というメソッドを持つ 2 つのトレイト、Pilot と Wizard を
定義しています。次に、すでに fly という名前のメソッドが実装されている
型 Human に対して、その両方のトレイトを実装します。それぞれの fly
メソッドは異なることを行います。
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {}
リスト 20-18 に示すように、Human のインスタンスに対して fly を呼び
出すと、コンパイラはデフォルトでその型に直接実装されたメソッドを呼び出
します。
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
person.fly();
}
このコードを実行すると *waving arms furiously* と出力され、Rust が
Human に直接実装された fly メソッドを呼び出したことがわかります。
Pilot トレイトまたは Wizard トレイトの fly メソッドを呼び出すに
は、どの fly メソッドを意味しているのかを指定する、より明示的な構文を
使う必要があります。リスト 20-19 はこの構文を示しています。
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
メソッド名の前にトレイト名を指定することで、fly のどの実装を呼び出し
たいのかが Rust に明確に伝わります。また、リスト 20-19 で使った
person.fly() と等価な Human::fly(&person) と書くこともできますが、
曖昧さを解消する必要がないのであれば、こちらは少し長くなります。
このコードを実行すると、次のように出力されます。
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
fly メソッドは self パラメータを取るため、1 つの トレイト を実装
する 2 つの 型 があったとしても、Rust は self の型に基づいて、どの
トレイト実装を使うべきかを判断できます。
しかし、メソッドではない関連関数には self パラメータがありません。
複数の型またはトレイトが同じ関数名の非メソッド関数を定義している場合、
完全修飾構文を使わない限り、Rust にはどの型を意味しているのかが常にわか
るとは限りません。たとえば、リスト 20-20 では、すべての子犬に Spot と
名付けたい動物保護施設のためのトレイトを作成します。関連する非メソッド
関数 baby_name を持つ Animal トレイトを作成します。Animal トレイ
トは構造体 Dog に対して実装されており、Dog 自体にも baby_name と
いう関連する非メソッド関数を直接定義しています。
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
Dog に定義された baby_name 関連関数には、すべての子犬を Spot と名付
けるためのコードを実装しています。Dog 型は、すべての動物が持つ特性を
記述する Animal トレイトも実装しています。犬の赤ちゃんは子犬と呼ばれ
ます。そのことは、Dog に対する Animal トレイト実装において、
Animal トレイトに関連付けられた baby_name 関数で表現されています。
main では Dog::baby_name 関数を呼び出します。これは Dog に定義さ
れた関連関数を直接呼び出します。このコードは次のように出力します。
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
この出力は望んでいたものではありません。コードが
A baby dog is called a puppy と出力されるように、Dog に実装した
Animal トレイトの一部である baby_name 関数を呼び出したいのです。
ここでは、リスト 20-19 で使ったトレイト名を指定する手法は役に立ちませ
ん。main をリスト 20-21 のコードに変更すると、コンパイルエラーになり
ます。
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
Animal::baby_name には self パラメータがなく、さらに Animal トレ
イトを実装するほかの型が存在する可能性もあるため、Rust には
Animal::baby_name のどの実装を使いたいのか判断できません。すると、次
のようなコンパイラエラーが発生します。
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
曖昧さをなくし、他の型に対する Animal の実装ではなく Dog に対する
Animal の実装を使いたいことを Rust に伝えるには、完全修飾構文を使う必要が
あります。リスト20-22は、完全修飾構文の使い方を示しています。
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
山かっこ内で型注釈を Rust に与えることで、この関数呼び出しでは Dog 型を
Animal として扱いたい、つまり Dog に実装された Animal トレイトの
baby_name メソッドを呼び出したいことを示しています。このコードはこれで、
意図した内容を表示します。
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
一般に、完全修飾構文は次のように定義されます。
<Type as Trait>::function(receiver_if_method, next_arg, ...);
メソッドではない関連関数では、receiver はありません。
あるのは他の引数のリストだけです。関数やメソッドを呼び出すすべての場所で
完全修飾構文を使うことができます。しかし、Rust がプログラム内のほかの情報から
判断できる部分については、この構文のその部分を省略してかまいません。同じ名前を
使う実装が複数あり、どの実装を呼び出したいのかを Rust が特定するのに助けが
必要な場合にだけ、このより冗長な構文を使う必要があります。
スーパートレイトを使う
ときには、別のトレイトに依存するトレイト定義を書くことがあります。ある型が 最初のトレイトを実装するには、その型が2つ目のトレイトも実装していることを 要求したい場合です。そうするのは、そのトレイト定義の中で2つ目のトレイトの 関連要素を利用できるようにするためです。あなたのトレイト定義が依存している そのトレイトは、あなたのトレイトの スーパートレイト と呼ばれます。
たとえば、アスタリスクで囲まれるように整形して与えられた値を表示する
outline_print メソッドを持つ OutlinePrint トレイトを作りたいとします。
つまり、標準ライブラリのトレイト Display を実装して (x, y) という形式で
表示される Point 構造体があるとして、x が 1 で y が 3 の Point
インスタンスに対して outline_print を呼び出すと、次のように表示されるはず
です。
**********
* *
* (1, 3) *
* *
**********
outline_print メソッドの実装では、Display トレイトの機能を使いたいと
考えています。したがって、OutlinePrint トレイトが機能するのは、Display
も実装していて OutlinePrint が必要とする機能を提供する型だけであることを
指定する必要があります。これは、トレイト定義で OutlinePrint: Display と
指定することで行えます。このテクニックは、トレイトにトレイト境界を追加する
のに似ています。リスト20-23は、OutlinePrint トレイトの実装を示しています。
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
fn main() {}
OutlinePrint が Display トレイトを必要とすると指定したため、Display を
実装している任意の型に自動的に実装される to_string 関数を使えます。コロンを
追加してトレイト名の後に Display トレイトを指定せずに to_string を
使おうとすると、現在のスコープでは型 &Self に対して to_string という名前の
メソッドは見つからない、というエラーになります。
Point 構造体のように Display を実装していない型に対して OutlinePrint を
実装しようとすると、どうなるか見てみましょう。
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
Display が必要だが実装されていない、というエラーになります。
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:23
|
20 | impl OutlinePrint for Point {}
| ^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:24:7
|
24 | p.outline_print();
| ^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
note: required by a bound in `OutlinePrint::outline_print`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
4 | fn outline_print(&self) {
| ------------- required by a bound in this associated function
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors
これを修正するには、次のように Point に Display を実装して、
OutlinePrint が要求する制約を満たします。
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
そうすれば、Point に対する OutlinePrint トレイトの実装は正常にコンパイル
され、Point インスタンスに対して outline_print を呼び出して、アスタリスクの
枠内に表示できます。
Newtype パターンで外部トレイトを実装する
第10章の「型にトレイトを実装する」節では、トレイトか型のいずれか、あるいはその両方が自分たちの クレートにローカルである場合にのみ、型にトレイトを実装できるという孤児規則に ついて説明しました。この制約は、タプル構造体で新しい型を作る newtype パターンを使うことで回避できます。(タプル構造体については、第5章の 「タプル構造体で別の型を作る」節で説明しました。) このタプル構造体は1つのフィールドを持ち、トレイトを実装したい型を薄く ラップするものになります。すると、このラッパー型は自分たちのクレートに ローカルになるので、そのラッパーに対してトレイトを実装できます。Newtype は Haskell プログラミング言語に由来する用語です。このパターンを使っても 実行時の性能ペナルティはなく、ラッパー型はコンパイル時に消去されます。
例として、Vec<T> に Display を実装したいとします。しかし、Display
トレイトも Vec<T> 型も自分たちのクレートの外で定義されているため、孤児規則に
よってこれを直接行うことはできません。そこで、Vec<T> のインスタンスを保持する
Wrapper 構造体を作ります。すると、リスト20-24に示すように、Wrapper に
Display を実装し、その Vec<T> の値を利用できます。
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {w}");
}
Display の実装では、内部の Vec<T> にアクセスするために self.0 を
使っています。これは、Wrapper がタプル構造体であり、Vec<T> がその
タプルのインデックス0の要素だからです。これで、Wrapper に対して Display
トレイトの機能を使えるようになります。
この手法を使う欠点は、Wrapper は新しい型であるため、保持している
値のメソッドを持っていないことです。Vec<T> のすべてのメソッドを
Wrapper に直接実装し、それらのメソッドが self.0 に処理を委譲する
ようにする必要があります。そうすれば、Wrapper を Vec<T> とまったく
同じように扱えるようになります。新しい型に内部の型が持つすべての
メソッドを持たせたいのであれば、Wrapper に Deref トレイトを実装して
内部の型を返すようにするのが解決策になります(Deref トレイトの実装に
ついては、第15章の 「スマートポインタを通常の参照のように扱う」
節で説明しました)。内部の型のすべてのメソッドを Wrapper 型に持たせたく
ない場合、たとえば Wrapper 型の振る舞いを制限したい場合には、必要な
メソッドだけを手作業で実装しなければなりません。
この newtype パターンは、トレイトが関係しない場合でも役に立ちます。ここで 話題を変えて、Rust の型システムとやり取りするいくつかの高度な方法を見て いきましょう。
高度な型
高度な型
Rust の型システムには、これまで触れてはきたものの、まだ説明していない機能がいくつかあります。まず、newtype が型としてなぜ有用なのかを見ながら、一般的な newtype について説明します。次に、newtype に似ていますが意味論が少し異なる機能である型エイリアスに進みます。さらに、! 型と動的サイズ付き型についても説明します。
Newtype パターンによる型安全性と抽象化
この節は、前の節 [「Newtype パターンで外部トレイトを実装する」][newtype] を読んでいることを前提としています。newtype パターンは、これまでに説明したもの以外の用途にも役立ちます。たとえば、値が決して取り違えられないことを静的に保証したり、値の単位を示したりできます。リスト 20-16 では、単位を示すために newtype を使う例を見ました。Millimeters 構造体と Meters 構造体は、u32 の値を newtype として包んでいたことを思い出してください。型 Millimeters の引数を取る関数を書いた場合、その関数を誤って Meters 型の値や単なる u32 で呼び出そうとするプログラムはコンパイルできません。
また、newtype パターンを使うことで、型の実装詳細の一部を抽象化することもできます。新しい型は、非公開の内部型の API とは異なる公開 API を公開できます。
newtype は内部実装を隠すこともできます。たとえば、人の ID をその名前に関連付けて保存する HashMap<i32, String> を包む People 型を提供できます。People を使うコードは、People コレクションに名前文字列を追加するメソッドのような、私たちが提供する公開 API とのみやり取りします。そのコードは、内部的に名前へ i32 の ID を割り当てていることを知る必要はありません。newtype パターンは、実装の詳細を隠すためのカプセル化を実現する軽量な方法です。これは第 18 章の [「実装の詳細を隠すカプセル化」][encapsulation-that-hides-implementation-details]
節で説明しました。
型同義語と型エイリアス
Rust では、既存の型に別名を付けるための 型エイリアス を宣言できます。これには type キーワードを使います。たとえば、i32 に対するエイリアス Kilometers を次のように作成できます。
fn main() {
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
println!("x + y = {}", x + y);
}
これで、エイリアス Kilometers は i32 の 同義語 になります。リスト 20-16 で作成した Millimeters 型や Meters 型とは異なり、Kilometers は独立した新しい型ではありません。型 Kilometers を持つ値は、型 i32 の値と同じように扱われます。
fn main() {
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
println!("x + y = {}", x + y);
}
Kilometers と i32 は同じ型であるため、両方の型の値を加算できますし、i32 型の引数を受け取る関数に Kilometers 型の値を渡すこともできます。しかし、この方法では、先に説明した newtype パターンで得られる型チェックの利点は得られません。言い換えると、どこかで Kilometers と i32 の値を取り違えても、コンパイラはエラーを出しません。
型同義語の主な用途は、繰り返しを減らすことです。たとえば、次のような長い型を使うことがあります。
Box<dyn Fn() + Send + 'static>
この長い型を、関数シグネチャや型注釈としてコードのあちこちに書くのは、面倒でミスの原因にもなります。リスト 20-25 のようなコードで満たされたプロジェクトを想像してみてください。
fn main() {
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
// --snip--
}
fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
// --snip--
Box::new(|| ())
}
}
型エイリアスを使うと、繰り返しが減るため、このコードはより扱いやすくなります。リスト 20-26 では、この冗長な型に対して Thunk というエイリアスを導入し、その型のすべての使用箇所を、より短いエイリアス Thunk に置き換えています。
fn main() {
type Thunk = Box<dyn Fn() + Send + 'static>;
let f: Thunk = Box::new(|| println!("hi"));
fn takes_long_type(f: Thunk) {
// --snip--
}
fn returns_long_type() -> Thunk {
// --snip--
Box::new(|| ())
}
}
このコードは、読むのも書くのもずっと簡単になります。また、型エイリアスに意味のある名前を選ぶことで、意図を伝えやすくなります(thunk は「後で評価されるコード」を意味する語なので、保存されるクロージャには適切な名前です)。
型エイリアスは、繰り返しを減らすために Result<T, E> 型と組み合わせて使われることもよくあります。標準ライブラリの std::io モジュールを考えてみましょう。I/O 操作は、処理が失敗する状況に対処するために、しばしば Result<T, E> を返します。このライブラリには、起こりうるすべての I/O エラーを表す std::io::Error 構造体があります。std::io 内の多くの関数は、E が std::io::Error である Result<T, E> を返します。たとえば、Write トレイトには次のような関数があります。
use std::fmt;
use std::io::Error;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
fn flush(&mut self) -> Result<(), Error>;
fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}
Result<..., Error> は何度も繰り返し現れます。そのため、std::io には次の型エイリアス宣言があります。
use std::fmt;
type Result<T> = std::result::Result<T, std::io::Error>;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
この宣言は std::io モジュール内にあるため、完全修飾されたエイリアス std::io::Result<T> を使えます。つまり、E が std::io::Error に埋められた Result<T, E> です。Write トレイトの関数シグネチャは最終的に次のようになります。
use std::fmt;
type Result<T> = std::result::Result<T, std::io::Error>;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
型エイリアスは 2 つの点で役立ちます。コードをより書きやすくし、さらに std::io 全体で一貫したインターフェースを与えてくれます。これはエイリアスなので、単なる別の Result<T, E> にすぎません。つまり、Result<T, E> に対して使えるあらゆるメソッドをそれにも使えますし、? 演算子のような特別な構文も使えます。
決して返らない never 型
Rust には ! という特別な型があり、型理論の用語では値を持たないことから 空型 として知られています。私たちはこれを never 型 と呼ぶことを好みます。これは、関数が決して返らないときに、その戻り値型の位置に置かれるからです。以下に例を示します。
fn bar() -> ! {
// --snip--
panic!();
}
このコードは「関数 bar は決して返らない」と読みます。決して返らない関数は 発散関数 と呼ばれます。型 ! の値は作れないので、bar が返ることはありえません。
しかし、値を作れない型に何の用途があるのでしょうか。数当てゲームの一部であるリスト 2-5 のコードを思い出してください。その一部をここでリスト 20-27 として再掲します。
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
// --snip--
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
そのとき、このコードのいくつかの詳細は飛ばしていました。第6章の[「`match`
制御フロー構文」][the-match-control-flow-construct]<!-- 無視 -->
節で、`match` のアームはすべて同じ型を返さなければならないと説明しました。
したがって、たとえば次のコードは動作しません。
```rust,ignore,does_not_compile
# fn main() {
# let guess = "3";
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "hello",
};
# }
このコードでは、guess の型は整数 かつ 文字列でなければならないことに
なりますが、Rust では guess は 1 つの型しか持てません。では、continue
は何を返すのでしょうか。リスト20-27では、どうして一方のアームから u32
を返し、もう一方のアームを continue で終わらせることが許されたのでしょうか。
予想どおりかもしれませんが、continue は ! の値を持ちます。つまり、Rust
が guess の型を計算するとき、u32 の値を持つ前者のアームと、!
の値を持つ後者のアームの両方を見ます。!
は決して値を持ちえないため、Rust は guess の型を u32 だと判断します。
この振る舞いを形式的に説明すると、型 !
の式は任意の他の型に型強制できる、ということです。この match アームを
continue で終えられるのは、continue
が値を返さないからです。代わりに、制御をループの先頭へ戻すため、Err
の場合には guess に値が代入されることはありません。
never 型は panic! マクロと組み合わせても便利です。この定義で、Option<T>
の値に対して呼び出して値を取り出すか panic する unwrap
関数を思い出してください。
enum Option<T> {
Some(T),
None,
}
use crate::Option::*;
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
このコードでは、リスト20-27の match と同じことが起こっています。Rust
は、val の型が T であり、panic! の型が !
であることを見ているため、match 式全体の結果は T
になります。このコードが動作するのは、panic!
が値を生成しないからです。これはプログラムを終了させます。None
の場合、unwrap から値を返すことはないため、このコードは有効です。
最後に、型 ! を持つ式としてループがあります。
fn main() {
print!("forever ");
loop {
print!("and ever ");
}
}
ここでは、ループは決して終了しないため、式の値は !
です。しかし、break
を含めた場合はそうではありません。break
に到達した時点でループが終了するからです。
動的サイズ型と Sized トレイト
Rust は、その型について、特定の型の値にどれだけの領域を割り当てるべきかといった、 いくつかの詳細を知る必要があります。このため、その型システムの一角には、最初は少しわかりにくいものがあります。それが 動的サイズ型 の概念です。ときに DSTs や サイズ不定型 とも呼ばれるこれらの型によって、サイズが実行時になって初めてわかる値を使うコードを書けます。
本書を通して使ってきた、str
という動的サイズ型の詳細を掘り下げてみましょう。そうです、&str ではなく、
それ単体の str が DST です。ユーザーが入力したテキストを格納するような多く
の場合、文字列の長さは実行時になるまでわかりません。つまり、str
型の変数を作ることも、str 型の引数を取ることもできません。動作しない次の
コードを見てください。
fn main() {
let s1: str = "Hello there!";
let s2: str = "How's it going?";
}
Rust は、ある特定の型のどんな値についても、どれだけのメモリを割り当てるべき
かを知る必要があり、また、ある型のすべての値は同じ量のメモリを使わなければ
なりません。もし Rust がこのコードを書くことを許したなら、この 2 つの str
の値は同じ量の領域を占める必要があります。しかし、これらは長さが異なります。
s1 には 12 バイトの記憶領域が必要で、s2 には 15 バイト必要です。これが、
動的サイズ型を保持する変数を作れない理由です。
では、どうすればよいのでしょうか。この場合、答えはすでに知っています。s1
と s2 の型を str ではなく文字列スライス(&str)にするのです。第4章の
[「文字列スライス」][string-slices]
節で見たように、スライスのデータ構造が保持するのは、スライスの開始位置と長さ
だけです。つまり、&T は T
が配置されているメモリアドレスを保持する単一の値ですが、文字列スライスは
2 つ の値、すなわち str のアドレスとその長さです。そのため、文字列
スライス値のサイズはコンパイル時にわかります。これは usize
の長さの 2 倍です。つまり、参照先の文字列がどれほど長くても、文字列スライス
のサイズは常にわかります。一般に、Rust
で動的サイズ型が使われるのはこのような形です。動的な情報のサイズを保持する
追加のメタデータを持つのです。動的サイズ型の鉄則は、動的サイズ型の値は必ず
何らかのポインタの背後に置かなければならない、ということです。
str はさまざまな種類のポインタと組み合わせられます。たとえば、Box<str> や
Rc<str> です。実は、これは以前にも見たことがあります。ただし、そのときは
別の動的サイズ型でした。トレイトです。すべてのトレイトは、トレイト名を使って
参照できる動的サイズ型です。第18章の[「トレイトオブジェクトを使って共有される
振る舞いを抽象化する」][using-trait-objects-to-abstract-over-shared-behavior]節で、トレイトをトレイトオブジェクトとして使うには、&dyn Trait
や Box<dyn Trait> のようにポインタの背後に置かなければならないと述べました
(Rc<dyn Trait> でも動作します)。
DST を扱うために、Rust
は型のサイズがコンパイル時に既知かどうかを判定する Sized
トレイトを提供しています。このトレイトは、サイズがコンパイル時にわかる
あらゆるものに自動実装されます。さらに、Rust
はすべてのジェネリック関数に Sized
の境界を暗黙に追加します。つまり、次のようなジェネリック関数定義は
fn generic<T>(t: T) {
// --snip--
}
実際には、次のように書いたものとして扱われます。
fn generic<T: Sized>(t: T) {
// --snip--
}
デフォルトでは、ジェネリック関数はコンパイル時にサイズが既知の型に対してのみ 動作します。しかし、次の特別な構文を使うと、この制約を緩めることができます。
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
?Sized というトレイト境界は、「T は Sized
である場合もそうでない場合もある」を意味し、この記法はジェネリック型は
コンパイル時に既知のサイズを持たなければならない、というデフォルトを上書き
します。この意味での ?Trait 構文は Sized
に対してのみ利用でき、他のトレイトには使えません。
また、t パラメータの型を T から &T に変更した点にも注意してください。
型が Sized でない可能性があるため、何らかのポインタの背後で使う必要があり
ます。この場合は参照を選んでいます。
次は、関数とクロージャについて見ていきましょう! [encapsulation-that-hides-implementation-details]: ch18-01-what-is-oo.html#encapsulation-that-hides-implementation-details [string-slices]: ch04-03-slices.html#string-slices [the-match-control-flow-construct]: ch06-02-match.html#the-match-control-flow-construct [using-trait-objects-to-abstract-over-shared-behavior]: ch18-02-trait-objects.html#using-trait-objects-to-abstract-over-shared-behavior [newtype]: ch20-02-advanced-traits.html#implementing-external-traits-with-the-newtype-pattern
高度な関数とクロージャ
高度な関数とクロージャ
この節では、関数とクロージャに関するいくつかの高度な機能、 具体的には関数ポインタやクロージャを返すことについて見ていきます。
関数ポインタ
関数にクロージャを渡す方法についてはすでに説明しましたが、通常の
関数を関数に渡すこともできます! この手法は、新しいクロージャを
定義する代わりに、すでに定義した関数を渡したいときに便利です。関数は
fn 型(小文字の f)に型強制されます。これは
Fn クロージャトレイトと混同しないでください。fn 型は
関数ポインタ と呼ばれます。関数ポインタを使って関数を渡すことで、
ほかの関数への引数として関数を利用できるようになります。
パラメータが関数ポインタであることを指定する構文は
クロージャの場合と似ています。リスト20-28では、引数に 1 を足す
add_one 関数を定義しています。do_twice 関数は 2 つの
パラメータを取ります。1 つは i32 パラメータを受け取り
i32 を返す任意の関数への関数ポインタ、もう 1 つは 1 個の i32
値です。do_twice 関数は f 関数を 2 回呼び出して arg
値を渡し、その 2 回の関数呼び出し結果を足し合わせます。main
関数は add_one と 5 を引数にして do_twice を呼び出します。
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {answer}");
}
このコードは The answer is: 12 を出力します。do_twice の
パラメータ f は、i32 型のパラメータを 1 つ受け取り
i32 を返す fn であると指定しています。そのため、
do_twice の本体の中で f を呼び出せます。main では、
add_one という関数名を do_twice の第 1 引数として渡せます。
クロージャとは異なり、fn はトレイトではなく型です。そのため、
Fn トレイトのいずれかをトレイト境界に持つジェネリック型パラメータを
宣言するのではなく、fn をパラメータ型として直接指定します。
関数ポインタは 3 つすべてのクロージャトレイト(Fn、FnMut、FnOnce)
を実装しているため、クロージャを期待する関数に対する引数として、
いつでも関数ポインタを渡せます。関数でもクロージャでも受け取れるように
するため、関数はジェネリック型といずれかのクロージャトレイトを使って
記述するのが最善です。
とはいえ、クロージャではなく fn のみを受け取りたい場面の 1 つは、
クロージャを持たない外部コードとやり取りするときです。C の関数は
関数を引数として受け取れますが、C にはクロージャがありません。
インラインで定義したクロージャと名前付き関数のどちらも使える例として、
標準ライブラリの Iterator トレイトが提供する map メソッドの利用を
見てみましょう。数値のベクタを文字列のベクタに変換するために map
メソッドを使うには、リスト20-29のようにクロージャを使えます。
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(|i| i.to_string()).collect();
}
あるいは、クロージャの代わりに関数名を map の引数にすることもできます。
リスト20-30は、それがどのようになるかを示しています。
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(ToString::to_string).collect();
}
利用可能な to_string という名前の関数が複数あるため、
「高度なトレイト」 節で説明した完全修飾構文を
使わなければならないことに注意してください。
ここでは、ToString トレイトで定義された to_string 関数を使っています。
標準ライブラリは、Display を実装するあらゆる型に対してこれを
実装しています。
第 6 章の 「enum の値」 節で説明したとおり、 私たちが定義する各 enum バリアントの名前は、初期化関数にもなります。 これらの初期化関数は、クロージャトレイトを実装する関数ポインタとして 使えます。つまり、リスト20-31にあるように、クロージャを受け取る メソッドの引数として初期化関数を指定できます。
fn main() {
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
ここでは、map が呼び出される範囲内の各 u32 値に対して
Status::Value の初期化関数を使うことで、Status::Value
インスタンスを作成しています。このスタイルを好む人もいれば、
クロージャを使うほうを好む人もいます。どちらも同じコードに
コンパイルされるので、自分にとってより明確なスタイルを使ってください。
クロージャを返す
クロージャはトレイトによって表現されるため、クロージャを直接
返すことはできません。通常、トレイトを返したくなる多くの場合では、
その代わりに、そのトレイトを実装する具体型を関数の戻り値として使えます。
しかし、クロージャには返せる具体型がないため、通常はそれをクロージャに
対して行うことはできません。たとえば、クロージャがスコープから何らかの値を
キャプチャする場合、戻り値の型として関数ポインタ fn を使うことは
できません。
その代わりに、通常は第 10 章で学んだ impl Trait 構文を使います。
Fn、FnOnce、FnMut を使って、任意の関数型を返せます。
たとえば、リスト20-32のコードは問題なくコンパイルされます。
#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
}
しかし、第 13 章の 「クロージャ型の推論と注釈」 節で述べたように、各クロージャはそれぞれ固有の別個の型でもあります。 同じシグネチャで実装が異なる複数の関数を扱う必要があるなら、 それらにはトレイトオブジェクトを使う必要があります。 リスト20-33のようなコードを書くとどうなるか、考えてみましょう。
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
move |x| x + init
}
ここには returns_closure と returns_initialized_closure という 2 つの
関数があり、どちらも impl Fn(i32) -> i32 を返します。それらが返す
クロージャは、同じ型として記述されているにもかかわらず、互いに異なることに
注目してください。これをコンパイルしようとすると、Rust はこれが
うまくいかないことを教えてくれます。
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
--> src/main.rs:2:44
|
2 | let handlers = vec![returns_closure(), returns_initialized_closure(123)];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
| ------------------- the found opaque type
|
= note: expected opaque type `impl Fn(i32) -> i32`
found opaque type `impl Fn(i32) -> i32`
= note: distinct uses of `impl Trait` result in different opaque types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions-example` (bin "functions-example") due to 1 previous error
エラーメッセージは、impl Trait を返すたびに、Rust が一意の 不透明型 を
作成することを示しています。これは、Rust が私たちのために構築するものの詳細を
中から見ることができず、また Rust が生成する型を自分で書けるように推測することも
できない型です。したがって、これらの関数は同じトレイト Fn(i32) -> i32 を
実装するクロージャを返しますが、Rust がそれぞれに対して生成する不透明型は
別個のものです。(これは、第17章の
「Pin 型と Unpin トレイト」 で見たように、
異なる async ブロックが同じ出力型を持っていても、Rust がそれぞれに対して異なる
具象型を生成するのと似ています。)この問題に対する解決策は、これまでにも何度か
見てきました。リスト20-34のように、トレイトオブジェクトを使えます。
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x + init)
}
このコードは問題なくコンパイルできます。トレイトオブジェクトの詳細については、 第18章の 「トレイトオブジェクトを使って共有された振る舞いを抽象化する」 の節を参照してください。
次に、マクロを見ていきましょう!
マクロ
マクロ
この本全体を通して println! のようなマクロを使ってきましたが、マクロが
何であり、どのように機能するのかは、まだ十分には掘り下げていません。マクロ という用語は、
Rust における機能の一群、すなわち macro_rules! による宣言的マクロと3種類の
手続き型マクロを指します:
- 構造体や列挙型で使う
derive属性によって追加されるコードを指定する、 カスタム#[derive]マクロ - あらゆる要素に使えるカスタム属性を定義する属性風マクロ
- 関数呼び出しのように見えるものの、引数として指定されたトークンに対して 動作する関数風マクロ
これらそれぞれについて順に見ていきますが、その前に、すでに関数があるのに なぜマクロが必要なのかを見てみましょう。
マクロと関数の違い
基本的に、マクロは別のコードを書くコードを書くための手段であり、これは
メタプログラミング として知られています。付録Cでは、さまざまなトレイトの実装を
生成してくれる derive 属性について説明しました。この本を通して、
println! マクロや vec! マクロも使ってきました。これらのマクロはすべて、
手で書いたコードよりも多くのコードを生成するように 展開 されます。
メタプログラミングは、書いて保守しなければならないコード量を減らすのに役立ちます。 これは関数の役割の1つでもあります。しかし、マクロには関数にはない 追加の力がいくつかあります。
関数シグネチャでは、その関数が持つパラメータの数と型を宣言しなければ
なりません。一方、マクロは可変個のパラメータを受け取れます。たとえば、
println!("hello") は1つの引数で呼び出せますし、
println!("hello {}", name) は2つの引数で呼び出せます。また、マクロは
コンパイラがコードの意味を解釈する前に展開されるため、たとえば与えられた
型に対してトレイトを実装できます。関数ではそれはできません。関数は実行時に
呼び出され、トレイトはコンパイル時に実装されている必要があるからです。
関数の代わりにマクロを実装する欠点は、マクロ定義のほうが関数定義よりも 複雑になることです。なぜなら、Rustコードを書くRustコードを書いているからです。 この間接性のため、マクロ定義は一般に、関数定義よりも読み、理解し、保守する ことが難しくなります。
マクロと関数のもう1つの重要な違いは、マクロはファイル内で呼び出す 前 に 定義するかスコープに導入しておく必要があることです。これに対し、関数はどこで 定義しても、どこからでも呼び出せます。
一般的なメタプログラミングのための宣言的マクロ
Rustで最も広く使われているマクロの形式は、宣言的マクロ です。これらは
「例によるマクロ」「macro_rules! マクロ」、
あるいは単に「マクロ」と呼ばれることもあります。その本質として、宣言的マクロは
Rustの match 式に似たものを書けるようにします。第6章で説明したように、
match 式は、式を取り、その式の結果の値をパターンと比較し、その後で
一致したパターンに関連付けられたコードを実行する制御構造です。マクロもまた、
値を特定のコードに関連付けられたパターンと比較します。この場合、値は文字どおりの
Rustソースコードであり、それがマクロに渡されます。パターンはその
ソースコードの構造と比較され、各パターンに関連付けられたコードは、
一致するとマクロに渡されたコードを置き換えます。これはすべて
コンパイル中に起こります。
マクロを定義するには、macro_rules! 構文を使います。vec! マクロがどのように
定義されているかを見ることで、macro_rules! の使い方を見ていきましょう。第8章では、
特定の値を持つ新しいベクタを作るために vec! マクロをどのように使えるかを
説明しました。たとえば、次のマクロは3つの整数を含む新しいベクタを
作成します:
#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}
vec! マクロを使えば、2つの整数からなるベクタや、5つの文字列スライスからなるベクタを
作ることもできます。同じことを関数で行うことはできません。というのも、値の数や型を
事前に知ることができないからです。
リスト20-35は、vec! マクロの定義を少し単純化したものを示しています。
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
注: 標準ライブラリにおける
vec!マクロの実際の定義には、 正しい量のメモリを事前に割り当てるためのコードが含まれています。そのコードは、 例を単純にするため、ここには含めていない最適化です。
#[macro_export] アノテーションは、このマクロが定義されているクレートが
スコープに導入されるたびに、このマクロも利用可能になるべきことを示します。
このアノテーションがないと、マクロをスコープに導入できません。
次に、macro_rules! と、定義しようとしているマクロの名前を 感嘆符を付けずに
書いて、マクロ定義を始めます。この場合、
vec という名前の後には、マクロ定義の本体を表す波括弧が続きます。
vec! の本体にある構造は、match 式の構造に似ています。
ここには、パターン ( $( $x:expr ),* ) を持つ1つのアームがあり、
その後に => と、このパターンに関連付けられたコードブロックが続きます。もし
パターンが一致すれば、関連付けられたコードブロックが生成されます。これが
このマクロにおける唯一のパターンであることを考えると、有効なマッチ方法は1つ
しかありません。それ以外のパターンはエラーになります。より複雑なマクロには
複数のアームがあります。
マクロ定義で有効なパターン構文は、第19章で扱ったパターン構文とは異なります。 なぜなら、マクロのパターンは値ではなくRustコードの構造に対してマッチするからです。 リスト20-29のパターンの各部分が何を意味するのか見ていきましょう。完全なマクロ パターン構文については、Rust リファレンス を参照してください。
まず、パターン全体を囲むために一組の丸括弧を使います。次に、
ドル記号($)を使って、パターンに一致するRustコードを保持する
マクロシステム内の変数を宣言します。ドル記号により、これが
通常のRust変数ではなくマクロ変数であることが明確になります。次に現れるのは、
括弧の内側でパターンに一致する値を捕捉し、それを置き換えコードで使うための一組の
丸括弧です。$() の中には $x:expr があり、これは任意の
Rust式に一致し、その式に $x という名前を付けます。
$() の後にあるコンマは、$() 内のコードに一致するコードの各要素の間に、
リテラルなコンマ区切り文字が現れなければならないことを示しています。
* は、パターンが * の前にあるものに0回以上一致することを
指定します。
このマクロを vec![1, 2, 3]; で呼び出すと、$x パターンは3つの
式 1、2、3 に対して3回一致します。
では、このアームに対応するコード本体内のパターンを見てみましょう。
$()* 内の temp_vec.push() は、パターン内の $() に一致する各部分に対して、
パターンが一致した回数に応じて 0 回以上生成されます。$x は、一致した各式
に置き換えられます。vec![1, 2, 3]; でこのマクロを呼び出すと、このマクロ
呼び出しを置き換えるために生成されるコードは次のようになります。
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
これで、任意の型の任意個の引数を受け取り、指定された要素を含むベクタを作成 するコードを生成できるマクロを定義できました。
マクロの書き方についてさらに学ぶには、オンラインドキュメントや、 Daniel Keep が始めて Lukas Wirth が引き継いだ 『The Little Book of Rust Macros』 のような他の資料を参照してくだ さい。
属性からコードを生成するためのプロシージャルマクロ
マクロの 2 つ目の形式はプロシージャルマクロで、これは関数により近い形で振る
舞います(そして一種のプロシージャです)。プロシージャルマクロ はコードを
入力として受け取り、そのコードに対して処理を行い、コードを出力として生成し
ます。これは、宣言的マクロが行うようにパターンに対してマッチし、そのコード
を別のコードに置き換えるのとは異なります。プロシージャルマクロには、カスタ
ム derive、属性風、関数風の 3 種類があり、いずれも似たような仕組みで動作
します。
プロシージャルマクロを作成する場合、その定義は特別な crate type を持つ専用
のクレート内になければなりません。これは複雑な技術的理由によるもので、将来
的には解消したいと考えています。リスト 20-36 では、プロシージャルマクロを
定義する方法を示します。ここで some_attribute は、特定の種類のマクロを使
うことを表すプレースホルダーです。
use proc_macro::TokenStream;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
プロシージャルマクロを定義する関数は、入力として TokenStream を受け取り、
出力として TokenStream を生成します。TokenStream 型は Rust に含まれる
proc_macro クレートによって定義されており、トークン列を表します。これが
このマクロの中核です。マクロが処理対象とするソースコードが入力
TokenStream を構成し、マクロが生成するコードが出力 TokenStream です。
この関数には、どの種類のプロシージャルマクロを作成しているのかを指定する属
性も付いています。同じクレート内に複数種類のプロシージャルマクロを持つこと
もできます。
では、さまざまな種類のプロシージャルマクロを見ていきましょう。まずはカスタ
ム derive マクロから始め、その後で他の形式を異なるものにしている小さな違
いを説明します。
カスタム derive マクロ
hello_macro という名前のクレートを作成し、その中で HelloMacro という名
前のトレイトと、hello_macro という名前の 1 つの関連関数を定義してみましょ
う。利用者にそれぞれの型ごとに HelloMacro トレイトを実装してもらうのでは
なく、利用者が自分の型に #[derive(HelloMacro)] と注釈を付けることで、
hello_macro 関数のデフォルト実装を得られるように、プロシージャルマクロを
提供します。そのデフォルト実装は、そのトレイトが定義されている型の名前が
TypeName であるとして、Hello, Macro! My name is TypeName! を出力しま
す。言い換えると、私たちのクレートを使うことで、別のプログラマがリスト
20-37 のようなコードを書けるようにするクレートを書くことになります。
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
完成すると、このコードは Hello, Macro! My name is Pancakes! を出力しま
す。最初のステップは、次のように新しいライブラリクレートを作成することで
す。
$ cargo new hello_macro --lib
次に、リスト 20-38 で HelloMacro トレイトとその関連関数を定義します。
pub trait HelloMacro {
fn hello_macro();
}
これで、トレイトとその関数が用意できました。この時点で、このクレートの利用 者は、リスト 20-39 のようにトレイトを実装して目的の機能を実現できます。
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! My name is Pancakes!");
}
}
fn main() {
Pancakes::hello_macro();
}
しかし、その場合は hello_macro とともに使いたい型ごとに実装ブロックを書
かなければなりません。私たちは、その作業を利用者にさせたくありません。
さらに、トレイトが実装されている型の名前を出力する hello_macro 関数のデ
フォルト実装も、まだ提供できません。Rust にはリフレクション機能がないた
め、実行時に型名を調べることができないのです。コンパイル時にコードを生成す
るためのマクロが必要になります。
次のステップは、プロシージャルマクロを定義することです。これを書いている時
点では、プロシージャルマクロは専用のクレートに置く必要があります。いずれ、
この制約はなくなるかもしれません。クレートとマクロクレートを構成する際の慣
例は次のとおりです。foo という名前のクレートに対して、カスタム derive
プロシージャルマクロのクレートは foo_derive と呼ばれます。hello_macro
プロジェクトの中に、hello_macro_derive という新しいクレートを作成してみ
ましょう。
$ cargo new hello_macro_derive --lib
この 2 つのクレートは密接に関連しているため、hello_macro クレートのディ
レクトリ内にプロシージャルマクロクレートを作成します。hello_macro 内のト
レイト定義を変更した場合は、hello_macro_derive 内のプロシージャルマクロ
実装も変更しなければなりません。この 2 つのクレートは別々に公開する必要が
あり、これらのクレートを使うプログラマは、両方を依存関係として追加し、両方
をスコープに導入する必要があります。別の方法として、hello_macro クレート
が hello_macro_derive を依存関係として使い、プロシージャルマクロのコード
を再エクスポートすることもできます。しかし、ここでのプロジェクト構成にして
おけば、derive 機能を望まないプログラマでも hello_macro を使えるように
なります。
hello_macro_derive クレートをプロシージャルマクロクレートとして宣言する必
要があります。また、すぐにわかるように、syn クレートと quote クレート
の機能も必要になるので、それらを依存関係として追加する必要があります。
hello_macro_derive の Cargo.toml ファイルに次を追加してください。
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
手続きマクロの定義を始めるには、リスト 20-40 のコードを
hello_macro_derive クレートの src/lib.rs ファイルに配置してください。このコードは、
impl_hello_macro 関数の定義を追加するまではコンパイルされないことに注意してください。
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate.
let ast = syn::parse(input).unwrap();
// Build the trait implementation.
impl_hello_macro(&ast)
}
このコードは hello_macro_derive 関数と impl_hello_macro
関数に分けられていることに注目してください。前者は TokenStream のパースを担当し、
後者は構文木の変換を担当します。こうすることで、手続きマクロをより便利に
書けるようになります。外側の関数本体
(この場合は hello_macro_derive)のコードは、目にする、あるいは作成する
ほぼすべての手続きマクロクレートで同じになります。内側の関数本体
(この場合は impl_hello_macro)で指定するコードは、
その手続きマクロの目的に応じて変わります。
ここでは 3 つの新しいクレートを導入しています。proc_macro、syn、
そして quote です。proc_macro クレートは Rust に
付属しているため、Cargo.toml の依存関係に追加する必要はありませんでした。
proc_macro クレートはコンパイラの API であり、これによって自分たちのコードから
Rust コードを読み取り、操作できるようになります。
syn クレートは、文字列としての Rust コードを、操作を行えるデータ構造へと
パースします。quote クレートは、syn のデータ構造を再び Rust コードへと
変換します。これらのクレートのおかげで、扱いたいあらゆる種類の Rust コードを
はるかに簡単にパースできます。Rust コード用の完全なパーサを書くのは、
決して簡単な作業ではありません。
ライブラリの利用者が型に #[derive(HelloMacro)] を指定すると、
hello_macro_derive 関数が呼び出されます。これが可能なのは、ここで
hello_macro_derive 関数に proc_macro_derive をアノテーションし、
トレイト名と一致する HelloMacro という名前を指定しているからです。
これは、ほとんどの手続きマクロが従っている慣例です。
hello_macro_derive 関数はまず、input を TokenStream から、
解釈や操作を行えるデータ構造へ変換します。ここで syn が登場します。
syn の parse 関数は TokenStream を受け取り、パースされた Rust コードを表す
DeriveInput 構造体を返します。リスト 20-41 は、
struct Pancakes; という文字列をパースして得られる DeriveInput
構造体の関連部分を示しています。
DeriveInput {
// --中略--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
この構造体のフィールドから、パースした Rust コードが、ident
(identifier、つまり名前)として Pancakes を持つユニット構造体であることが
分かります。あらゆる種類の Rust コードを記述するためのフィールドがこの構造体には
さらにあります。詳しくは、DeriveInput の syn
ドキュメントを確認してください。
まもなく impl_hello_macro 関数を定義します。ここで、組み込みたい新しい
Rust コードを構築します。しかしその前に、derive
マクロの出力もまた TokenStream であることに注意してください。返された
TokenStream はクレートの利用者が書いたコードに追加されるため、
利用者が自分のクレートをコンパイルすると、変更後の TokenStream
の中で私たちが提供した追加機能を得ることになります。
ここで unwrap を呼び出して、syn::parse 関数の呼び出しに失敗した場合に
hello_macro_derive 関数がパニックするようにしていることに気づいたかもしれません。
手続きマクロ API に従うには、proc_macro_derive 関数は Result ではなく
TokenStream を返さなければならないため、エラー時に手続きマクロが
パニックすることは必要です。この例では unwrap を使って単純化していますが、
実運用のコードでは、panic! や expect
を使って、何がうまくいかなかったのかについて、より具体的なエラーメッセージを
提供すべきです。
これで、アノテーションが付いた Rust コードを TokenStream から
DeriveInput インスタンスへ変換するコードができました。次は、
リスト 20-42 に示すように、アノテーションが付いた型に対して HelloMacro
トレイトを実装するコードを生成しましょう。
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let generated = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
generated.into()
}
ast.ident を使うことで、アノテーションが付いた型の名前
(識別子)を含む Ident 構造体インスタンスを取得します。リスト 20-41 の
構造体は、リスト 20-37 のコードに対して impl_hello_macro
関数を実行すると、取得される ident の ident フィールドの値が
"Pancakes" になることを示しています。したがって、リスト 20-42 の
name 変数には Ident 構造体インスタンスが入り、それを表示すると
"Pancakes"、つまりリスト 20-37 の構造体の名前になります。
quote! マクロを使うと、返したい Rust コードを定義できます。コンパイラが期待するのは
quote! マクロの実行結果そのものとは少し異なるものなので、それを
TokenStream に変換する必要があります。これには into
メソッドを呼び出します。これにより、この中間表現が消費され、必要な
TokenStream 型の値が返されます。
quote! マクロは非常に便利なテンプレート機構も提供しています。#name
と書くと、quote! はそれを変数 name の値で置き換えます。通常のマクロの
動作と似た繰り返しも行えます。詳しい導入については、quote
クレートのドキュメントを参照してください。
私たちは、利用者がアノテーションを付けた型に対して HelloMacro
トレイトの実装を生成したいので、#name
を使ってその型を取得できます。トレイト実装には hello_macro
という 1 つの関数があり、その本体には私たちが提供したい機能、すなわち
Hello, Macro! My name is を出力し、その後にアノテーションが付いた型の
名前を出力する処理が含まれます。
ここで使っている stringify! マクロは Rust に組み込まれています。これは
1 + 2 のような Rust 式を受け取り、コンパイル時にその式を "1 + 2" のような
文字列リテラルへ変換します。これは format! や println!
とは異なります。これらは式を評価してから、その結果を String
に変換するマクロです。#name
入力が、そのまま文字どおり出力したい式である可能性があるため、ここでは
stringify! を使います。また、stringify! を使うことで、#name
をコンパイル時に文字列リテラルへ変換するため、割り当てを 1 回節約できます。
この時点で、hello_macro と
hello_macro_derive の両方で cargo build が正常に完了するはずです。これらのクレートをリスト
20-37 のコードに組み込み、手続きマクロが実際に動く様子を見てみましょう! cargo new pancakes を使って、
projects ディレクトリに新しいバイナリプロジェクトを作成してください。pancakes
クレートの Cargo.toml に、依存関係として hello_macro と
hello_macro_derive を追加する必要があります。自分の hello_macro と
hello_macro_derive のバージョンを crates.io に公開するのであれば、それらは通常の依存関係になります。そうでない場合は、次のように path
依存関係として指定できます:
[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
リスト 20-37 のコードを src/main.rs に入れて、cargo run を実行してください。すると
Hello, Macro! My name is Pancakes! と表示されるはずです。手続きマクロによる
HelloMacro トレイトの実装が含まれるので、pancakes
クレート側でそれを実装する必要はありません。#[derive(HelloMacro)] がトレイト実装を追加したのです。
次に、他の種類の手続きマクロがカスタム derive マクロとどのように異なるかを見ていきましょう。
属性風マクロ
属性風マクロはカスタム derive マクロに似ていますが、derive
属性用のコードを生成する代わりに、新しい属性を作成できます。さらに、こちらの方が柔軟です。derive
は構造体と列挙型にしか使えませんが、属性は関数のような他の項目にも適用できます。以下は属性風マクロの使用例です。Web
アプリケーションフレームワークを使うときに、関数に注釈を付ける route
という属性があるとしましょう:
#[route(GET, "/")]
fn index() {
この #[route]
属性は、そのフレームワークによって手続きマクロとして定義されます。マクロ定義関数のシグネチャは次のようになります:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
ここでは、型 TokenStream の引数が 2 つあります。1 つ目は属性の内容、つまり
GET, "/" の部分です。2 つ目は、その属性が付けられている項目の本体で、この場合は
fn index() {} と関数本体の残りです。
それ以外については、属性風マクロはカスタム derive
マクロと同じように動作します。proc-macro
クレート型を持つクレートを作成し、必要なコードを生成する関数を実装します!
関数風マクロ
関数風マクロは、関数呼び出しのように見えるマクロを定義します。macro_rules!
マクロと同様に、これらは関数よりも柔軟で、たとえば引数を不定個受け取れます。しかし、macro_rules!
マクロは、前の「一般的なメタプログラミングのための宣言的マクロ」節で説明した、マッチに似た構文でしか定義できません。
関数風マクロは TokenStream 引数を取り、その定義では他の 2
種類の手続きマクロと同様に Rust コードを使ってその TokenStream
を操作します。関数風マクロの一例として、次のように呼び出せる sql!
マクロがあります:
let sql = sql!(SELECT * FROM posts WHERE id=1);
このマクロは内部の SQL 文を解析し、それが構文的に正しいことを検査します。これは
macro_rules! マクロでできる処理よりも、はるかに複雑です。sql!
マクロは次のように定義されます:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
この定義はカスタム derive
マクロのシグネチャと似ています。括弧の中にあるトークンを受け取り、生成したいコードを返します。
まとめ
ふう!これで、頻繁には使わないかもしれないものの、ごく特定の状況で利用できる Rust の機能がいくつか道具箱に加わりました。ここでは複雑なトピックをいくつも導入しましたが、それはエラーメッセージの提案や他の人のコードの中でそれらに出会ったときに、これらの概念や構文を見分けられるようにするためです。解決策へ導くための参考として、この章を活用してください。
次は、この本を通して議論してきたことをすべて実践に移し、もう 1 つプロジェクトに取り組みます!
最終プロジェクト:マルチスレッド Web サーバーの構築
長い旅路でしたが、ついに本書の終わりにたどり着きました。この 章では、最後の数章で扱った概念のいくつかを実演するとともに、 以前に学んだ内容も振り返るために、もう 1 つのプロジェクトを一緒に作ります。
最終プロジェクトでは、「Hello!」と表示し、Web ブラウザーで 図 21-1 のように見える Web サーバーを作ります。
Web サーバーを構築するための計画は次のとおりです。
- TCP と HTTP について少し学ぶ。
- ソケットで TCP 接続を待ち受ける。
- 少数の HTTP リクエストをパースする。
- 適切な HTTP レスポンスを作成する。
- スレッドプールでサーバーのスループットを改善する。
図 21-1:最後に一緒に取り組むプロジェクト
始める前に、2 つの点に触れておくべきでしょう。まず、これから使う方法は、 Rust で Web サーバーを構築する最良の方法ではありません。コミュニティの メンバーは、私たちがこれから作るものよりも、より完全な Web サーバーや スレッドプールの実装を提供する、本番運用可能なクレートを crates.io で多数公開しています。しかし、この章の目的は、 楽をすることではなく、みなさんの学習を助けることにあります。Rust は システムプログラミング言語なので、扱いたい抽象化のレベルを選べますし、 他の言語では不可能または現実的でないほど低いレベルまで踏み込むこともできます。
次に、ここでは async と await は使いません。非同期ランタイムの構築まで 加えてしまうと、スレッドプールの構築だけでも十分に大きな挑戦だからです! ただし、この章で目にするいくつかの同じ問題に対して、async と await が どのように適用できるかについては触れます。最終的には、第 17 章で述べた とおり、多くの非同期ランタイムは作業を管理するためにスレッドプールを 使っています。
したがって、将来使うかもしれないクレートの背後にある一般的な考え方や テクニックを学べるように、基本的な HTTP サーバーとスレッドプールを手作業で 実装していきます。
シングルスレッドのWebサーバーを構築する
シングルスレッドのWebサーバーを構築する
まずは、シングルスレッドのWebサーバーを動かすところから始めましょう。始める前に、 Webサーバーを構築する際に関係するプロトコルの概要を簡単に見ておきます。これらの プロトコルの詳細はこの本の範囲を超えていますが、概要を短く確認するだけでも必要な 情報は得られます。
Webサーバーに関係する主要なプロトコルは、ハイパーテキスト転送プロトコル (HTTP) と 伝送制御プロトコル (TCP) の2つです。どちらのプロトコルも リクエストレスポンス 型のプロトコルであり、つまり クライアント がリクエストを 開始し、サーバー がそのリクエストを待ち受けてクライアントにレスポンスを返します。 それらのリクエストとレスポンスの内容は、これらのプロトコルによって定義されています。
TCP は、情報があるサーバーから別のサーバーへどのように届くかの詳細を記述する、 より低レベルのプロトコルですが、その情報が何であるかは規定しません。HTTP は TCP の 上に構築され、リクエストとレスポンスの内容を定義します。技術的には HTTP をほかの プロトコルと組み合わせて使うことも可能ですが、大多数のケースでは HTTP は TCP 上で データを送信します。ここでは TCP と HTTP のリクエストおよびレスポンスの生の バイト列を扱います。
TCP接続を待ち受ける
Webサーバーは TCP 接続を待ち受ける必要があるため、まずはそこから取り組みます。
標準ライブラリには、これを行うための std::net モジュールがあります。いつもの
やり方で新しいプロジェクトを作成しましょう。
$ cargo new hello
Created binary (application) `hello` project
$ cd hello
では、まずはリスト 21-1 のコードを src/main.rs に入力してください。このコードは、
ローカルアドレス 127.0.0.1:7878 で到着する TCP ストリームを待ち受けます。到着した
ストリームを受け取ると、Connection established! と表示します。
use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
println!("Connection established!");
}
}
TcpListener を使うと、アドレス 127.0.0.1:7878 で TCP 接続を待ち受けることが
できます。このアドレスでは、コロンの前の部分はあなたのコンピュータを表す IP
アドレスです(これはどのコンピュータでも同じであり、著者のコンピュータを特に
表しているわけではありません)。そして 7878 がポートです。このポートを選んだ
理由は2つあります。HTTP は通常このポートでは受け付けられないため、あなたの
マシン上で動いているほかの Web サーバーと競合する可能性が低いこと、そして
7878 は電話のキーで rust を入力したものになっていることです。
この場合の bind 関数は new 関数と同じように動作し、新しい TcpListener
インスタンスを返します。この関数が bind と呼ばれているのは、ネットワークの
世界では、待ち受けるためにポートに接続することを「ポートにバインドする」と
呼ぶからです。
bind 関数は Result<T, E> を返します。これは、バインドに失敗する可能性がある
ことを示しています。たとえば、同じプログラムを2つ起動してしまうと、2つの
プログラムが同じポートを待ち受けようとするため失敗します。今回は学習用の
基本的なサーバーを書いているだけなので、この種のエラー処理は気にしません。
代わりに、エラーが起きたらプログラムを停止するために unwrap を使います。
TcpListener の incoming メソッドはイテレータを返し、そこからストリームの列
(より正確には TcpStream 型のストリーム)を取得できます。1つの ストリーム は、
クライアントとサーバーの間の開いている接続を表します。接続 とは、クライアントが
サーバーに接続し、サーバーがレスポンスを生成し、サーバーが接続を閉じるまでの、
リクエストとレスポンスの一連の完全な処理全体を指す名前です。そのため、クライアントが
送ってきた内容を確認するには TcpStream から読み取り、その後クライアントへデータを
送り返すためにレスポンスをストリームへ書き込みます。全体として、この for ループは
各接続を順番に処理し、扱うべきストリームの列を生成します。
今のところ、ストリームの処理では、そのストリームにエラーがあれば unwrap を
呼び出してプログラムを終了させるだけです。エラーがなければ、プログラムは
メッセージを表示します。成功する場合の処理については、次のリストで機能を追加します。
クライアントがサーバーに接続したときに incoming メソッドからエラーを受け取ることが
ある理由は、実際には接続そのものを反復しているのではなく、接続試行 を反復して
いるからです。接続が成功しない理由はいくつもあり、その多くはオペレーティング
システム固有です。たとえば、多くのオペレーティングシステムには同時に開ける接続数の
上限があります。その数を超える新しい接続試行は、既存の開いている接続の一部が
閉じられるまでエラーになります。
このコードを実行してみましょう。ターミナルで cargo run を実行し、その後 Web
ブラウザで 127.0.0.1:7878 を開いてください。サーバーは現在まだ何のデータも返して
いないため、ブラウザには「Connection reset」のようなエラーメッセージが表示される
はずです。しかし、ターミナルを見ると、ブラウザがサーバーに接続したときに表示された
いくつかのメッセージが見えるはずです。
Running `target/debug/hello`
Connection established!
Connection established!
Connection established!
1回のブラウザのリクエストに対して複数のメッセージが表示されることがあります。その 理由としては、ブラウザがページ本体のリクエストに加えて、ブラウザのタブに表示される favicon.ico アイコンのような別のリソースも要求している可能性があります。
また、サーバーが何のデータも返していないために、ブラウザがサーバーへの接続を複数回
試みている可能性もあります。stream がスコープを抜け、ループの終わりで drop
されると、drop 実装の一部として接続は閉じられます。問題が一時的なものである
可能性があるため、ブラウザは閉じられた接続に対して再試行することがあります。
ブラウザはまた、後で 実際に リクエストを送ることになったときにそれらをより速く 処理できるよう、まだ何のリクエストも送らずにサーバーへの接続を複数開くことも あります。これが起こると、その接続上にリクエストがあるかどうかに関係なく、サーバーは それぞれの接続を認識します。たとえば、Chrome ベースのブラウザの多くのバージョンは このような動作をします。この最適化は、プライベートブラウジングモードを使うか、 別のブラウザを使うことで無効にできます。
重要なのは、TCP 接続へのハンドルを正常に取得できたことです!
あるバージョンのコードの実行が終わったら、ctrl-C を押して
プログラムを停止するのを忘れないでください。その後、コードを変更するたびに
cargo run コマンドを実行してプログラムを再起動し、常に最新のコードを実行して
いることを確認してください。
リクエストを読み取る
ブラウザからのリクエストを読み取る機能を実装しましょう! まず接続を取得することと、
その接続に対して何らかの処理を行うことの関心を分離するために、接続を処理する新しい
関数を作成します。この新しい handle_connection 関数では、TCP ストリームから
データを読み取り、それを表示して、ブラウザから送られてくるデータを確認できるように
します。コードをリスト 21-2 のように変更してください。
```rust,no_run
use std::{
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
println!("Request: {http_request:#?}");
}
std::io::BufReader と std::io::prelude をスコープに導入し、ストリームの読み書きを可能にするトレイトと型にアクセスできるようにします。main 関数の for ループでは、接続を確立したことを示すメッセージを出力する代わりに、新しい handle_connection 関数を呼び出し、stream をそれに渡します。
handle_connection 関数では、stream への参照をラップする新しい BufReader インスタンスを作成します。BufReader は、std::io::Read トレイトのメソッド呼び出しを私たちの代わりに管理することで、バッファリングを追加します。
http_request という名前の変数を作成して、ブラウザがサーバーに送信するリクエストの各行を集めます。Vec<_> 型注釈を追加することで、これらの行をベクタに収集したいことを示しています。
BufReader は std::io::BufRead トレイトを実装しており、このトレイトは lines メソッドを提供します。lines メソッドは、データのストリーム内で改行バイトを見つけるたびに分割し、Result<String, std::io::Error> のイテレータを返します。各 String を取得するために、各 Result に対して map と unwrap を行います。データが有効な UTF-8 でない場合や、ストリームからの読み取り中に問題が発生した場合、Result はエラーになる可能性があります。繰り返しになりますが、本番用プログラムではこれらのエラーをもっと適切に処理すべきですが、ここでは単純化のため、エラーの場合はプログラムを停止することにしています。
ブラウザは HTTP リクエストの終わりを、改行文字を 2 つ連続で送ることで示します。そのため、ストリームから 1 つのリクエストを取得するには、空文字列である行に達するまで行を取り出します。行をベクタに収集したら、見やすいデバッグフォーマットでそれらを出力し、Web ブラウザがサーバーに送っている命令を確認できるようにします。
このコードを試してみましょう! プログラムを起動し、再び Web ブラウザからリクエストを送ってください。ブラウザには引き続きエラーページが表示されますが、ターミナルでのプログラムの出力は次のようになります。
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/hello`
Request: [
"GET / HTTP/1.1",
"Host: 127.0.0.1:7878",
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip, deflate, br",
"DNT: 1",
"Connection: keep-alive",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: none",
"Sec-Fetch-User: ?1",
"Cache-Control: max-age=0",
]
使用するブラウザによっては、少し異なる出力になるかもしれません。リクエストデータを出力するようになったので、リクエストの最初の行で GET の後にあるパスを見ることで、1 回のブラウザリクエストから複数の接続が発生する理由がわかります。繰り返される接続がすべて / を要求しているなら、ブラウザはプログラムからレスポンスを受け取れていないため、繰り返し / を取得しようとしているのだとわかります。
このリクエストデータを分解して、ブラウザがプログラムに何を要求しているのかを理解しましょう。
HTTP リクエストをさらに詳しく見る
HTTP はテキストベースのプロトコルであり、リクエストは次の形式を取ります。
Method Request-URI HTTP-Version CRLF
headers CRLF
message-body
最初の行は request line で、クライアントが何を要求しているかに関する情報を保持しています。リクエスト行の最初の部分は、GET や POST など、使用されているメソッドを示し、これはクライアントがどのようにこのリクエストを行っているかを表します。私たちのクライアントは GET リクエストを使用しており、これは情報を要求していることを意味します。
リクエスト行の次の部分は / で、これはクライアントが要求している Uniform Resource Identifier (URI) を示しています。URI は Uniform Resource Locator (URL) とほとんど同じですが、完全には同じではありません。URI と URL の違いはこの章の目的には重要ではありませんが、HTTP 仕様では URI という用語が使われているので、ここでは頭の中で URI を URL に置き換えて考えてかまいません。
最後の部分はクライアントが使用している HTTP バージョンで、その後リクエスト行は CRLF シーケンスで終わります。(CRLF は carriage return と line feed の略で、これらはタイプライター時代の用語です!)CRLF シーケンスは \r\n と書くこともでき、ここで \r はキャリッジリターン、\n はラインフィードです。CRLF sequence は、リクエスト行と残りのリクエストデータを区切ります。CRLF が出力されるとき、\r\n として表示されるのではなく、新しい行が始まることに注目してください。
ここまでにプログラムを実行して受け取ったリクエスト行のデータを見ると、GET がメソッド、/ がリクエスト URI、そして HTTP/1.1 がバージョンであることがわかります。
リクエスト行の後で、Host: から始まる残りの行はヘッダーです。GET リクエストにはボディがありません。
別のブラウザからリクエストを送ったり、127.0.0.1:7878/test のような別のアドレスを要求したりして、リクエストデータがどのように変化するかを見てみてください。
ブラウザが何を求めているのかわかったので、今度は何らかのデータを送り返しましょう!
レスポンスを書き込む
クライアントのリクエストに応じてデータを送信する処理を実装します。レスポンスは次の形式を持ちます。
HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body
最初の行は status line で、レスポンスで使用される HTTP バージョン、リクエストの結果を要約する数値のステータスコード、そしてそのステータスコードのテキストによる説明を提供する reason phrase を含みます。CRLF シーケンスの後には、任意のヘッダー、さらに別の CRLF シーケンス、そしてレスポンスのボディが続きます。
以下は HTTP バージョン 1.1 を使用し、ステータスコード 200、OK という reason phrase、ヘッダーなし、ボディなしのレスポンスの例です。
HTTP/1.1 200 OK\r\n\r\n
ステータスコード 200 は標準的な成功レスポンスです。このテキストは、ごく小さな成功 HTTP レスポンスです。これを、成功したリクエストへのレスポンスとしてストリームに書き込んでみましょう! handle_connection 関数から、リクエストデータを出力していた println! を削除し、Listing 21-3 のコードに置き換えてください。
use std::{
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
let response = "HTTP/1.1 200 OK\r\n\r\n";
stream.write_all(response.as_bytes()).unwrap();
}
これらの変更を加えたら、コードを実行してリクエストを送ってみましょう。 もうターミナルにデータを出力していないため、Cargo からの出力以外は何も 表示されません。Web ブラウザで 127.0.0.1:7878 を開くと、エラーではなく 空白のページが表示されるはずです。これで、HTTP リクエストを受け取り、 レスポンスを送信する処理を手作業で実装したことになります。
実際の HTML を返す
空白のページ以上のものを返す機能を実装しましょう。プロジェクト ディレクトリのルートに、新しいファイル hello.html を作成してください。 hello.html は src ディレクトリの中ではありません。HTML の内容は好きに 入力してかまいません。リスト 21-4 にその一例を示します。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Hello!</h1>
<p>Hi from Rust</p>
</body>
</html>
これは、見出しといくつかのテキストを含む最小限の HTML5 ドキュメントです。
リクエストを受け取ったときにサーバーからこれを返すために、リスト 21-5 に
示すように handle_connection を修正して HTML ファイルを読み込み、
それを本文としてレスポンスに追加して送信します。
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
// --snip--
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
標準ライブラリのファイルシステムモジュールをスコープに取り込むために、
use 文に fs を追加しました。ファイルの内容を文字列として読み込む
コードは見覚えがあるはずです。リスト 12-4 の I/O プロジェクトで
ファイルの内容を読んだときに使いました。
次に、format! を使ってファイルの内容を成功レスポンスの本文として追加
します。有効な HTTP レスポンスにするために、Content-Length ヘッダーを
追加します。これはレスポンス本文のサイズ、この場合は hello.html の
サイズに設定されます。
このコードを cargo run で実行し、ブラウザで 127.0.0.1:7878 を
開いてください。HTML がレンダリングされるはずです。
現在は、http_request 内のリクエストデータを無視して、HTML ファイルの
内容を無条件に送り返しています。つまり、ブラウザで
127.0.0.1:7878/something-else をリクエストしても、同じ HTML
レスポンスが返ってきます。現時点では、私たちのサーバーは非常に限定的で、
ほとんどの Web サーバーが行うことをしていません。リクエストに応じて
レスポンスをカスタマイズし、/ への正しい形式のリクエストに対してのみ
HTML ファイルを返すようにしたいところです。
リクエストを検証して条件に応じて応答する
現時点では、クライアントが何を要求しても、この Web サーバーはファイル内の
HTML を返します。HTML ファイルを返す前に、ブラウザが / を要求している
ことを確認し、それ以外を要求した場合にはエラーを返す機能を追加しましょう。
そのためには、リスト 21-6 に示すように handle_connection を修正する
必要があります。この新しいコードは、受信したリクエストの内容を、/ への
リクエストがどのようなものかという既知の内容と照合し、リクエストを
異なる方法で扱うための if ブロックと else ブロックを追加します。
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
// --snip--
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
if request_line == "GET / HTTP/1.1" {
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
} else {
// some other request
}
}
HTTP リクエストの最初の行だけを見るので、リクエスト全体をベクターに
読み込む代わりに、イテレーターから最初の要素を取得するために next を
呼び出しています。最初の unwrap は Option を処理し、イテレーターに
要素がない場合はプログラムを停止します。2 つ目の unwrap は Result
を処理し、リスト 21-2 で追加した map 内にあった unwrap と同じ効果を
持ちます。
次に、request_line が / パスへの GET リクエストのリクエスト行と等しい
かどうかを確認します。等しければ、if ブロックが HTML ファイルの内容を
返します。
request_line が / パスへの GET リクエストと一致しない場合は、ほかの
何らかのリクエストを受信したことを意味します。すべてのほかのリクエストに
応答するためのコードは、このあと else ブロックに追加します。
このコードを今実行して、127.0.0.1:7878 をリクエストしてください。 hello.html の HTML が返されるはずです。127.0.0.1:7878/something-else のような別のリクエストを行うと、リスト 21-1 とリスト 21-2 のコードを 実行したときに見たような接続エラーが発生します。
では次に、リスト 21-7 のコードを else ブロックに追加して、リクエスト
されたコンテンツが見つからなかったことを示す 404 のステータスコードを
持つレスポンスを返しましょう。また、エンドユーザーに対するレスポンス内容を
ブラウザに表示するための HTML も返します。
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
if request_line == "GET / HTTP/1.1" {
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
// --snip--
} else {
let status_line = "HTTP/1.1 404 NOT FOUND";
let contents = fs::read_to_string("404.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
}
}
ここで、レスポンスにはステータスコード 404 と理由句 NOT FOUND を含む
ステータス行があります。レスポンスの本文は、ファイル 404.html にある
HTML になります。次に、エラーページ用として hello.html の隣に
404.html ファイルを作成する必要があります。ここでも、HTML の内容は
好きにしてかまいませんし、リスト 21-8 の HTML の例を使ってもかまいません。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Oops!</h1>
<p>Sorry, I don't know what you're asking for.</p>
</body>
</html>
これらの変更を加えたら、サーバーを再度実行してください。 127.0.0.1:7878 をリクエストすると hello.html の内容が返され、 127.0.0.1:7878/foo のようなそれ以外のリクエストでは 404.html の エラー HTML が返されるはずです。
リファクタリング
現時点では、if ブロックと else ブロックには多くの重複があります。
どちらもファイルを読み取り、そのファイルの内容をストリームに書き込んで
います。違うのは、ステータス行とファイル名だけです。これらの違いを、
ステータス行とファイル名の値を変数に代入する別々の if 行と else 行に
切り出すことで、コードをより簡潔にしましょう。そうすれば、その後の
コードでは無条件にそれらの変数を使ってファイルを読み込み、レスポンスを
書き込めます。リスト 21-9 は、大きな if ブロックと else ブロックを
置き換えた後のコードを示しています。
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
// --snip--
fn handle_connection(mut stream: TcpStream) {
// --snip--
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
("HTTP/1.1 200 OK", "hello.html")
} else {
("HTTP/1.1 404 NOT FOUND", "404.html")
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
いまや if と else のブロックは、ステータス行とファイル名に対応する適切な値をタプルとして返すだけになりました。その後、第19章で説明したように、let 文のパターンを使って分解し、この2つの値を status_line と filename に代入します。
以前は重複していたコードは、いまでは if と else のブロックの外にあり、status_line と filename 変数を使っています。これにより、2つのケースの違いが見やすくなり、ファイルの読み込みやレスポンスの書き込みの動作を変更したい場合にも、コードを更新する場所は1か所だけになります。リスト21-9のコードの振る舞いは、リスト21-7のものと同じです。
すばらしい! これで、約40行のRustコードで、ある1つのリクエストにはコンテンツのページを返し、それ以外のすべてのリクエストには404レスポンスを返す、シンプルなWebサーバーができました。
現在、私たちのサーバーは単一スレッドで動作しているため、一度に処理できるリクエストは1つだけです。いくつかの遅いリクエストをシミュレートして、これがどのように問題になりうるのかを見てみましょう。その後、サーバーが複数のリクエストを同時に処理できるように修正します。
シングルスレッドからマルチスレッドサーバーへ
シングルスレッドサーバーからマルチスレッドサーバーへ
現時点では、このサーバーは各リクエストを順番に処理するため、最初の接続の処理が完了するまで2つ目の接続は処理されません。サーバーが受け取るリクエストが増えれば増えるほど、この逐次実行はますます最適ではなくなります。サーバーが処理に長い時間のかかるリクエストを受け取ると、たとえ新しいリクエストをすばやく処理できるとしても、その長いリクエストの処理が終わるまで後続のリクエストは待たなければなりません。これを修正する必要がありますが、その前にまず実際にその問題が起こる様子を見てみましょう。
遅いリクエストをシミュレートする
処理に時間のかかるリクエストが、現在のサーバー実装に対するほかのリクエストにどのような影響を与えるかを見てみましょう。リスト21-10では、/sleep へのリクエスト処理を、応答前にサーバーが5秒間スリープするようにして、遅いレスポンスをシミュレートしています。
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
// --snip--
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
// --snip--
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
// --snip--
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
ケースが3つになったので、if から match に切り替えました。文字列リテラル値に対してパターンマッチするには、request_line のスライスを明示的にマッチさせる必要があります。match は、等価比較メソッドが行うような自動の参照および参照外しを行わないためです。
最初のアームは、リスト21-9の if ブロックと同じです。2番目のアームは、/sleep へのリクエストにマッチします。そのリクエストを受け取ると、サーバーは正常なHTMLページを返す前に5秒間スリープします。3番目のアームは、リスト21-9の else ブロックと同じです。
このサーバーがどれほど原始的かがわかるでしょう。実際のライブラリであれば、複数のリクエストの認識をこれほど冗長でない方法で処理するはずです!
cargo run を使ってサーバーを起動してください。次に、2つのブラウザウィンドウを開きます。1つは http://127.0.0.1:7878、もう1つは http://127.0.0.1:7878/sleep です。前と同じように / のURIを数回入力すると、すばやく応答することがわかります。しかし、/sleep を入力してから / を読み込むと、/ は sleep が5秒間すべてスリープし終えるまで待ってから読み込まれることがわかります。
遅いリクエストの後ろにリクエストが滞留しないようにするために使える手法は複数あります。第17章で行ったように async を使う方法もその1つです。ここで実装するのは、スレッドプールです。
スレッドプールでスループットを向上させる
スレッドプール とは、タスクを処理する準備ができて待機している、生成済みスレッドの集まりです。プログラムが新しいタスクを受け取ると、そのタスクをプール内のいずれかのスレッドに割り当て、そのスレッドがタスクを処理します。プール内の残りのスレッドは、最初のスレッドが処理している間に到着するほかのタスクを処理できる状態のままです。最初のスレッドがタスクの処理を終えると、そのスレッドは待機中のスレッドのプールに戻され、新しいタスクを処理する準備が整います。スレッドプールを使うと、接続を並行して処理できるため、サーバーのスループットが向上します。
DoS攻撃から身を守るために、プール内のスレッド数は少ない数に制限します。もし受け取った各リクエストごとにプログラムが新しいスレッドを作成するようにしていたら、誰かがサーバーに1000万件のリクエストを送ることで、サーバーのリソースをすべて使い果たし、リクエスト処理を停止状態に追い込むという大混乱を引き起こせてしまうからです。
そこで、無制限にスレッドを生成するのではなく、固定数のスレッドをプール内で待機させます。到着したリクエストは、処理のためにプールへ送られます。プールは到着したリクエストのキューを維持します。プール内の各スレッドは、このキューから1件のリクエストを取り出して処理し、その後キューに別のリクエストを要求します。この設計により、最大で N 件のリクエストを同時に処理できます。ここで N はスレッド数です。各スレッドが長時間実行されるリクエストに応答している場合、後続のリクエストは依然としてキューに滞留する可能性がありますが、その状態に達するまでに処理できる長時間実行リクエストの数は増えています。
この手法は、Webサーバーのスループットを向上させる多くの方法のうちの1つにすぎません。ほかに検討できる選択肢としては、fork/joinモデル、シングルスレッドの async I/O モデル、マルチスレッドの async I/O モデルなどがあります。このトピックに興味があれば、ほかの解決策についてさらに調べて、実装してみることもできます。Rustのような低レベル言語であれば、これらの選択肢はすべて実現可能です。
スレッドプールの実装を始める前に、プールの使い方がどのように見えるべきかを考えてみましょう。コードを設計しようとしているときは、最初にクライアントインターフェースを書くことが、設計の指針になることがあります。コードのAPIを、自分が呼び出したい形になるように書き、その構造の中に機能を実装していくのです。まず機能を実装してから公開APIを設計するのではありません。
第12章のプロジェクトでテスト駆動開発を使ったのと同じように、ここではコンパイラ駆動開発を使います。まず使いたい関数を呼び出すコードを書き、その後コンパイラのエラーを見て、コードを動作させるために次に何を変更すべきかを判断します。ただしその前に、出発点として使わないことにした手法を見ておきましょう。
リクエストごとにスレッドを生成する
まず、接続ごとに新しいスレッドを作成するとしたら、コードがどのようになるかを見てみましょう。前に述べたように、スレッド数が無制限に増える可能性があるという問題があるため、これは最終的な計画ではありません。しかし、まず動作するマルチスレッドサーバーを作るための出発点にはなります。その後で改善としてスレッドプールを追加すれば、2つの解決策を対比するのも簡単になります。
リスト21-11は、for ループ内で各ストリームを処理するために新しいスレッドを生成するよう、main に加える変更を示しています。
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
thread::spawn(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
第16章で学んだように、thread::spawn は新しいスレッドを作成し、その後その新しいスレッド内でクロージャ内のコードを実行します。このコードを実行して、ブラウザで /sleep を読み込み、その後さらに2つのブラウザタブで / を読み込むと、確かに / へのリクエストは /sleep の完了を待たなくてよいことがわかります。しかし、前に述べたように、制限なしに新しいスレッドを作り続けることになるため、これはいずれシステムを圧迫します。
また、第17章で、まさにこの種の状況こそ async と await が真価を発揮する場面だと学んだことも思い出してください。スレッドプールを構築しながら、async では何が異なり、何が同じように見えるかを考えてみてください。
有限個のスレッドを作成する
スレッドプールが、同様で親しみやすい方法で動作するようにしたいと考えています。そうすれば、スレッドからスレッドプールに切り替える際に、私たちの API を使うコードへ大きな変更を加える必要がありません。リスト 21-12 は、thread::spawn の代わりに使いたい ThreadPool 構造体の仮想的なインターフェイスを示しています。
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
ThreadPool::new を使って、設定可能な数のスレッドを持つ新しいスレッドプールを作成します。この場合は 4 つです。続いて for ループの中で、pool.execute は thread::spawn と同様のインターフェイスを持ち、プールが各ストリームに対して実行すべきクロージャを受け取ります。pool.execute を実装して、そのクロージャを受け取り、実行のためにプール内のスレッドへ渡す必要があります。このコードはまだコンパイルできませんが、どう修正すべきかをコンパイラに導いてもらうために、まず試してみます。
コンパイラ駆動開発で ThreadPool を構築する
リスト 21-12 の変更を src/main.rs に加え、それから cargo check のコンパイラエラーを手がかりに開発を進めていきましょう。最初に得られるエラーは次のとおりです。
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0433]: failed to resolve: use of undeclared type `ThreadPool`
--> src/main.rs:11:16
|
11 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^ use of undeclared type `ThreadPool`
For more information about this error, try `rustc --explain E0433`.
error: could not compile `hello` (bin "hello") due to 1 previous error
すばらしいです!このエラーは、ThreadPool 型またはモジュールが必要だと教えてくれているので、ここでそれを作成しましょう。私たちの ThreadPool 実装は、Web サーバーがどのような仕事をしているかには依存しません。そこで、hello クレートをバイナリクレートからライブラリクレートへ切り替え、ThreadPool 実装をそこに置くことにしましょう。ライブラリクレートに変更すれば、Web リクエストを処理するためだけでなく、スレッドプールを使って行いたいあらゆる作業に、その独立したスレッドプールライブラリを利用できるようになります。
以下を含む src/lib.rs ファイルを作成してください。これは、現時点で用意できる最も単純な ThreadPool 構造体の定義です。
pub struct ThreadPool;
次に、main.rs ファイルを編集して、以下のコードを src/main.rs の先頭に追加し、ライブラリクレートから ThreadPool をスコープに持ち込みます。
use hello::ThreadPool;
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
このコードもまだ動きませんが、対処すべき次のエラーを得るために、もう一度確認してみましょう。
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in the current scope
--> src/main.rs:12:28
|
12 | let pool = ThreadPool::new(4);
| ^^^ function or associated item not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error
このエラーは、次に ThreadPool のために new という関連関数を作成する必要があることを示しています。また、new は引数として 4 を受け取れる 1 つのパラメータを持ち、ThreadPool インスタンスを返す必要があることもわかっています。それらの性質を持つ最も単純な new 関数を実装してみましょう。
pub struct ThreadPool;
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
}
size パラメータの型として usize を選んだのは、スレッド数が負の値であることには意味がないとわかっているからです。また、この 4 はスレッドのコレクション内の要素数として使うこともわかっています。これは usize 型の用途であり、第 3 章の 「整数型」 の節で説明したとおりです。
もう一度コードを確認してみましょう。
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope
--> src/main.rs:17:14
|
17 | pool.execute(|| {
| -----^^^^^^^ method not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error
今度のエラーは、ThreadPool に execute メソッドがないために発生しています。「有限個のスレッドを作成する」 の節で、スレッドプールは thread::spawn に似たインターフェイスを持つべきだと決めたことを思い出してください。さらに、execute 関数は、与えられたクロージャを受け取り、それをプール内のアイドル状態のスレッドに渡して実行させるように実装します。
ThreadPool の execute メソッドは、パラメータとしてクロージャを受け取るように定義します。第 13 章の 「キャプチャした値をクロージャの外へムーブする」 で見たように、クロージャは 3 種類の異なるトレイト Fn、FnMut、FnOnce のいずれかとしてパラメータに取ることができます。ここでは、どの種類のクロージャを使うべきかを決める必要があります。最終的には標準ライブラリの thread::spawn 実装に似たことをすることになるので、thread::spawn のシグネチャがそのパラメータにどのような境界を課しているかを見てみましょう。ドキュメントには次のようにあります。
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
ここで関心があるのは型パラメータ F です。型パラメータ T は戻り値に関係しており、今回は関係ありません。spawn が F に対するトレイト境界として FnOnce を使っていることがわかります。これはおそらく私たちにも望ましいものです。というのも、最終的には execute で受け取った引数を spawn に渡すことになるからです。さらに、FnOnce が使いたいトレイトだとより確信できます。なぜなら、リクエストを処理するスレッドはそのリクエストのクロージャを 1 回だけ実行するので、これは FnOnce の Once と一致するからです。
型パラメータ F には、トレイト境界 Send とライフタイム境界 'static もあります。これらは今回の状況で有用です。クロージャをあるスレッドから別のスレッドへ移動するには Send が必要であり、またスレッドの実行にどれくらい時間がかかるかわからないため 'static が必要です。これらの境界を持つ型 F のジェネリックパラメータを受け取る execute メソッドを ThreadPool に作成しましょう。
pub struct ThreadPool;
impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
FnOnce の後ろに引き続き () を使っているのは、この FnOnce が、パラメータを取らず、ユニット型 () を返すクロージャを表しているからです。関数定義と同様に、戻り値の型はシグネチャから省略できますが、パラメータがない場合でも括弧は必要です。
繰り返しますが、これは execute メソッドの最も単純な実装です。何もしませんが、私たちはただコードをコンパイル可能にしようとしているだけです。もう一度確認してみましょう。
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
コンパイルできました!ただし、cargo run を試してブラウザでリクエストを行うと、この章の冒頭で見たものと同じエラーがブラウザに表示されることに注意してください。私たちのライブラリは、execute に渡されたクロージャをまだ実際には呼び出していません。
注: Haskell や Rust のような厳格なコンパイラを持つ言語について、「コードがコンパイルできるなら、動く」という言い回しを耳にすることがあるかもしれません。しかし、この言い回しは普遍的に正しいわけではありません。私たちのプロジェクトはコンパイルできますが、まったく何もしません! 実際の完全なプロジェクトを構築しているのであれば、この時点でユニットテストを書き始め、コードがコンパイルできることと、望んでいる振る舞いをすることの両方を確認するのがよいでしょう。
考えてみてください: ここで、クロージャの代わりに future を実行しようとしていたら、何が異なっていたでしょうか?
new でスレッド数を検証する
new と execute のパラメータについては、まだ何もしていません。これらの関数本体を、望んでいる振る舞いになるように実装していきましょう。まずは new について考えます。先ほど、size パラメータには符号なし型を選びました。負の数のスレッドを持つプールには意味がないからです。しかし、スレッド数が 0 のプールにも意味はありません。それでも 0 は完全に有効な usize です。ThreadPool インスタンスを返す前に、size が 0 より大きいことを確認するコードを追加し、assert! マクロを使って 0 を受け取った場合にはプログラムを panic させます。これはリスト 21-13 に示されています。
pub struct ThreadPool;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
ThreadPool
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
また、doc コメントを使って ThreadPool のドキュメントもいくつか追加しました。第 14 章で説明したように、関数が panic しうる状況を明示するセクションを追加するという、よいドキュメント作成の実践に従っている点に注目してください。cargo doc --open を実行し、ThreadPool 構造体をクリックして、new 用に生成されたドキュメントがどのように見えるか確認してみてください!
ここで行ったように assert! マクロを追加する代わりに、new を build に変更し、リスト 12-9 の I/O プロジェクトで Config::build に対して行ったのと同じように Result を返すこともできます。しかしこの場合、スレッドを 1 本も持たないスレッドプールを作ろうとすることは回復不能なエラーであると判断しました。意欲があれば、次のシグネチャを持つ build という名前の関数を書いて、new 関数と比較してみてください。
pub fn build(size: usize) -> Result<ThreadPool, PoolCreationError> {
スレッドを格納するための領域を作成する
これで、プールに格納するスレッド数が有効であることを確認する方法ができたので、そのスレッドを作成し、構造体を返す前に ThreadPool 構造体へ格納できます。しかし、スレッドを「格納する」とはどういうことでしょうか? thread::spawn のシグネチャをもう一度見てみましょう。
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
spawn 関数は JoinHandle<T> を返します。ここで T はクロージャが返す型です。JoinHandle も使ってみて、何が起こるか見てみましょう。今回の場合、スレッドプールに渡すクロージャは接続を処理し、何も返しません。そのため T はユニット型 () になります。
リスト 21-14 のコードはコンパイルできますが、まだスレッドは 1 本も作成しません。ThreadPool の定義を変更して thread::JoinHandle<()> インスタンスのベクタを保持するようにし、そのベクタを size の容量で初期化し、スレッドを作成するコードを実行する for ループを用意し、それらを含む ThreadPool インスタンスを返すようにしました。
use std::thread;
pub struct ThreadPool {
threads: Vec<thread::JoinHandle<()>>,
}
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let mut threads = Vec::with_capacity(size);
for _ in 0..size {
// create some threads and store them in the vector
}
ThreadPool { threads }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
ライブラリクレート内で std::thread をスコープに持ち込みました。これは、ThreadPool 内のベクタの要素型として thread::JoinHandle を使っているためです。
有効な size を受け取ると、ThreadPool は size 個の要素を保持できる新しいベクタを作成します。with_capacity 関数は Vec::new と同じ役割を果たしますが、重要な違いがあります。それは、ベクタ内に領域を事前に確保することです。ベクタに size 個の要素を格納する必要があるとわかっているため、最初にこの確保を行うのは、要素が挿入されるたびに自分自身をリサイズする Vec::new を使うより、わずかに効率的です。
再び cargo check を実行すると、成功するはずです。
ThreadPool からスレッドへコードを送る
リスト 21-14 の for ループには、スレッドの作成に関するコメントを残していました。ここでは、実際にどのようにスレッドを作成するのかを見ていきます。標準ライブラリはスレッドを作成する方法として thread::spawn を提供しており、thread::spawn はスレッド作成直後にそのスレッドが実行すべきコードを受け取ることを想定しています。しかし今回の場合、私たちはスレッドを作成したうえで、あとから送るコードを 待機 させたいのです。標準ライブラリのスレッド実装にはそれを行う方法は含まれていないため、自分たちで実装する必要があります。
この振る舞いを実装するために、ThreadPool とスレッドの間に新しいデータ構造を導入し、この新しい振る舞いを管理させます。このデータ構造を Worker と呼ぶことにします。これはプーリング実装で一般的な用語です。Worker は実行すべきコードを受け取り、そのコードを自分のスレッド上で実行します。
レストランの厨房で働く人たちを考えてみてください。ワーカーたちは顧客から注文が入るまで待ち、注文が入ったらそれを受け取り、内容をこなす責任を負います。
スレッドプール内に JoinHandle<()> インスタンスのベクタを格納する代わりに、Worker 構造体のインスタンスを格納します。各 Worker は 1 つの JoinHandle<()> インスタンスを保持します。そして Worker にメソッドを実装し、実行すべきコードのクロージャを受け取って、それをすでに動作中のスレッドへ送って実行させるようにします。また、ログ出力やデバッグの際にプール内の異なる Worker インスタンスを区別できるよう、各 Worker に id も持たせます。
ThreadPool を作成するときに起こる新しい処理は次のとおりです。このように Worker を準備したあとで、クロージャをスレッドへ送るコードを実装します。
idとJoinHandle<()>を保持するWorker構造体を定義する。ThreadPoolがWorkerインスタンスのベクタを保持するように変更する。id番号を受け取り、そのidと空のクロージャで生成されたスレッドを保持するWorkerインスタンスを返すWorker::new関数を定義する。ThreadPool::newでforループのカウンタを使ってidを生成し、そのidを持つ新しいWorkerを作成して、ベクタに格納する。
挑戦してみたいなら、リスト 21-15 のコードを見る前に、これらの変更を自分で実装してみてください。
準備はできましたか? それでは、前述の変更を行う 1 つの方法を示したリスト 21-15 を見ていきましょう。
```rust,noplayground
use std::thread;
pub struct ThreadPool {
workers: Vec<Worker>,
}
impl ThreadPool {
// --snip--
# /// Create a new ThreadPool.
# ///
# /// The size is the number of threads in the pool.
# ///
# /// # Panics
# ///
# /// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id));
}
ThreadPool { workers }
}
// --snip--
#
# pub fn execute<F>(&self, f: F)
# where
# F: FnOnce() + Send + 'static,
# {
# }
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize) -> Worker {
let thread = thread::spawn(|| {});
Worker { id, thread }
}
}
ThreadPool のフィールド名を threads から workers に変更しました。
これは、保持しているのが JoinHandle<()> のインスタンスではなく、
Worker のインスタンスになったためです。for ループ内のカウンタを
Worker::new の引数として使い、新しい Worker をそれぞれ workers という名前のベクタに格納します。
外部のコード(src/main.rs にある私たちのサーバーなど)は、
ThreadPool 内で Worker 構造体を使っている実装の詳細を知る必要が
ないため、Worker 構造体とその new 関数は非公開にします。Worker::new
関数は、与えられた id を使い、空のクロージャで新しいスレッドを生成して作られた
JoinHandle<()> インスタンスを保持します。
注: システムリソースが不足していてオペレーティングシステムがスレッドを 作成できない場合、
thread::spawnはパニックします。すると、一部の スレッドの作成には成功していても、サーバー全体がパニックすることに なります。単純化のため、この振る舞いでも問題ありませんが、本番用の スレッドプール実装では、代わりにstd::thread::Builderと そのResultを返すspawnメソッドを使いたく なるでしょう。
このコードはコンパイルでき、ThreadPool::new の引数として指定した数の
Worker インスタンスを格納します。しかし、私たちは まだ execute で受け取る
クロージャを処理していません。次に、それをどう行うかを見ていきましょう。
チャネルを介してスレッドにリクエストを送る
次に取り組む問題は、thread::spawn に渡しているクロージャが
まったく何もしないことです。現在、実行したいクロージャは execute
メソッドで受け取っています。しかし、ThreadPool の作成中に各 Worker
を生成するとき、thread::spawn に実行させるクロージャを渡す必要があります。
今作成した Worker 構造体には、ThreadPool が保持しているキューから
実行するコードを取り出し、そのコードをスレッドに送って実行させたいと
考えています。
第 16 章で学んだチャネルは、2 つのスレッド間で通信するための単純な方法であり、
このユースケースにぴったりです。ジョブのキューとして機能するようにチャネルを使い、
execute は ThreadPool から Worker インスタンスへジョブを送り、
Worker インスタンスはそのジョブを自分のスレッドに送ります。計画は次のとおりです。
ThreadPoolはチャネルを作成し、送信側を保持します。- 各
Workerは受信側を保持します。 - チャネルに送りたいクロージャを保持する新しい
Job構造体を作成します。 executeメソッドは、実行したいジョブを送信側経由で送ります。Workerは自分のスレッド内で受信側に対してループし、受け取った ジョブのクロージャを実行します。
まず、リスト 21-16 に示すように、ThreadPool::new でチャネルを作成し、
送信側を ThreadPool インスタンスに保持させましょう。Job 構造体は
今のところ何も保持しませんが、チャネルを通して送るアイテムの型になります。
use std::{sync::mpsc, thread};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
struct Job;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id));
}
ThreadPool { workers, sender }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize) -> Worker {
let thread = thread::spawn(|| {});
Worker { id, thread }
}
}
ThreadPool::new では、新しいチャネルを作成し、プールに送信側を保持させます。
これは問題なくコンパイルできます。
次に、スレッドプールがチャネルを作成するときに、チャネルの受信側を
各 Worker に渡してみましょう。受信側は Worker インスタンスが
生成するスレッドの中で使いたいので、クロージャ内で receiver
引数を参照することになります。リスト 21-17 のコードは、まだ完全にはコンパイルしません。
use std::{sync::mpsc, thread};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
struct Job;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, receiver));
}
ThreadPool { workers, sender }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
// --snip--
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
let thread = thread::spawn(|| {
receiver;
});
Worker { id, thread }
}
}
小さくてわかりやすい変更をいくつか行いました。受信側を Worker::new に
渡し、その後クロージャの中でそれを使っています。
このコードをチェックしようとすると、次のエラーが出ます。
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0382]: use of moved value: `receiver`
--> src/lib.rs:26:42
|
21 | let (sender, receiver) = mpsc::channel();
| -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
...
25 | for id in 0..size {
| ----------------- inside of this loop
26 | workers.push(Worker::new(id, receiver));
| ^^^^^^^^ value moved here, in previous iteration of loop
|
note: consider changing this parameter type in method `new` to borrow instead if owning the value isn't necessary
--> src/lib.rs:47:33
|
47 | fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
| --- in this method ^^^^^^^^^^^^^^^^^^^ this parameter takes ownership of the value
help: consider moving the expression out of the loop so it is only moved once
|
25 ~ let mut value = Worker::new(id, receiver);
26 ~ for id in 0..size {
27 ~ workers.push(value);
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `hello` (lib) due to 1 previous error
コードは receiver を複数の Worker インスタンスに渡そうとしています。これは、
第 16 章で思い出せるように、うまくいきません。Rust が提供するチャネル実装は、
複数の producer、単一の consumer だからです。つまり、このコードを修正するために
チャネルの受信側を単純にクローンすることはできません。また、複数の consumer に
メッセージを複数回送りたいわけでもありません。必要なのは、複数の Worker
インスタンスがある 1 つのメッセージのリストであり、各メッセージが 1 回だけ
処理されるようにしたいのです。
さらに、チャネルキューからジョブを取り出すには receiver を変更する必要が
あるため、スレッドには receiver を安全に共有して変更する方法が必要です。
そうしないと、競合状態が発生する可能性があります(第 16 章で扱いました)。
第 16 章で説明したスレッドセーフなスマートポインタを思い出してください。
複数のスレッドで所有権を共有し、かつスレッドが値を変更できるようにするには、
Arc<Mutex<T>> を使う必要があります。Arc 型によって複数の Worker
インスタンスが受信側を所有できるようになり、Mutex は一度に 1 つの Worker
だけが受信側からジョブを取得できるようにします。リスト 21-18 に必要な変更を示します。
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
// --snip--
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
struct Job;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
// --snip--
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
// --snip--
let thread = thread::spawn(|| {
receiver;
});
Worker { id, thread }
}
}
ThreadPool::new では、受信側を Arc と Mutex に入れます。新しい
各 Worker について、参照カウントを増やすために Arc をクローンし、
Worker インスタンスが受信側の所有権を共有できるようにします。
これらの変更により、コードはコンパイルできます! だいぶ近づいてきました!
execute メソッドを実装する
最後に、ThreadPool の execute メソッドを実装しましょう。また、Job
を構造体から、execute が受け取るクロージャの型を保持するトレイトオブジェクトの
型エイリアスに変更します。第 20 章の 「型シノニムと型エイリアス」 節で
説明したように、型エイリアスを使うと、長い型を使いやすいように短くできます。
リスト 21-19 を見てください。
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
// --snip--
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
// --snip--
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(|| {
receiver;
});
Worker { id, thread }
}
}
execute で受け取ったクロージャを使って新しい Job インスタンスを作成した後、
そのジョブをチャネルの送信側から送ります。送信に失敗する場合に備えて、send に対して
unwrap を呼び出しています。たとえば、すべてのスレッドの実行を停止させて、
受信側が新しいメッセージを受け取らなくなった場合に、これが起こりえます。現時点では、
スレッドの実行を止めることはできません。プールが存在する限り、スレッドは実行を
続けます。unwrap を使っている理由は、この失敗ケースは起こらないとわかっている
からですが、コンパイラはそれを知らないからです。
ですが、まだ完全には終わっていません! Worker では、thread::spawn に渡して
いるクロージャが、依然としてチャネルの受信側を 参照しているだけ です。
代わりに、このクロージャが永遠にループし、チャネルの受信側にジョブを求め、
ジョブを受け取ったらそのジョブを実行する必要があります。Worker::new に、
リスト21-20に示す変更を加えましょう。
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
// --snip--
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
ここでは、まず receiver に対して lock を呼び出して mutex を取得し、それから
エラーがあれば panic するために unwrap を呼び出します。mutex が ポイズン
状態にあると、ロックの取得は失敗する可能性があります。これは、ほかのスレッドが
ロックを解放せずに保持したまま panic した場合に起こり得ます。この状況では、
このスレッドも panic するように unwrap を呼び出すのが正しい対処です。必要で
あれば、この unwrap を、自分にとって意味のあるエラーメッセージを持つ
expect に変更して構いません。
mutex のロックを取得できたら、recv を呼び出してチャネルから Job を受け取り
ます。最後の unwrap でも、ここで起こり得るエラーがあれば panic します。これ
は、受信側がシャットダウンしたときに send メソッドが Err を返すのと同様に、
送信側を保持しているスレッドが停止している場合に発生し得ます。
recv の呼び出しはブロックするので、まだジョブがなければ、現在のスレッドは
ジョブが利用可能になるまで待機します。Mutex<T> により、ある時点でジョブを
要求しようとする Worker スレッドは1つだけであることが保証されます。
これでスレッドプールは動作する状態になりました! cargo run で実行して、
いくつかリクエストを送ってみましょう:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
warning: field `workers` is never read
--> src/lib.rs:7:5
|
6 | pub struct ThreadPool {
| ---------- field in this struct
7 | workers: Vec<Worker>,
| ^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: fields `id` and `thread` are never read
--> src/lib.rs:48:5
|
47 | struct Worker {
| ------ fields in this struct
48 | id: usize,
| ^^
49 | thread: thread::JoinHandle<()>,
| ^^^^^^
warning: `hello` (lib) generated 2 warnings
Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.91s
Running `target/debug/hello`
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
成功です!これで、接続を非同期に処理するスレッドプールができました。 作成されるスレッドは4本を超えないので、サーバーが大量のリクエストを受け取っても、 システムに過負荷はかかりません。/sleep にリクエストしても、別の スレッドがそれらを実行できるため、サーバーはほかのリクエストを処理できます。
注: /sleep を複数のブラウザウィンドウで同時に開くと、5秒間隔で1つずつ 読み込まれることがあります。一部のWebブラウザは、キャッシュ上の理由から、 同じリクエストを複数回行う場合に順番に実行します。この制限は、私たちの Webサーバーが原因ではありません。
ここでいったん立ち止まって、リスト21-18、21-19、21-20のコードが、処理すべき 作業にクロージャではなく future を使っていたらどう違っていたかを考えるのは 良いタイミングです。どの型が変わるでしょうか。メソッドシグネチャは、変わると すればどのように違っていたでしょうか。コードのどの部分は同じままでしょうか。
第17章と第19章で while let ループについて学んだあとでは、Worker の
スレッドコードをなぜリスト21-21のように書かなかったのか、不思議に思うかも
しれません。
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
// --snip--
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
while let Ok(job) = receiver.lock().unwrap().recv() {
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
このコードはコンパイルも実行もできますが、望んだスレッディングの振る舞いには
なりません。つまり、遅いリクエストがほかのリクエストの処理待ちを引き起こした
ままになります。その理由はやや微妙です。Mutex 構造体には公開された unlock
メソッドがありません。というのも、ロックの所有権は、lock メソッドが返す
LockResult<MutexGuard<T>> 内の MutexGuard<T> のライフタイムに基づいて
いるからです。そうすることで、コンパイル時に借用チェッカが、Mutex によって
保護されたリソースにはロックを保持しているときにしかアクセスできない、という
規則を強制できます。しかし、この実装では、MutexGuard<T> のライフタイムに
注意していないと、意図したよりも長くロックを保持してしまうこともあります。
let job = receiver.lock().unwrap().recv().unwrap(); を使っているリスト21-20のコードが
動くのは、let では、等号の右辺の式で使われた一時値が let 文の終わりで
直ちにドロップされるからです。しかし、while let(および if let と match)
は、対応するブロックの終わりまで一時値をドロップしません。リスト21-21では、
ロックは job() の呼び出し中ずっと保持されたままになるため、ほかの Worker
インスタンスはジョブを受け取れません。
グレースフルシャットダウンとクリーンアップ
グレースフルシャットダウンとクリーンアップ
リスト 21-20 のコードは、意図したとおり、スレッドプールを使ってリクエストに非同期に応答しています。直接は使っていない workers、id、thread フィールドについていくつか警告が出ますが、これは何もクリーンアップしていないことを思い出させてくれます。あまり洗練されていない ctrl-C の方法でメインスレッドを停止すると、たとえリクエストの処理中であっても、ほかのすべてのスレッドも即座に停止します。
次に、Drop トレイトを実装して、プール内の各スレッドに対して join を呼び出し、クローズする前に処理中のリクエストを完了できるようにします。続いて、スレッドに新しいリクエストの受け付けを停止してシャットダウンすべきことを伝える方法を実装します。このコードが実際に動作するところを見るために、サーバーが 2 つのリクエストだけを受け付けたあとでスレッドプールをグレースフルにシャットダウンするように変更します。
途中で 1 つ注意しておくべきことがあります。これらはいずれもクロージャの実行を扱うコード部分には影響しないので、非同期ランタイムのためにスレッドプールを使っていたとしても、ここでの内容はすべて同じです。
ThreadPool に Drop トレイトを実装する
まずはスレッドプールに Drop を実装するところから始めましょう。プールがドロップされるとき、スレッドはすべて join して、作業を確実に完了させる必要があります。リスト 21-22 は Drop 実装の最初の試みを示しています。このコードは、まだ完全には動きません。
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
まず、スレッドプールの各 workers を順にループします。ここで &mut を使うのは、self が可変参照であり、さらに worker も変更できる必要があるからです。各 worker について、この特定の Worker インスタンスがシャットダウン中であることを示すメッセージを表示し、その後でその Worker インスタンスのスレッドに対して join を呼び出します。join の呼び出しに失敗した場合は、unwrap を使って Rust にパニックを起こさせ、グレースフルでないシャットダウンに入ります。
このコードをコンパイルすると、次のエラーが表示されます。
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference
--> src/lib.rs:52:13
|
52 | worker.thread.join().unwrap();
| ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call
| |
| move occurs because `worker.thread` has type `JoinHandle<()>`, which does not implement the `Copy` trait
|
note: `JoinHandle::<T>::join` takes ownership of the receiver `self`, which moves `worker.thread`
--> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/std/src/thread/mod.rs:1921:17
For more information about this error, try `rustc --explain E0507`.
error: could not compile `hello` (lib) due to 1 previous error
このエラーは、各 worker に対して可変借用しか持っておらず、join は引数の所有権を取るため、join を呼び出せないことを示しています。この問題を解決するには、thread を所有している Worker インスタンスからそのスレッドを取り出して、join がそのスレッドを消費できるようにする必要があります。これを行う方法の 1 つは、リスト 18-15 で取ったのと同じアプローチを使うことです。Worker が Option<thread::JoinHandle<()>> を保持していれば、Option に対して take メソッドを呼び出して、Some バリアントから値を取り出し、その場所に None バリアントを残せます。言い換えると、実行中の Worker では thread に Some バリアントが入り、Worker をクリーンアップしたいときには Some を None に置き換えることで、その Worker が実行すべきスレッドを持たないようにできるわけです。
しかし、これが問題になるのは Worker をドロップするときの 唯一の 場面です。その代わりに、worker.thread にアクセスするあらゆる場所で Option<thread::JoinHandle<()>> を扱わなければならなくなります。慣用的な Rust では Option はかなり多く使われますが、このような回避策として、常に存在すると分かっているものを Option で包んでいることに気づいたら、コードをよりクリーンでエラーが起こりにくくする別のアプローチを探すのがよい考えです。
この場合には、よりよい代替手段があります。それが Vec::drain メソッドです。これは、ベクタからどの要素を削除するかを指定する範囲パラメータを受け取り、それらの要素のイテレータを返します。.. という範囲構文を渡すと、ベクタから すべての 値が削除されます。
したがって、ThreadPool の drop 実装を次のように更新する必要があります。
#![allow(unused)]
fn main() {
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in self.workers.drain(..) {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
}
これでコンパイラエラーは解消され、コードにほかの変更は必要ありません。なお、パニック中に drop が呼び出されることもあるため、unwrap もさらにパニックし、二重パニックを引き起こす可能性があります。そうなるとプログラムは即座にクラッシュし、進行中のクリーンアップも終了します。これはサンプルプログラムとしては問題ありませんが、本番コードでは推奨されません。
ジョブの受信を停止するようスレッドに通知する
ここまでの変更によって、コードは警告なしでコンパイルされるようになりました。しかし、悪い知らせとして、このコードはまだ私たちの望むようには動作しません。鍵になるのは、Worker インスタンスのスレッドによって実行されるクロージャ内のロジックです。現時点では join を呼び出していますが、それではスレッドはシャットダウンしません。なぜなら、それらはジョブを探して永遠に loop し続けるからです。現在の drop 実装のままで ThreadPool をドロップしようとすると、メインスレッドは最初のスレッドが終了するのを待って永遠にブロックされます。
この問題を修正するには、まず ThreadPool の drop 実装を変更し、そのあとで Worker ループも変更する必要があります。
まず、スレッドの終了を待つ前に sender を明示的にドロップするように、ThreadPool の drop 実装を変更します。リスト 21-23 は、sender を明示的にドロップするための ThreadPool への変更を示しています。スレッドの場合とは異なり、ここでは Option::take を使って sender を ThreadPool からムーブできるようにするために、実際に Option を使う必要があります。
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
// --snip--
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
// --snip--
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool {
workers,
sender: Some(sender),
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());
for worker in self.workers.drain(..) {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
sender をドロップするとチャネルが閉じられ、それ以上メッセージが送られないことが示されます。そうなると、Worker インスタンスが無限ループ内で行っている recv の呼び出しはすべてエラーを返すようになります。リスト 21-24 では、その場合にループをグレースフルに抜けるよう Worker ループを変更しています。これにより、ThreadPool の drop 実装がそれらに対して join を呼び出したときに、スレッドは終了します。
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool {
workers,
sender: Some(sender),
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());
for worker in self.workers.drain(..) {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let message = receiver.lock().unwrap().recv();
match message {
Ok(job) => {
println!("Worker {id} got a job; executing.");
job();
}
Err(_) => {
println!("Worker {id} disconnected; shutting down.");
break;
}
}
}
});
Worker { id, thread }
}
}
このコードが実際に動作するところを見るために、リスト 21-25 に示すように、サーバーをグレースフルにシャットダウンする前に main が 2 つのリクエストだけを受け付けるよう変更しましょう。
use hello::ThreadPool;
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming().take(2) {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
println!("Shutting down.");
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
現実の Web サーバーを、わずか 2 つのリクエストを処理しただけでシャットダウンさせたいとは思わないでしょう。このコードは、グレースフルシャットダウンとクリーンアップが正しく機能していることを示すためのものです。
take メソッドは Iterator トレイトで定義されており、イテレーションを最大で最初の 2 項目に制限します。ThreadPool は main の終わりでスコープを抜け、drop 実装が実行されます。
cargo run でサーバーを起動し、3 回リクエストを送ってください。3 回目のリクエスト
はエラーになるはずで、ターミナルには次のような出力が表示されるはずです:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/hello`
Worker 0 got a job; executing.
Shutting down.
Shutting down worker 0
Worker 3 got a job; executing.
Worker 1 disconnected; shutting down.
Worker 2 disconnected; shutting down.
Worker 3 disconnected; shutting down.
Worker 0 disconnected; shutting down.
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3
Worker の ID や表示されるメッセージの順序は、異なる場合があります。この
メッセージから、このコードがどのように動作しているかがわかります。Worker
インスタンス 0 と 3 が最初の 2 つのリクエストを受け取りました。サーバーは 2 つ目
の接続のあとで接続の受け付けを停止し、ThreadPool の Drop 実装は、Worker 3
が仕事を開始する前から実行を始めます。sender をドロップすると、すべての
Worker インスタンスが切断され、シャットダウンするよう通知されます。各
Worker インスタンスは切断されたときにそれぞれメッセージを表示し、そのあと
スレッドプールは join を呼び出して各 Worker スレッドの終了を待ちます。
この特定の実行には、1 つ興味深い点があります。ThreadPool が sender を
ドロップし、どの Worker もまだエラーを受け取っていない段階で、Worker 0 に
対して join を試みました。Worker 0 はまだ recv からエラーを受け取って
いなかったため、メインスレッドはブロックされ、Worker 0 が完了するのを待ち
ました。その間に、Worker 3 は仕事を受け取り、そのあとですべてのスレッドが
エラーを受け取りました。Worker 0 が完了すると、メインスレッドは残りの
Worker インスタンスが完了するのを待ちました。その時点で、それらはすべて
ループを抜けて停止していました。
おめでとうございます! これでプロジェクトは完了です。スレッドプールを使って 非同期に応答する基本的な Web サーバーができました。サーバーをグレースフルに シャットダウンできるため、プール内のすべてのスレッドをクリーンアップできます。
参考までに、完全なコードを示します:
use hello::ThreadPool;
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming().take(2) {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
println!("Shutting down.");
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool {
workers,
sender: Some(sender),
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}
struct Worker {
id: usize,
thread: Option<thread::JoinHandle<()>>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let message = receiver.lock().unwrap().recv();
match message {
Ok(job) => {
println!("Worker {id} got a job; executing.");
job();
}
Err(_) => {
println!("Worker {id} disconnected; shutting down.");
break;
}
}
}
});
Worker {
id,
thread: Some(thread),
}
}
}
ここからさらに発展させることもできます! このプロジェクトを引き続き改善したい なら、次のようなアイデアがあります:
ThreadPoolとその公開メソッドに、さらにドキュメントを追加する。- ライブラリの機能に対するテストを追加する。
unwrapの呼び出しを、より堅牢なエラーハンドリングに変更する。- Web リクエストの処理以外の何らかのタスクを実行するために
ThreadPoolを使う。 - crates.io でスレッドプールクレートを探し、そのクレートを使って同様の Web サーバーを実装する。次に、その API と 堅牢性を、私たちが実装したスレッドプールと比較する。
まとめ
お疲れさまでした! ついに本書を最後まで読み終えました! Rust の旅に参加して くれて、ありがとうございます。これで、自分自身の Rust プロジェクトを実装したり、 ほかの人のプロジェクトを手伝ったりする準備が整いました。Rust の学習の道のりで どんな課題に出会っても、喜んで助けてくれる温かい Rustacean のコミュニティが あることを忘れないでください。
付録
以下のセクションには、Rustを学ぶ過程で役立つ 参考資料が含まれています。
A - キーワード
付録A: キーワード
以下の一覧には、Rust言語において現在または将来の使用のために予約されて いるキーワードが含まれています。そのため、これらは識別子として使用できません(ただし、 「生の 識別子」 の節で説明するように、生の識別子として使用する場合を除きます)。識別子 とは、 関数、変数、パラメータ、struct フィールド、モジュール、クレート、定数、 マクロ、static 値、属性、型、トレイト、またはライフタイムの名前です。
現在使用されているキーワード
以下は、現在使用されているキーワードと、その機能の説明の一覧です。
as: プリミティブ型へのキャストを行い、項目を含む特定のトレイトを 明確化し、またはuse文で項目の名前を変更する。async: 現在のスレッドをブロックする代わりにFutureを返す。await:Futureの結果が準備できるまで実行を一時停止する。break: ループを即座に抜ける。const: 定数アイテムまたは定数生ポインタを定義する。continue: 次のループ反復に進む。crate: モジュールパスにおいて、クレートルートを指す。dyn: トレイトオブジェクトへの動的ディスパッチ。else:ifおよびif letの制御フロー構文におけるフォールバック。enum: 列挙型を定義する。extern: 外部関数または外部変数をリンクする。false: ブール値の偽リテラル。fn: 関数または関数ポインタ型を定義する。for: イテレータから項目を反復処理し、トレイトを実装し、または 高ランクのライフタイムを指定する。if: 条件式の結果に基づいて分岐する。impl: 固有の機能またはトレイトの機能を実装する。in:forループ構文の一部。let: 変数を束縛する。loop: 無条件にループする。match: 値をパターンに照合する。mod: モジュールを定義する。move: クロージャがキャプチャしたすべての値の所有権を取るようにする。mut: 参照、生ポインタ、またはパターン束縛における可変性を示す。pub: struct フィールド、implブロック、または モジュールにおける公開可視性を示す。ref: 参照によって束縛する。return: 関数から戻る。Self: 定義または実装している型を表す型エイリアス。self: メソッドの対象、または現在のモジュール。static: プログラムの実行全体にわたって存続するグローバル変数または ライフタイム。struct: 構造体を定義する。super: 現在のモジュールの親モジュール。trait: トレイトを定義する。true: ブール値の真リテラル。type: 型エイリアスまたは関連型を定義する。union: 共用体 を定義する。union宣言で使用される場合にのみキーワードである。unsafe: unsafe なコード、関数、トレイト、または実装を示す。use: シンボルをスコープに導入する。where: 型を制約する句を示す。while: 式の結果に基づいて条件付きでループする。
将来の使用のために予約されているキーワード
以下のキーワードにはまだ何の機能もありませんが、将来使用される可能性に備えて Rust によって予約されています。
abstractbecomeboxdofinalgenmacrooverrideprivtrytypeofunsizedvirtualyield
生の識別子
生の識別子 とは、通常であれば使えない場所でキーワードを使えるようにする構文です。
生の識別子は、キーワードの先頭に r# を付けて使用します。
たとえば、match はキーワードです。match を名前として使う次の関数を
コンパイルしようとすると:
ファイル名: src/main.rs
fn match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
次のエラーが表示されます:
error: expected identifier, found keyword `match`
--> src/main.rs:4:4
|
4 | fn match(needle: &str, haystack: &str) -> bool {
| ^^^^^ expected identifier, found keyword
このエラーが示しているとおり、キーワード match は関数の識別子として
使用できません。match を関数名として使うには、次のように生の
識別子構文を使う必要があります:
ファイル名: src/main.rs
fn r#match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
fn main() {
assert!(r#match("foo", "foobar"));
}
このコードはエラーなしでコンパイルできます。関数の定義時の名前だけでなく、
main でその関数を呼び出している箇所でも、関数名に r# プレフィックスが
付いていることに注目してください。
生の識別子を使うと、たまたまその語が予約キーワードであっても、好きな語を
識別子として使うことができます。これにより、識別子名を選ぶ自由度が増すだけでなく、
こうした語がキーワードではない言語で書かれたプログラムと連携することもできます。
さらに、生の識別子を使うと、あなたのクレートが使っているものとは異なる Rust
エディションで書かれたライブラリを使うこともできます。たとえば、try は 2015
edition ではキーワードではありませんが、2018、2021、
および 2024 editions ではキーワードです。2015
edition を使って書かれ、try 関数を持つライブラリに依存している場合、
後の editions でその関数を自分のコードから呼び出すには、この場合は r#try である
生の識別子構文を使う必要があります。エディションの詳細については、
付録E を参照してください。
B - 演算子と記号
付録B: 演算子と記号
この付録には、Rust の構文の用語集として、演算子やその他の記号を収録しています。これらの記号は、単独で現れる場合もあれば、パス、ジェネリクス、トレイト境界、マクロ、属性、コメント、タプル、括弧の文脈で現れる場合もあります。
演算子
表B-1には、Rust における演算子、その演算子が文脈の中でどのように現れるかの例、簡単な説明、その演算子がオーバーロード可能かどうかが示されています。演算子がオーバーロード可能な場合は、その演算子をオーバーロードするために使用する対応するトレイトも記載されています。
表B-1: 演算子
| 演算子 | 例 | 説明 | オーバーロード可能? |
|---|---|---|---|
! | ident!(...), ident!{...}, ident![...] | マクロ展開 | |
! | !expr | ビット単位または論理補数 | Not |
!= | expr != expr | 不等比較 | PartialEq |
% | expr % expr | 算術剰余 | Rem |
%= | var %= expr | 算術剰余と代入 | RemAssign |
& | &expr, &mut expr | 借用 | |
& | &type, &mut type, &'a type, &'a mut type | 借用ポインタ型 | |
& | expr & expr | ビット単位 AND | BitAnd |
&= | var &= expr | ビット単位 AND と代入 | BitAndAssign |
&& | expr && expr | 短絡論理 AND | |
* | expr * expr | 算術乗算 | Mul |
*= | var *= expr | 算術乗算と代入 | MulAssign |
* | *expr | 逆参照 | Deref |
* | *const type, *mut type | 生ポインタ | |
+ | trait + trait, 'a + trait | 複合型制約 | |
+ | expr + expr | 算術加算 | Add |
+= | var += expr | 算術加算と代入 | AddAssign |
, | expr, expr | 引数および要素の区切り | |
- | - expr | 算術否定 | Neg |
- | expr - expr | 算術減算 | Sub |
-= | var -= expr | 算術減算と代入 | SubAssign |
-> | fn(...) -> type, |…| -> type | 関数およびクロージャの戻り値型 | |
. | expr.ident | フィールドアクセス | |
. | expr.ident(expr, ...) | メソッド呼び出し | |
. | expr.0, expr.1, and so on | タプルのインデックス指定 | |
.. | .., expr.., ..expr, expr..expr | 右端を含まない範囲リテラル | PartialOrd |
..= | ..=expr, expr..=expr | 右端を含む範囲リテラル | PartialOrd |
.. | ..expr | 構造体リテラル更新構文 | |
.. | variant(x, ..), struct_type { x, .. } | 「残りすべて」パターン束縛 | |
... | expr...expr | (非推奨。代わりに ..= を使用)パターン内: 包含範囲パターン | |
/ | expr / expr | 算術除算 | Div |
/= | var /= expr | 算術除算と代入 | DivAssign |
: | pat: type, ident: type | 制約 | |
: | ident: expr | 構造体フィールド初期化子 | |
: | 'a: loop {...} | ループラベル | |
; | expr; | 文およびアイテムの終端 | |
; | [...; len] | 固定長配列構文の一部 | |
<< | expr << expr | 左シフト | Shl |
<<= | var <<= expr | 左シフトと代入 | ShlAssign |
< | expr < expr | より小さいかの比較 | PartialOrd |
<= | expr <= expr | 以下比較 | PartialOrd |
= | var = expr, ident = type | 代入/等価 | |
== | expr == expr | 等価比較 | PartialEq |
=> | pat => expr | match アーム構文の一部 | |
> | expr > expr | より大きいかの比較 | PartialOrd |
>= | expr >= expr | 以上比較 | PartialOrd |
>> | expr >> expr | 右シフト | Shr |
>>= | var >>= expr | 右シフトと代入 | ShrAssign |
@ | ident @ pat | パターン束縛 | |
^ | expr ^ expr | ビット単位排他的 OR | BitXor |
^= | var ^= expr | ビット単位排他的 OR と代入 | BitXorAssign |
| | pat | pat | パターンの代替 | |
| | expr | expr | ビット単位 OR | BitOr |
|= | var |= expr | ビット単位 OR と代入 | BitOrAssign |
|| | expr || expr | 短絡論理 OR | |
? | expr? | エラー伝播 |
演算子ではない記号
以下の表には、演算子として機能しないすべての記号、つまり関数呼び出しやメソッド呼び出しのように振る舞わない記号が含まれています。
表B-2は、単独で現れ、さまざまな場所で有効な記号を示しています。
表B-2: 単独で現れる構文
| 記号 | 説明 |
|---|---|
'ident | 名前付きライフタイムまたはループラベル |
直後に u8、i32、f64、usize などが続く数字 | 特定の型の数値リテラル |
"..." | 文字列リテラル |
r"...", r#"..."#, r##"..."## など | 生文字列リテラル。エスケープ文字は処理されない |
b"..." | バイト文字列リテラル。文字列ではなくバイト配列を構築する |
br"...", br#"..."#, br##"..."## など | 生バイト文字列リテラル。生文字列リテラルとバイト文字列リテラルの組み合わせ |
'...' | 文字リテラル |
b'...' | ASCIIバイトリテラル |
|…| expr | クロージャ |
! | 発散する関数のための、常に空のボトム型 |
_ | 「無視される」パターン束縛。整数リテラルを読みやすくするためにも使われる |
表B-3は、モジュール階層を通ってアイテムに至るパスの文脈で現れる記号を示しています。
表B-3: パス関連の構文
| 記号 | 説明 |
|---|---|
ident::ident | 名前空間パス |
::path | クレートルートを基準とするパス(つまり、明示的な絶対パス) |
self::path | 現在のモジュールを基準とするパス(つまり、明示的な相対パス) |
super::path | 現在のモジュールの親を基準とするパス |
type::ident, <type as trait>::ident | 関連定数、関連関数、関連型 |
<type>::... | 直接名前を付けられない型の関連アイテム(例: <&T>::...、<[T]>::... など) |
trait::method(...) | それを定義するトレイトを指定してメソッド呼び出しの曖昧さを解消する |
type::method(...) | それが定義されている型を指定してメソッド呼び出しの曖昧さを解消する |
<type as trait>::method(...) | トレイトと型を指定してメソッド呼び出しの曖昧さを解消する |
表B-4は、ジェネリック型パラメータを使用する文脈で現れる記号を示しています。
表B-4: ジェネリクス
| 記号 | 説明 |
|---|---|
path<...> | 型の中でジェネリック型へのパラメータを指定する(例: Vec<u8>) |
path::<...>, method::<...> | 式の中でジェネリック型、関数、またはメソッドへのパラメータを指定する。しばしば turbofish と呼ばれる(例: "42".parse::<i32>()) |
fn ident<...> ... | ジェネリック関数を定義する |
struct ident<...> ... | ジェネリック構造体を定義する |
enum ident<...> ... | ジェネリック列挙型を定義する |
impl<...> ... | ジェネリック実装を定義する |
for<...> type | 高階ライフタイム境界 |
type<ident=type> | 1つ以上の関連型に特定の割り当てがあるジェネリック型(例: Iterator<Item=T>) |
表B-5は、トレイト境界によってジェネリック型パラメータを制約する文脈で現れる記号を示しています。
表B-5: トレイト境界の制約
| 記号 | 説明 |
|---|---|
T: U | U を実装する型に制約されたジェネリックパラメータ T |
T: 'a | ジェネリック型 T はライフタイム 'a より長く存続しなければならない(つまり、その型は推移的に 'a より短いライフタイムを持つ参照を含んではならない) |
T: 'static | ジェネリック型 T は 'static 以外の借用参照を含まない |
'b: 'a | ジェネリックライフタイム 'b はライフタイム 'a より長く存続しなければならない |
T: ?Sized | ジェネリック型パラメータが動的サイズ型であることを許可する |
'a + trait, trait + trait | 複合型制約 |
表B-6は、マクロの呼び出しまたは定義の文脈、および アイテムに属性を指定する文脈で現れる記号を示しています。
表B-6: マクロと属性
| 記号 | 説明 |
|---|---|
#[meta] | 外側属性 |
#![meta] | 内側属性 |
$ident | マクロ置換 |
$ident:kind | マクロメタ変数 |
$(...)... | マクロ繰り返し |
ident!(...), ident!{...}, ident![...] | マクロ呼び出し |
表B-7は、コメントを作成する記号を示しています。
表B-7: コメント
| 記号 | 説明 |
|---|---|
// | 行コメント |
//! | 内側の行ドキュメントコメント |
/// | 外側の行ドキュメントコメント |
/*...*/ | ブロックコメント |
/*!...*/ | 内側のブロックドキュメントコメント |
/**...*/ | 外側のブロックドキュメントコメント |
表B-8は、丸かっこが使用される文脈を示しています。
表B-8: 丸かっこ
| 記号 | 説明 |
|---|---|
() | 空のタプル(別名ユニット)。リテラルと型の両方 |
(expr) | 丸かっこで囲まれた式 |
(expr,) | 単一要素のタプル式 |
(type,) | 単一要素のタプル型 |
(expr, ...) | タプル式 |
(type, ...) | タプル型 |
expr(expr, ...) | 関数呼び出し式。タプル struct およびタプル enum バリアントの初期化にも使用される |
表B-9は、波かっこが使用される文脈を示しています。
表B-9: 波かっこ
| コンテキスト | 説明 |
|---|---|
{...} | ブロック式 |
Type {...} | 構造体リテラル |
表B-10は、角かっこが使用される文脈を示しています。
表B-10: 角かっこ
| コンテキスト | 説明 |
|---|---|
[...] | 配列リテラル |
[expr; len] | expr のコピーを len 個含む配列リテラル |
[type; len] | type の要素を len 個含む配列型 |
expr[expr] | コレクションの添字アクセス。オーバーロード可能(Index, IndexMut) |
expr[..], expr[a..], expr[..b], expr[a..b] | コレクションのスライスのように見えるコレクション添字アクセスで、「インデックス」として Range、RangeFrom、RangeTo、または RangeFull を使用する |
C - 導出可能なトレイト
付録C: 導出可能なトレイト
本書のさまざまな箇所で、derive 属性について説明してきました。これは struct や enum の定義に適用できます。derive 属性は、derive 構文で注釈を付けた型に対して、独自のデフォルト実装を持つトレイトを実装するコードを生成します。
この付録では、derive とともに使用できる標準ライブラリ内のすべてのトレイトのリファレンスを提供します。各セクションでは、次の内容を扱います。
- このトレイトを導出すると、どの演算子やメソッドが使えるようになるか
deriveが提供するそのトレイトの実装が何を行うか- そのトレイトを実装していることが、その型について何を意味するか
- そのトレイトを実装してよい条件、または実装してはいけない条件
- そのトレイトを必要とする操作の例
derive 属性が提供するものとは異なる振る舞いが必要な場合は、各トレイトを手動で実装する方法の詳細について、標準ライブラリのドキュメントを参照してください。
ここに挙げるトレイトは、標準ライブラリで定義されているもののうち、derive を使って自分の型に実装できる唯一のものです。標準ライブラリで定義されているほかのトレイトには妥当なデフォルトの振る舞いがないため、何を実現しようとしているのかに応じて意味のある形で自分で実装する必要があります。
導出できないトレイトの例として Display があります。これはエンドユーザー向けの整形を扱います。型をエンドユーザーにどのように表示するのが適切かは、常に検討すべきです。型のどの部分をエンドユーザーに見せるべきでしょうか。どの部分がユーザーにとって関連があるでしょうか。どのようなデータ形式が最も関連性が高いでしょうか。Rust コンパイラにはこのような洞察がないため、適切なデフォルトの振る舞いを提供できません。
この付録で示す導出可能なトレイトの一覧は、網羅的なものではありません。ライブラリは独自のトレイトに対して derive を実装できるため、derive とともに使えるトレイトの一覧は実際には際限なく広がります。derive の実装には手続きマクロを使用します。これについては、第20章の 「カスタム derive マクロ」 節で扱います。
プログラマ向け出力のための Debug
Debug トレイトは、フォーマット文字列でのデバッグ整形を有効にします。これは {} プレースホルダー内に :? を追加することで指定します。
Debug トレイトを使うと、デバッグ目的である型のインスタンスを出力できるため、その型を使うあなたやほかのプログラマは、プログラム実行中の特定の時点でインスタンスを調べることができます。
たとえば Debug トレイトは、assert_eq! マクロの使用時に必要です。このマクロは、等価性のアサーションが失敗した場合に引数として与えられたインスタンスの値を出力するため、プログラマは 2 つのインスタンスがなぜ等しくなかったのかを確認できます。
等価比較のための PartialEq と Eq
PartialEq トレイトを使うと、型のインスタンスを比較して等しいかどうかを確認でき、== および != 演算子を使用できるようになります。
PartialEq を導出すると、eq メソッドが実装されます。struct に対して PartialEq を導出した場合、2 つのインスタンスが等しいのは、すべての フィールドが等しい場合に限られ、いずれかの フィールドが等しくなければ、そのインスタンス同士は等しくありません。enum に対して導出した場合、各バリアントは自分自身とは等しく、ほかのバリアントとは等しくありません。
たとえば PartialEq トレイトは、assert_eq! マクロを使う際に必要です。このマクロは、型の 2 つのインスタンスを等しいかどうか比較できる必要があります。
Eq トレイトにはメソッドがありません。その目的は、注釈を付けた型のすべての値について、その値が自分自身と等しいことを示すことです。Eq トレイトは、PartialEq も実装している型にしか適用できませんが、PartialEq を実装しているすべての型が Eq を実装できるわけではありません。この例の 1 つが浮動小数点数型です。浮動小数点数の実装では、非数 (NaN) 値の 2 つのインスタンスは互いに等しくないと定義されています。
Eq が必要になる例として、HashMap<K, V> のキーがあります。これは HashMap<K, V> が 2 つのキーが同じかどうかを判定できるようにするためです。
順序比較のための PartialOrd と Ord
PartialOrd トレイトを使うと、ソートのために型のインスタンスを比較できます。PartialOrd を実装した型は、<、>、<=、>= 演算子とともに使用できます。PartialOrd トレイトは、PartialEq も実装している型にしか適用できません。
PartialOrd を導出すると、partial_cmp メソッドが実装されます。このメソッドは Option<Ordering> を返し、与えられた値から順序を決定できない場合は None になります。その型のほとんどの値は比較できるとしても、順序を決定できない値の例として NaN 浮動小数点値があります。任意の浮動小数点数と NaN 浮動小数点値に対して partial_cmp を呼び出すと、None が返ります。
struct に対して導出した場合、PartialOrd は struct 定義内でフィールドが現れる順序に従って、各フィールドの値を比較することで 2 つのインスタンスを比較します。enum に対して導出した場合、enum 定義で先に宣言されたバリアントは、後に列挙されたバリアントよりも小さいと見なされます。
たとえば PartialOrd トレイトは、rand クレートの gen_range メソッドで、範囲式によって指定された範囲内のランダムな値を生成する際に必要です。
Ord トレイトを使うと、注釈を付けた型の任意の 2 つの値について、妥当な順序が存在することがわかります。Ord トレイトは cmp メソッドを実装します。このメソッドは Option<Ordering> ではなく Ordering を返します。なぜなら、妥当な順序は常に存在するからです。Ord トレイトは、PartialOrd と Eq も実装している型にしか適用できません(そして Eq には PartialEq が必要です)。struct と enum に対して導出した場合、cmp は PartialOrd における partial_cmp の導出実装と同じように振る舞います。
Ord が必要になる例として、値の並び順に基づいてデータを格納するデータ構造である BTreeSet<T> に値を格納する場合があります。
値を複製するための Clone と Copy
Clone トレイトを使うと、値のディープコピーを明示的に作成でき、その複製処理には任意のコードの実行やヒープデータのコピーが含まれる場合があります。Clone の詳細については、第4章の 「Clone と相互作用する変数とデータ」 節を参照してください。
Clone を導出すると、clone メソッドが実装されます。このメソッドは、型全体に対して実装されると、型を構成する各部分に対して clone を呼び出します。つまり、Clone を導出するには、その型のすべてのフィールドまたは値も Clone を実装していなければなりません。
Clone が必要になる例として、スライスに対して to_vec メソッドを呼び出す場合があります。スライスは含んでいる型のインスタンスを所有していませんが、to_vec が返すベクタはそれらのインスタンスを所有する必要があるため、to_vec は各要素に対して clone を呼び出します。したがって、スライスに格納されている型は Clone を実装していなければなりません。
Copy トレイトを使うと、スタックに格納されているビットをコピーするだけで値を複製できます。任意のコードは必要ありません。Copy の詳細については、第4章の 「スタックのみに置かれるデータ: Copy」 節を参照してください。
Copy トレイトは、プログラマーがそれらのメソッドをオーバーロードして、任意のコードが実行されていないという前提を破ることを防ぐために、いかなるメソッドも定義していません。そうすることで、すべてのプログラマーは、値のコピーが非常に高速であると想定できます。
構成要素がすべて Copy を実装している任意の型に対して、Copy を導出できます。Copy を実装する型は、Copy と同じ処理を行う自明な Clone 実装を持つため、Clone も実装していなければなりません。
Copy トレイトが必要になることはまれです。Copy を実装する型では最適化を利用できるため、clone を呼び出す必要がなくなり、コードをより簡潔にできます。
Copy で可能なことはすべて Clone でも実現できますが、コードが遅くなったり、ところどころで clone を使わなければならなくなったりする可能性があります。
値を固定サイズの値に対応付けるための Hash
Hash トレイトを使うと、任意のサイズの型のインスタンスを受け取り、ハッシュ関数を使ってそのインスタンスを固定サイズの値に対応付けることができます。Hash を導出すると、hash メソッドが実装されます。導出された hash メソッドの実装は、その型の各部分に対して hash を呼び出した結果を組み合わせるため、Hash を導出するには、すべてのフィールドまたは値も Hash を実装していなければなりません。
Hash が必要になる例としては、データを効率的に格納するために、HashMap<K, V> にキーを格納する場合があります。
デフォルト値のための Default
Default トレイトを使うと、型のデフォルト値を作成できます。Default を導出すると、default 関数が実装されます。導出された default 関数の実装は、その型の各部分に対して default 関数を呼び出すため、Default を導出するには、その型のすべてのフィールドまたは値も Default を実装していなければなりません。
Default::default 関数は、第5章の 「Struct Update
Syntax を使って他のインスタンスからインスタンスを生成する」 節で説明した構造体更新構文と組み合わせてよく使われます。構造体のいくつかのフィールドをカスタマイズし、残りのフィールドには ..Default::default() を使ってデフォルト値を設定して利用できます。
たとえば、Option<T> のインスタンスに対して unwrap_or_default メソッドを使う場合には、Default トレイトが必要です。Option<T> が None の場合、unwrap_or_default メソッドは Option<T> に格納されている型 T に対する Default::default の結果を返します。
D - 便利な開発ツール
付録D: 便利な開発ツール
この付録では、Rust プロジェクトが提供している便利な開発ツールをいくつか取り上げます。自動フォーマット、警告の修正をすばやく適用する方法、リンター、そして IDE との統合について見ていきます。
rustfmt による自動フォーマット
rustfmt ツールは、コミュニティのコードスタイルに従ってコードを再フォーマットします。多くの共同開発プロジェクトでは、Rust を書くときにどのスタイルを使うかでもめないように rustfmt を使っています。全員がこのツールを使ってコードをフォーマットするからです。
Rust のインストールにはデフォルトで rustfmt が含まれているので、システムにはすでに rustfmt と cargo-fmt のプログラムがあるはずです。この 2 つのコマンドは rustc と cargo に対応するもので、rustfmt はより細かな制御を可能にし、cargo-fmt は Cargo を使うプロジェクトの慣習を理解しています。任意の Cargo プロジェクトをフォーマットするには、次を入力します。
$ cargo fmt
このコマンドを実行すると、現在のクレート内のすべての Rust コードが再フォーマットされます。変更されるのはコードのスタイルだけであり、コードの意味は変わらないはずです。rustfmt の詳細については、そのドキュメントを参照してください。
rustfix でコードを修正する
rustfix ツールは Rust のインストールに含まれており、問題の修正方法が明確で、その修正がおそらく望ましいコンパイラ警告を自動的に修正できます。これまでにコンパイラの警告を見たことがあるでしょう。たとえば、次のコードを考えてみましょう。
ファイル名: src/main.rs
fn main() {
let mut x = 42;
println!("{x}");
}
ここでは、変数 x を可変として定義していますが、実際には一度も変更していません。そのことについて Rust は警告を出します。
$ cargo build
Compiling myprogram v0.1.0 (file:///projects/myprogram)
warning: variable does not need to be mutable
--> src/main.rs:2:9
|
2 | let mut x = 0;
| ----^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` on by default
この警告は、mut キーワードを削除するよう提案しています。cargo fix コマンドを実行すれば、rustfix ツールを使ってその提案を自動的に適用できます。
$ cargo fix
Checking myprogram v0.1.0 (file:///projects/myprogram)
Fixing src/main.rs (1 fix)
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
もう一度 src/main.rs を見ると、cargo fix がコードを変更したことがわかります。
ファイル名: src/main.rs
fn main() {
let x = 42;
println!("{x}");
}
変数 x は不変になり、その警告はもう表示されません。
cargo fix コマンドは、コードを異なる Rust エディション間で移行するためにも使えます。エディションについては 付録E で扱います。
Clippy による追加の lint
Clippy ツールは、コードを解析して一般的なミスを見つけ、Rust コードを改善するための lint 集です。Clippy は標準的な Rust のインストールに含まれています。
任意の Cargo プロジェクトで Clippy の lint を実行するには、次を入力します。
$ cargo clippy
たとえば、このプログラムのように、円周率のような数学定数の近似値を使うプログラムを書いたとしましょう。
fn main() {
let x = 3.1415;
let r = 8.0;
println!("the area of the circle is {}", x * r * r);
}
このプロジェクトで cargo clippy を実行すると、次のエラーになります。
error: approximate value of `f{32, 64}::consts::PI` found
--> src/main.rs:2:13
|
2 | let x = 3.1415;
| ^^^^^^
|
= note: `#[deny(clippy::approx_constant)]` on by default
= help: consider using the constant directly
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
このエラーは、Rust にはすでにより正確な PI 定数が定義されており、その定数を使ったほうがプログラムはより正確になることを教えてくれます。その場合、コードを PI 定数を使うように変更します。
次のコードでは、Clippy からエラーも警告も出ません。
fn main() {
let x = std::f64::consts::PI;
let r = 8.0;
println!("the area of the circle is {}", x * r * r);
}
Clippy の詳細については、そのドキュメントを参照してください。
rust-analyzer を使った IDE 統合
IDE との統合を支援するために、Rust コミュニティは rust-analyzer の使用を推奨しています。このツールは、Language Server Protocol を話す、コンパイラ中心のユーティリティ群です。これは、IDE とプログラミング言語が相互に通信するための仕様です。rust-analyzer は、Visual Studio Code 用 Rust analyzer プラグイン など、さまざまなクライアントから利用できます。
rust-analyzer プロジェクトの ホームページ
にアクセスしてインストール手順を確認し、その後、使っている IDE に言語サーバーのサポートをインストールしてください。IDE では、自動補完、定義へのジャンプ、インラインエラーなどの機能が使えるようになります。
E - エディション
付録E: エディション
第1章では、cargo new がエディションに関する少しのメタデータを Cargo.toml ファイルに追加することを見ました。この付録では、それが何を意味するのかを説明します!
Rust 言語とコンパイラは6週間のリリースサイクルを採用しており、つまりユーザーには新機能が絶えず提供されます。ほかのプログラミング言語では、より大きな変更をより低い頻度でリリースすることがありますが、Rust はより小さな更新をより頻繁にリリースします。しばらくすると、これらの小さな変更がすべて積み重なっていきます。しかし、リリースからリリースへと見ていくと、「すごい、Rust 1.10 から Rust 1.31 の間に Rust は大きく変わった!」と振り返って言うのは難しいことがあります。
およそ3年ごとに、Rust チームは新しい Rust の edition を作成します。各エディションでは、それまでに導入された機能が、完全に更新されたドキュメントやツール群とともに、分かりやすいひとまとまりのパッケージとしてまとめられます。新しいエディションは、通常の6週間ごとのリリースプロセスの一部として提供されます。
エディションは、人によって異なる目的を果たします。
- Rust を日常的に使っているユーザーにとって、新しいエディションは段階的な変更を理解しやすいパッケージとしてまとめたものです。
- まだ使っていない人にとって、新しいエディションは何らかの大きな進歩がもたらされたことを示すものであり、Rust を改めて検討する価値があるかもしれないという合図になります。
- Rust を開発している人にとって、新しいエディションはプロジェクト全体の結集点を提供します。
本書の執筆時点では、4つの Rust エディションが利用可能です: Rust 2015、Rust 2018、Rust 2021、Rust 2024。本書は Rust 2024 エディションのイディオムを用いて書かれています。
Cargo.toml の edition キーは、コンパイラがあなたのコードに対してどのエディションを使うべきかを示します。このキーが存在しない場合、Rust は後方互換性の理由から 2015 をエディション値として使用します。
各プロジェクトは、デフォルトの 2015 エディション以外のエディションを選択できます。エディションには、コード中の識別子と衝突する新しいキーワードの追加など、互換性のない変更が含まれることがあります。しかし、それらの変更を明示的に選択しない限り、使用している Rust コンパイラのバージョンをアップグレードしても、あなたのコードは引き続きコンパイルできます。
すべての Rust コンパイラのバージョンは、そのコンパイラのリリース以前に存在していたあらゆるエディションをサポートしており、サポート対象の任意のエディションの crate 同士を一緒にリンクできます。エディションの変更は、コンパイラが最初にコードを解析する方法にのみ影響します。したがって、あなたが Rust 2015 を使っていて、依存関係の1つが Rust 2018 を使っている場合でも、あなたのプロジェクトはコンパイルでき、その依存関係を利用できます。逆の状況、つまりあなたのプロジェクトが Rust 2018 を使い、依存関係が Rust 2015 を使っている場合でも、同様に機能します。
明確にしておくと、ほとんどの機能はすべてのエディションで利用可能です。どの Rust エディションを使っている開発者も、新しい安定版リリースが行われるたびに改善を引き続き享受できます。ただし、一部のケースでは、主に新しいキーワードが追加されるときに、新機能の一部が後のエディションでのみ利用可能になることがあります。そのような機能を活用したい場合は、エディションを切り替える必要があります。
詳しくは、Rust エディションガイド を参照してください。これは、エディション間の違いを列挙し、cargo fix を使ってコードを新しいエディションに自動的にアップグレードする方法を説明した完全な書籍です。
F - 本書の翻訳
付録 F: 本書の翻訳
英語以外の言語のリソースです。ほとんどはまだ進行中です。協力したり、新しい翻訳について知らせたりするには Translations ラベル を参照してください!
- ポルトガル語 (BR)
- ポルトガル語 (PT)
- 簡体字中国語: KaiserY/trpl-zh-cn, gnu4cn/rust-lang-Zh_CN
- 繁体字中国語
- ウクライナ語
- スペイン語, 別版, RustLangES によるスペイン語
- ロシア語
- 韓国語
- 日本語
- フランス語
- ポーランド語
- セブアノ語
- タガログ語
- エスペラント
- ギリシャ語
- スウェーデン語
- ファルシ, ペルシア語 (FA)
- ドイツ語
- ヒンディー語
- タイ語
- デンマーク語
- ウズベク語
- ベトナム語
- イタリア語
- ベンガル語
G - Rustはどのように作られるかと「Nightly Rust」
付録 G - Rust の作られ方と「Nightly Rust」
この付録では、Rust がどのように作られているのか、そしてそれが Rust 開発者であるあなたにどのような影響を与えるのかを扱います。
停滞なき安定性
Rust は言語として、あなたのコードの安定性を_非常に_重視しています。私たちは、 Rust が安心してその上に構築できる盤石な基盤であってほしいと考えています。もし 物事が絶えず変わっていたら、それは不可能でしょう。同時に、新機能を試験できなければ、 重要な欠陥がリリース後、つまりもう変更できなくなってからでないと 見つからないかもしれません。
この問題に対する私たちの解決策が、「停滞なき安定性」と呼ぶものです。 そして、私たちの指針は次のとおりです。stable Rust の新しい バージョンへのアップグレードを恐れる必要は決してない、ということです。 アップグレードのたびに苦痛がないだけでなく、新機能、より少ないバグ、 そしてより高速なコンパイル時間ももたらされるべきです。
シュッシュッ! リリースチャネルと列車に乗ること
Rust の開発は_列車ダイヤ方式_で進められています。つまり、すべての開発は Rust リポジトリの main ブランチで行われます。リリースは、Cisco IOS やほかのソフトウェア プロジェクトでも使われてきた、ソフトウェアのリリーストレインモデルに従います。 Rust には 3 つの_リリースチャネル_があります。
- Nightly
- Beta
- Stable
ほとんどの Rust 開発者は主に stable チャネルを使いますが、実験的な新機能を 試したい人は nightly や beta を使うことがあります。
開発とリリースのプロセスがどのように機能するか、例を見てみましょう。Rust チームが Rust 1.5 のリリースに取り組んでいると仮定します。そのリリースは 2015 年 12 月に実際に行われましたが、現実的なバージョン番号の例として使えます。 Rust に新機能が追加されると、新しいコミットが main ブランチに入ります。毎晩、新しい nightly 版の Rust が作成されます。毎日が リリース日であり、これらのリリースは私たちのリリース基盤によって自動的に 作成されます。したがって、時間がたつにつれて、リリースは毎晩次のようになります。
nightly: * - - * - - *
6 週間ごとに、新しいリリースを準備する時期が来ます! Rust リポジトリの
beta ブランチは、nightly が使っている main ブランチから分岐します。これで、
リリースは 2 つになります。
nightly: * - - * - - *
|
beta: *
ほとんどの Rust ユーザーは beta リリースを積極的には使いませんが、 CI システムで beta に対してテストを行い、Rust が起こりうるリグレッションを 見つけるのを助けます。その間も、nightly のリリースは毎晩続きます。
nightly: * - - * - - * - - * - - *
|
beta: *
リグレッションが見つかったとしましょう。リグレッションが stable
リリースに紛れ込む前に beta リリースをテストする時間があって助かりました! 修正は
main ブランチに適用され、nightly が直されます。その後、その修正は
beta ブランチにもバックポートされ、新しい beta リリースが作成されます。
nightly: * - - * - - * - - * - - * - - *
|
beta: * - - - - - - - - *
最初の beta が作成されてから 6 週間後、stable リリースの時期になります! stable
ブランチは beta ブランチから作成されます。
nightly: * - - * - - * - - * - - * - - * - * - *
|
beta: * - - - - - - - - *
|
stable: *
やった! Rust 1.5 の完成です! しかし、1 つ忘れていました。6
週間が経過したので、Rust の_次の_バージョンである 1.6 の新しい beta も必要です。
そこで、stable が beta から分岐したあと、次のバージョンの beta が
再び nightly から分岐します。
nightly: * - - * - - * - - * - - * - - * - * - *
| |
beta: * - - - - - - - - * *
|
stable: *
これが「列車モデル」と呼ばれるのは、6 週間ごとに 1 つのリリースが「駅を出発」 するものの、stable リリースとして到着する前に、beta チャネルを通る旅をしなければならないからです。
Rust は時計仕掛けのように、6 週間ごとにリリースされます。ある Rust の リリース日を知っていれば、次のリリース日もわかります。6 週間後だからです。リリースが 6 週間ごとに予定されていることのよい点は、次の列車がすぐに来ることです。 ある機能がたまたま特定のリリースに間に合わなくても、心配する必要は ありません。次のものがすぐにやってくるからです! これにより、十分に磨かれていない かもしれない機能をリリース期限直前に滑り込ませようとするプレッシャーを 減らせます。
このプロセスのおかげで、いつでも Rust の次のビルドを試し、
アップグレードが容易であることを自分で確認できます。beta リリースが期待どおりに
動かなければ、チームに報告し、次の stable
リリースが行われる前に修正してもらえます! beta
リリースで何かが壊れることは比較的まれですが、rustc も依然として
ソフトウェアであり、バグは実際に存在します。
メンテナンス期間
Rust プロジェクトは、最新の stable バージョンをサポートします。新しい stable バージョンがリリースされると、古いバージョンはサポート終了(EOL)を迎えます。これは 各バージョンが 6 週間サポートされることを意味します。
不安定な機能
このリリースモデルには、もう 1 つ注意点があります。それが不安定な機能です。Rust は あるリリースでどの機能を有効にするかを決めるために、「フィーチャーフラグ」と 呼ばれる手法を使います。新機能が活発に開発中であれば、それは main ブランチに入り、したがって nightly にも入りますが、フィーチャーフラグ の背後に 置かれます。ユーザーとして開発途中の機能を試したければできますが、そのためには nightly リリースの Rust を使い、オプトインするために適切なフラグを ソースコードに付けなければなりません。
beta または stable リリースの Rust を使っている場合、フィーチャーフラグは一切 使えません。これこそが、新機能を永久に stable だと宣言する前に、 実際に使ってみられるようにする鍵です。最先端にオプトインしたい人は そうできますし、盤石な体験を望む人は stable にとどまり、 自分のコードが壊れないとわかります。停滞なき安定性です。
本書には stable な機能に関する情報しか含まれていません。開発中の 機能はまだ変化しており、この本が書かれた時点と、それらが stable ビルドで有効になる時点とでは、きっと異なっているからです。nightly 専用機能の ドキュメントはオンラインで見つけられます。
Rustup と Rust Nightly の役割
Rustup を使うと、Rust の異なるリリースチャネルをグローバルまたは プロジェクトごとに簡単に切り替えられます。デフォルトでは stable Rust が インストールされています。たとえば nightly をインストールするには、次のようにします。
$ rustup toolchain install nightly
rustup でインストールしたすべての_ツールチェーン_(Rust のリリースと関連
コンポーネント)も確認できます。以下は、著者の 1 人の Windows コンピューターでの例です。
> rustup toolchain list
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc
ご覧のとおり、stable ツールチェーンがデフォルトです。ほとんどの Rust ユーザーは、ほとんどの時間 stable を使います。たいていは stable を使いたいが、最先端の機能を使いたいために、特定のプロジェクトでは nightly を使いたいということもあるでしょう。そのためには、そのプロジェクトのディレクトリで rustup override を使って、そのディレクトリにいるときに rustup が使うべきツールチェーンとして nightly を設定できます。
$ cd ~/projects/needs-nightly
$ rustup override set nightly
これで、~/projects/needs-nightly の中で rustc または cargo を呼び出すたびに、rustup は stable Rust をデフォルトにしていても、stable Rust ではなく nightly Rust を使っていることを保証してくれます。Rust のプロジェクトをたくさん持っていると、これはとても便利です!
RFC プロセスとチーム
では、こうした新機能についてはどうやって知るのでしょうか。Rust の開発モデルは、Request For Comments(RFC)プロセス に従っています。Rust に改善を加えたい場合は、RFC と呼ばれる提案を書くことができます。
Rust を改善するための RFC は誰でも書くことができ、それらの提案は、多くの分野別サブチームで構成される Rust チームによってレビューされ、議論されます。Rust の Web サイト にはチームの完全な一覧があり、そこにはプロジェクトの各分野、つまり言語設計、コンパイラ実装、インフラストラクチャ、ドキュメントなどのチームが含まれています。適切なチームが提案とコメントを読み、自分たちでもコメントを書き、最終的にその機能を受け入れるか拒否するかについて合意が形成されます。
機能が受け入れられると、Rust リポジトリに issue が作成され、誰かがその実装を行えます。それを実装する人は、最初にその機能を提案した人とは限りません! 実装の準備が整うと、「不安定な機能」 セクションで説明したように、機能ゲートの背後に置かれた状態で main ブランチに入ります。
しばらくすると、nightly リリースを使う Rust 開発者が新しい機能を試せるようになり、チームメンバーはその機能と、nightly 上でそれがどう機能したかを議論し、それを stable Rust に入れるべきかどうかを決定します。前に進めると決まれば、機能ゲートは取り除かれ、その機能は stable と見なされるようになります! その機能は train に乗って、Rust の新しい stable リリースへと入ります。