The Rust Programming Language 日本語版
著:Steve Klabnik、Carol Nichols、貢献:Rustコミュニティ
このテキストのこの版ではRust 1.58(2022年1月13日リリース)かそれ以降が使われていることを前提にしています。 Rustをインストールしたりアップデートしたりするには第1章の「インストール」節を読んでください。
HTML版はhttps://doc.rust-lang.org/stable/book/で公開されています。
オフラインのときは、rustupでインストールしたRustを使ってrustup docs --bookで開けます。
訳注:日本語のHTML版はhttps://doc.rust-jp.rs/book-ja/で公開されています。
rustupを使ってオフラインで読むことはできません。
また、コミュニティによるいくつかの翻訳版もあります。
このテキストの(英語版の)ペーパーバック版と電子書籍版はNo Starch出版から発売されています。
まえがき
すぐにはわかりにくいかもしれませんが、Rustプログラミング言語は、エンパワーメント(empowerment)を根本原理としています: どんな種類のコードを現在書いているにせよ、Rustは幅広い領域で以前よりも遠くへ到達し、 自信を持ってプログラムを組む力を与え(empower)ます。
一例を挙げると、メモリ管理やデータ表現、並行性などの低レベルな詳細を扱う「システムレベル」のプログラミングがあります。 伝統的にこの分野は難解で、年月をかけてやっかいな落とし穴を回避する術を習得した選ばれし者にだけ可能と見なされています。 そのように鍛錬を積んだ者でさえ注意が必要で、さもないと書いたコードがクラッキングの糸口になったりクラッシュやデータ破損を引き起こしかねないのです。
この難しさを取り除くために、Rustは、古い落とし穴を排除し、その過程で使いやすく役に立つ洗練された一連のツールを提供します。 低レベルな制御に「下がる」必要があるプログラマは、お決まりのクラッシュやセキュリティホールのリスクを負わず、 気まぐれなツールチェーンのデリケートな部分を学ぶ必要なくRustで同じことができます。さらにいいことに、 Rustは、スピードとメモリ使用の観点で効率的な信頼性の高いコードへと自然に導くよう設計されています。
既に低レベルコードに取り組んでいるプログラマは、Rustを使用してさらなる高みを目指せます。例えば、 Rustで並列性を導入することは、比較的低リスクです: コンパイラが伝統的なミスを捕捉してくれるのです。 そして、クラッシュや脆弱性の糸口を誤って導入しないという自信を持ってコードの大胆な最適化に取り組めるのです。
ですが、Rustは低レベルなシステムプログラミングに限定されているわけではありません。十分に表現力豊かでエルゴノミックなので、 コマンドラインアプリやWebサーバ、その他様々な楽しいコードを書けます。この本の後半に両者の単純な例が見つかるでしょう。 Rustを使うことで1つの領域から他の領域へと使い回せる技術を身につけられます; ウェブアプリを書いてRustを学び、それからその同じ技術をラズベリーパイを対象に適用できるのです。
この本は、ユーザに力を与え(empower)るRustのポテンシャルを全て含んでいます。あなたのRustの知識のみをレベルアップさせるだけでなく、 プログラマとしての全般的な能力や自信をもレベルアップさせる手助けを意図した親しみやすくわかりやすいテキストです。 さあ、飛び込んで学ぶ準備をしてください。Rustコミュニティへようこそ!
- ニコラス・マットサキス(Nicholas Matsakis)とアーロン・チューロン(Aaron Turon)
はじめに
注釈: この本のこの版は、本として利用可能なThe Rust Programming Languageと、 No Starch Pressのebook形式と同じです。
The Rust Programming Languageへようこそ。Rustに関する入門書です。
Rustプログラミング言語は、高速で信頼できるソフトウェアを書く手助けをしてくれます。
高レベルのエルゴノミクス(訳注: ergonomicsとは、人間工学的という意味。砕いて言えば、人間に優しいということ)と低レベルの制御は、
しばしばプログラミング言語の設計においてトレードオフの関係になります;
Rustは、その衝突に挑戦しています。バランスのとれた強力な技術の許容量と素晴らしい開発者経験を通して、
Rustは伝統的にそれらの制御と紐付いていた困難全てなしに低レベルの詳細(メモリ使用など)を制御する選択肢を与えてくれます。
Rustは誰のためのものなの
Rustは、様々な理由により多くの人にとって理想的です。いくつか最も重要なグループを見ていきましょう。
開発者チーム
Rustは、いろんなレベルのシステムプログラミングの知識を持つ開発者の巨大なチームとコラボするのに生産的なツールであると証明してきています。 低レベルコードは様々な種類の微細なバグを抱える傾向があり、そのようなバグは他の言語だと広範なテストと、 経験豊富な開発者による注意深いコードレビューによってのみ捕捉されるものです。Rustにおいては、 コンパイラが並行性のバグも含めたこのようなとらえどころのないバグのあるコードをコンパイルするのを拒むことで、 門番の役割を担います。コンパイラとともに取り組むことで、チームはバグを追いかけるよりもプログラムのロジックに集中することに、 時間を費やせるのです。
Rustはまた、現代的な開発ツールをシステムプログラミング世界に導入します。
- Cargoは、付属の依存関係管理ツール兼ビルドツールで、依存関係の追加、コンパイル、管理を容易にし、Rustのエコシステム全体で一貫性を持たせます。
- Rustfmtフォーマットツールは開発者の間で一貫したコーディングスタイルを保証します。
- Rust言語サーバーは、IDE(統合開発環境)との統合により、コード補完やインラインエラーメッセージに対応しています。
これらのツールやRustのエコシステムの他のツールを使用することで、開発者はシステムレベルのコードを書きながら生産性を高めることができます。
学生
Rustは、学生やシステムの概念を学ぶことに興味のある方向けです。Rustを使用して、 多くの人がOS開発などの話題を学んできました。コミュニティはとても暖かく、喜んで学生の質問に答えてくれます。 この本のような努力を通じて、Rustチームはシステムの概念を多くの人、特にプログラミング初心者にとってアクセス可能にしたいと考えています。
企業
数百の企業が、大企業、中小企業を問わず、様々なタスクにプロダクションでRustを使用しています。 そのタスクには、コマンドラインツール、Webサービス、DevOpsツール、組み込みデバイス、 オーディオとビデオの解析および変換、暗号通貨、生物情報学、サーチエンジン、IoTアプリケーション、 機械学習、Firefoxウェブブラウザの主要部分さえ含まれます。
オープンソース開発者
Rustは、Rustプログラミング言語やコミュニティ、開発者ツール、ライブラリを開発したい方向けです。 あなたがRust言語に貢献されることを心よりお待ちしております。
スピードと安定性に価値を見出す方
Rustは、スピードと安定性を言語に渇望する方向けです。ここでいうスピードとは、 Rustコードの実行速度とプログラムを書くスピードのことです。Rustコンパイラのチェックにより、 機能の追加とリファクタリングを通して安定性を保証してくれます。これはこのようなチェックがない言語の脆いレガシーコードとは対照的で、 その場合開発者はしばしば、変更するのを恐れてしまいます。ゼロコスト抽象化を志向し、 手で書いたコードと同等の速度を誇る低レベルコードにコンパイルされる高レベル機能により、 Rustは安全なコードを高速なコードにもしようと努力しています。
Rust言語は他の多くのユーザのサポートも望んでいます; ここで名前を出した方は、 ただの最大の出資者の一部です。総合すると、Rustの最大の野望は、プログラマが数十年間受け入れてきた代償を、安全性と生産性、 スピードとエルゴノミクスを提供することで排除することです。Rustを試してみて、その選択が自分に合っているか確かめてください。
この本は誰のためのものなの
この本は、あなたが他のプログラミング言語でコードを書いたことがあることを想定していますが、 具体的にどの言語かという想定はしません。私たちは、幅広い分野のプログラミング背景からの人にとってこの資料を広くアクセスできるようにしようとしてきました。 プログラミングとはなんなのかやそれについて考える方法について多くを語るつもりはありません。 もし、完全なプログラミング初心者であれば、プログラミング入門を特に行う本を読むことでよりよく役に立つでしょう。
この本の使い方
一般的に、この本は、順番に読み進めていくことを前提にしています。後の章は、前の章の概念の上に成り立ち、 前の章では、特定の話題にさほど深入りしない可能性がありますが、後ほどの章で同じ話題を再検討するでしょう。
この本には2種類の章があるとわかるでしょう: 概念の章とプロジェクトの章です。概念の章では、 Rustの一面を学ぶでしょう。プロジェクトの章では、それまでに学んだことを適用して一緒に小さなプログラムを構築します。 2、12、20章がプロジェクトの章です。つまり、残りは概念の章です。
第1章はRustのインストール方法、“Hello, world!”プログラムの書き方、Rustのパッケージマネージャ兼、 ビルドツールのCargoの使用方法を説明します。第2章は、数当てゲームを作りながら、実際にRustでのプログラミングをやってもらう導入です。 ここでは概念をざっくりと講義し、後ほどの章で追加の詳細を提供します。 今すぐRustの世界に飛び込みたいなら、第2章こそがそのためのものです。第3章は他のプログラミング言語の機能に似たRustの機能を講義し、 第4章ではRustの所有権システムについて学びます。 あなたが次に進む前に全ての詳細を学ぶことを好む特別に几帳面な学習者なら、 第2章を飛ばして真っ先に第3章に行き、学んだ詳細を適用するプロジェクトに取り組みたくなった時に第2章に戻りたくなる可能性があります。
第5章は、構造体とメソッドについて議論し、第6章はenum、match式、if let制御フロー構文を講義します。
構造体とenumを使用してRustにおいて独自の型を作成します。
第7章では、Rustのモジュールシステムと自分のコードとその公開されたAPI(Application Programming Interface)を体系化するプライバシー規則について学びます。 第8章では、ベクタ、文字列、ハッシュマップなどの標準ライブラリが提供する一般的なコレクションデータ構造の一部を議論します。 第9章では、Rustのエラー処理哲学とテクニックを探究します。
第10章ではジェネリクス、トレイト、ライフタイムについて深入りし、これらは複数の型に適用されるコードを定義する力をくれます。
第11章は、完全にテストに関してで、Rustの安全性保証があってさえ、プログラムのロジックが正しいことを保証するために、
必要になります。第12章では、ファイル内のテキストを検索するgrepコマンドラインツールの一部の機能を自身で構築します。
このために、以前の章で議論した多くの概念を使用します。
第13章はクロージャとイテレータを探究します。これらは、関数型プログラミング言語由来のRustの機能です。 第14章では、Cargoをより詳しく調査し、他人と自分のライブラリを共有する最善の策について語ります。 第15章では、標準ライブラリが提供するスマートポインタとその機能を可能にするトレイトを議論します。
第16章では、並行プログラミングの異なるモデルを見ていき、Rustが恐れなしに複数のスレッドでプログラムする手助けをする方法を語ります。 第17章では、馴染み深い可能性のあるオブジェクト指向プログラミングの原則とRustのイディオムがどう比較されるかに目を向けます。
第18章は、パターンとパターンマッチングのリファレンスであり、これらはRustプログラムを通して、
考えを表現する強力な方法になります。第19章は、unsafe Rustやマクロ、ライフタイム、トレイト、型、関数、クロージャの詳細を含む、
興味のある高度な話題のスモーガスボード(訳注: 日本でいうバイキングのこと)を含みます。
第20章では、低レベルなマルチスレッドのWebサーバを実装するプロジェクトを完成させます!
最後に、言語についての有用な情報をよりリファレンスのような形式で含む付録があります。 付録AはRustのキーワードを講義し、付録Bは、Rustの演算子と記号、付録Cは、 標準ライブラリが提供する導出可能なトレイト、付録Dはいくつか便利な開発ツールを講義し、 付録EではRustのエディションについて説明します。付録Fではこの本の翻訳を見つけることができ、 付録GではRustの作られ方、そしてnightly Rustとは何かについて講義します。
この本を読む間違った方法なんてありません: 飛ばしたければ、どうぞご自由に! 混乱したら、前の章に戻らなければならない可能性もあります。ですが、自分に合った方法でどうぞ。
Rustを学ぶ過程で重要な部分は、コンパイラが表示するエラーメッセージを読む方法を学ぶことです: それは動くコードへと導いてくれます。そのため、各場面でコンパイラが表示するエラーメッセージとともに、 コンパイルできない例を多く提供します。適当に例を選んで走らせたら、コンパイルできないかもしれないことを知ってください! 周りのテキストを読んで実行しようとしている例がエラーになることを意図しているのか確認することを確かめてください。 フェリスもコードが動作するとは意図されていないコードを見分けるのを手助けしてくれます:
| Ferris | Meaning |
|---|---|
| このコードはコンパイルできません! | |
| このコードはパニックします! | |
| このコードは求められている振る舞いをしません。 |
ほとんどの場合、コンパイルできないあらゆるコードの正しいバージョンへと導きます。
ソースコード
この本が生成されるソースファイルは、GitHubで見つかります。
訳注: 日本語版はこちらです。
事始め
Rustの旅を始めましょう!学ぶべきことはたくさんありますが、いかなる旅もどこかから始まります。 この章では、以下のことを説明します:
- RustをLinux、macOS、Windowsにインストールする
Hello, world!と表示するプログラムを書くcargoというRustのパッケージマネージャ兼ビルドシステムを使用する
インストール
最初の手順は、Rustをインストールすることです。Rustは、Rustのバージョンと関連するツールを管理する、rustupというコマンドラインツールを使用してダウンロードします。ダウンロードには、インターネットへの接続が必要になります。
注釈: なんらかの理由で
rustupを使用したくない場合、Other Rust Installation Methods ページで、 他の選択肢をご覧になってください。
以下の手順で最新の安定版のRustコンパイラをインストールします。 Rustは安定性 (stability) を保証しているので、現在この本の例でコンパイルできるものは、新しいバージョンになってもコンパイルでき続けることが保証されます。 出力は、バージョンによって多少異なる可能性があります。Rustは頻繁にエラーメッセージと警告を改善しているからです。 言い換えると、どんな新しいバージョンでもこの手順に従ってインストールした安定版なら、 この本の内容で想定通りに動くはずです。
コマンドラインの記法
この章及び、本を通して、端末で使用するなんらかのコマンドを示すことがあります。読者が入力するべき行は、 全て
$で始まります。ただし、読者が$文字を入力する必要はありません; これは各コマンドの開始を示すために表示しているコマンドラインプロンプトです。$で始まらない行は、典型的には直前のコマンドの出力を示します。また、PowerShell限定の例には、$ではなく、>を使用します。
LinuxとmacOSにrustupをインストールする
LinuxかmacOSを使用しているなら、端末(ターミナル)を開き、以下のコマンドを入力してください:
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
このコマンドはスクリプトをダウンロードし、rustupツールのインストールを開始し、Rustの最新の安定版をインストールします。
パスワードを求められる可能性があります。インストールがうまく行けば、以下の行が出現するでしょう:
Rust is installed now. Great!
リンカも必要になるでしょう。 リンカは、コンパイルされた出力をひとつのファイルに合体させるためにRustが使用するプログラムです。 リンカが既にインストールされている可能性は高いでしょう。 リンカエラーが発生したときは、Cコンパイラは典型的にリンカを含んでいるでしょうから、Cコンパイラをインストールすべきです。 一般的なRustパッケージの中には、Cコードに依存し、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 2013以降用のMSVCビルドツールも必要になるという旨のメッセージが出るでしょう。
ビルドツールを取得するには、Visual Studio 2022をインストールする必要があるでしょう。 どのワークロード (workloads) をインストールするかと質問されたときは、以下を含めてください:
- 「C++によるデスクトップ開発」(“Desktop Development with C++”)
- Windows 10または11のSDK
- 英語の言語パック (English language pack) コンポーネント (お好みで他の任意の言語パックも)
訳注:Windowsの言語を日本語にしている場合は言語パックのところで「日本語」が選択されており、そのままの設定でインストールしても基本的に問題ないはずです。しかし、サードパーティーのツールやライブラリの中には英語の言語パックを必要とするものがあるため、「日本語」に加えて「英語」も選択することをお勧めします。
これ以降、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(Rustユーザが自分たちのことを呼ぶ、冗談めいたニックネーム)たちと交流する方法をコミュニティページで探してください。
訳注1:Rustaceanについて、いらないかもしれない補足です。公式Twitter曰く、Rustaceanはcrustaceans(甲殻類)から来ているそうです。 そのため、Rustのマスコットは(非公式らしいですが)カニ。上の会話でCの欠点を削ぎ落としているからcを省いてるの?みたいなことを聞いていますが、 違うそうです。検索したら、堅牢性が高いから甲殻類という意見もありますが、真偽は不明です。 明日使えるかもしれないトリビアでした。
訳注2:上にあるコミュニティページはどれも英語話者のコミュニティへのリンク集です。日本語話者のためのコミュニティがZulip rust-lang-jpにあり、こちらでもRustaceanたちが活発に議論をしています。 公式Discord同様、初心者向けの#beginnersチャンネルが存在するので、気軽に質問してみてください。
更新及びアンインストール
rustup経由でRustがインストールされたなら、新しくリリースされた版へ更新するのは簡単です。
シェルから以下の更新スクリプトを実行してください:
$ rustup update
Rustとrustupをアンインストールするには、シェルから以下のアンインストールスクリプトを実行してください:
$ rustup self uninstall
ローカルのドキュメンテーション
インストールされたRustには、オフラインでドキュメンテーションを閲覧できるように、ドキュメンテーションのローカルコピーが含まれています。
ブラウザでローカルのドキュメンテーションを開くには、rustup docを実行してください。
標準ライブラリにより提供される型や関数がなんなのかや、それをどう使えば良いのかがよくわからないときは、いつでもAPIのドキュメンテーションを検索してみてください!
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という拡張子で終わります。 ファイル名に2単語以上使っているなら、アンダースコアで区切るのが規約です。例えば、helloworld.rsではなく、 hello_world.rsを使用してください。
さて、作ったばかりのmain.rsファイルを開き、リスト1-1のコードを入力してください。
ファイル名: main.rs
fn main() { // 世界よ、こんにちは println!("Hello, world!"); }
リスト1-1: Hello, world!と出力するプログラム
ファイルを保存し、~/projects/hello_worldディレクトリの端末ウィンドウに戻ってください。 LinuxかmacOSなら、以下のコマンドを打ってファイルをコンパイルし、実行してください:
$ rustc main.rs
$ ./main
Hello, world!
Windowsなら、./mainの代わりに.\main.exeと打ちます:
> rustc main.rs
> .\main.exe
Hello, world!
OSに関わらず、Hello, world!という文字列が端末に出力されるはずです。この出力が見れないなら、
インストールの節の「トラブルシューティング」の部分に立ち戻って、助けを得る方法を参照してください。
Hello, world!が確かに出力されたら、おめでとうございます!正式にRustプログラムを書きました。
Rustプログラマになったのです!ようこそ!
Rustプログラムの解剖
この「Hello, world!」プログラムを詳しく再確認しましょう。 こちらがパズルの最初のピースです:
fn main() { }
これらの行はmainという名前の関数を定義しています。main関数は特別です: 常に全ての実行可能なRustプログラムで走る最初のコードになります。
ここで、1行目は、引数がなく何も返さないmainという関数を宣言しています。引数があるなら、かっこ(())の内部に入ります。
関数の本体は{}に包まれます。Rustでは、全ての関数本体の周りに波括弧が必要になります。
スペースを1つあけて、開き波括弧を関数宣言と同じ行に配置するのがいいスタイルです。
注釈: 複数のRustプロジェクトに渡って標準的なスタイルにこだわりたいなら、
rustfmtを使うことでコードを決まったスタイルに整形できるでしょう(rustfmtの詳細は付録Dで)。 Rustチームは、rustcのように標準的なRustの配布にこのツールを含んでいるため、既にコンピューターにインストールされているはずです!
main関数の本体は、こんなコードを抱えています:
#![allow(unused)] fn main() { println!("Hello, world!"); }
この行が、この小さなプログラムの全作業をしています: テキストを画面に出力するのです。 ここで気付くべき重要な詳細が4つあります。
まず、Rustのスタイルは、タブではなく、4スペースでインデントするということです。
2番目にprintln!はRustのマクロを呼び出すということです。代わりに関数を呼んでいたら、
println(!なし)と入力されているでしょう。Rustのマクロについて詳しくは、第19章で議論します。
とりあえず、!を使用すると、普通の関数ではなくマクロを呼んでいるのだということと、マクロは関数と同じルールには必ずしも従わないということを知っておくだけでいいでしょう。
3番目に、"Hello, world!"文字列が見えます。この文字列を引数としてprintln!に渡し、
この文字列が画面に表示されているのです。
4番目にこの行をセミコロン(;)で終え、この式が終わり、次の式の準備ができていると示唆していることです。
Rustコードのほとんどの行は、セミコロンで終わります。
コンパイルと実行は個別のステップ
新しく作成したプログラムをちょうど実行したので、その途中の手順を調査しましょう。
Rustプログラムを実行する前に、以下のように、rustcコマンドを入力し、ソースファイルの名前を渡すことで、
Rustコンパイラを使用してコンパイルしなければなりません。
$ 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 %= the /B option says to only show the file names =%
%= /Bオプションは、ファイル名だけを表示することを宣言する =%
main.exe
main.pdb
main.rs
これは、.rs拡張子のソースコードファイル、実行可能ファイル(Windowsならmain.exe、他のプラットフォームでは、main)、 そして、Windowsを使用しているなら、.pdb拡張子のデバッグ情報を含むファイルを表示します。ここから、 mainかmain.exeを走らせます。このように:
$ ./main # or .\main.exe on Windows
# または、Widnowsなら.\main.exe
main.rsがHello, world!プログラムなら、この行はHello, world!と端末に出力します。
RubyやPython、JavaScriptなどの動的言語により造詣が深いなら、プログラムのコンパイルと実行を個別の手順で行うことに慣れていない可能性があります。
RustはAOTコンパイル(ahead-of-time; 訳注: 予め)言語です。つまり、プログラムをコンパイルし、
実行可能ファイルを誰かにあげ、あげた人がRustをインストールしていなくても実行できるわけです。
誰かに .rb、.py、.jsファイルをあげたら、それぞれRuby、Python、JavaScriptの処理系がインストールされている必要があります。
ですが、そのような言語では、プログラムをコンパイルし実行するには、1コマンドしか必要ないのです。
全ては言語設計においてトレードオフなのです。
簡単なプログラムならrustcでコンパイルするだけでも十分ですが、プロジェクトが肥大化してくると、
オプションを全て管理し、自分のコードを簡単に共有したくなるでしょう。次は、Cargoツールを紹介します。
これは、現実世界のRustプログラムを書く手助けをしてくれるでしょう。
Hello, Cargo!
CargoはRustのビルドシステム兼パッケージマネージャです。 ほとんどのRustaceanはこのツールを使ってRustプロジェクトを管理しています。 なぜなら、Cargoは多くの仕事、たとえばコードのビルド、コードが依存するライブラリのダウンロード、それらのライブラリのビルドなどを扱ってくれるからです。 (コードが必要とするライブラリのことを依存(dependencies)と呼びます)
いままでに書いたようなごく単純なRustプログラムには依存がありません。 「Hello, world!」プロジェクトをCargoでビルドしても、Cargoの中のコードをビルドする部分しか使わないでしょう。 より複雑なRustプログラムを書くようになると依存を追加することになりますが、Cargoを使ってプロジェクトを開始したなら、依存の追加もずっと簡単になります。
Rustプロジェクトの大多数がCargoを使用しているので、これ以降、この本では、あなたもCargoを使用していると想定します。 もし「インストール」節で紹介した公式のインストーラを使用したなら、CargoはRustと共にインストールされています。 Rustを他の方法でインストールした場合は、以下のコマンドをターミナルに入れて、Cargoがインストールされているか確認してください。
$ cargo --version
バージョンナンバーが表示されたならインストールされています!
command not foundなどのエラーが表示された場合は、自分がインストールした方法についてのドキュメントを参照して、Cargoを個別にインストールする方法を調べてください。
Cargoでプロジェクトを作成する
Cargoを使って新しいプロジェクトを作成し、元の「Hello, world!」プロジェクトとの違いを見ていきましょう。 projectsディレクトリ(または自分がコードを保存すると決めた場所)に戻ってください。 それから、OSに関係なく、以下を実行してください。
$ cargo new hello_cargo
$ cd hello_cargo
最初のコマンドはhello_cargoという名の新しいディレクトリとプロジェクトを作成します。 プロジェクトをhello_cargoと名付けたので、Cargoはそれに関連するいくつかのファイルを同名のディレクトリに作成します。
hello_cargoディレクトリに行き、ファイルの一覧を取得してください。 Cargoが2つのファイルと1つのディレクトリを生成してくれたことがわかるでしょう。 Cargo.tomlファイルとsrcディレクトリがあり、srcの中にはmain.rsファイルがあります。
また、.gitignoreファイルと共に新しいGitリポジトリも初期化されています。
もし、すでに存在するGitリポジトリの中でcargo newを実行したなら、Git関連のファイルは作られません。
cargo new --vcs=gitとすることで、この振る舞いを変更できます。
補足:Gitは一般的なバージョン管理システムです。
cargo newコマンドに--vcsフラグを与えることで、別のバージョン管理システムを使用したり、何も使用しないようにもできます。 利用可能なオプションを確認するにはcargo new --helpを実行します。
お気に入りのテキストエディタでCargo.tomlを開いてください。 リスト1-2のコードのようになっているはずです。
ファイル名:Cargo.toml
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
リスト1-2:cargo newで生成されたCargo.tomlの内容
このファイルはTOML(Tom's Obvious, Minimal Language、トムの明確な最小限の言語)形式で、Cargoの設定フォーマットです。
最初の行の[package]はセクションヘッダーで、それ以降の文がパッケージを設定することを示します。
このファイルに情報を追加してく中で、他のセクションも追加していくことになります。
次の3行はCargoがプログラムをコンパイルするのに必要となる設定情報を指定します。
ここでは、名前、バージョン、使用するRustのエディションを指定しています。
editionキーについては付録Eで説明されています。
最後の行の[dependencies]は、プロジェクトの依存を列挙するためのセクションの始まりです。
Rustではコードのパッケージのことをクレートと呼びます。
このプロジェクトでは他のクレートは必要ありませんが、第2章の最初のプロジェクトでは必要になるので、そのときにこの依存セクションを使用します。
では、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プロジェクトをビルドし、実行する
では「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 # or .\target\debug\hello_cargo.exe on Windows
# Windowsでは .\target\debug\hello_cargo.exe
Hello, world!
すべてがうまくいけば、ターミナルにHello, world!と表示されるはずです。
cargo buildを初めて実行したとき、Cargoは最上位にCargo.lockという新しいファイルを作成します。
このファイルはプロジェクト内の依存関係の正確なバージョンを記録しています。
このプロジェクトには依存がないので、このファイルの中は少しまばらです。
このファイルは手動で変更する必要はありません。
Cargoがその内容を管理してくれます。
先ほどはcargo buildでプロジェクトをビルドし、./target/debug/hello_cargoで実行しました。
cargo runを使うと、コードのコンパイルから、できた実行ファイルの実行までの全体を一つのコマンドで行えます。
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/hello_cargo`
Hello, world!
cargo buildを実行してから、バイナリへのパス全体を使って実行する、という手順をいちいち踏むより、
cargo runを使う方が便利なので、ほとんどの開発者はcargo runを使います。
今回はCargoがhello_cargoをコンパイルしていることを示す出力がないことに注目してください。
Cargoはファイルが変更されていないことに気づいたので、再ビルドせずに単にバイナリを実行したのです。
もしソースコードを変更していたら、Cargoは実行前にプロジェクトを再ビルドし、以下のような出力が表示されたことでしょう。
$ 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を使用するもう一つの利点は、どのOSで作業していてもコマンドが同じであることです。 そのため、これ以降はLinuxやmacOS向けの手順と、Windows向けの手順を分けて説明することはありません。
リリースに向けたビルド
プロジェクトが最終的にリリースできるようになったら、cargo build --releaseを使い、最適化した状態でコンパイルできます。
このコマンドは実行ファイルを、target/debugではなく、target/releaseに作成します。
最適化によってRustコードの実行速度が上がりますが、それを有効にすることでプログラムのコンパイルにかかる時間が長くなります。
このため二つの異なるプロファイルがあるのです。
一つは開発用で、素早く頻繁に再ビルドしたいときのもの。
もう一つはユーザに渡す最終的なプログラムをビルドするためのもので、繰り返し再ビルドすることはなく、可能な限り高速に動作するようにします。
コードの実行時間をベンチマークするなら、必ず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のバージョンに更新する
- ローカルにインストールされたドキュメントを開く
- 「Hello, world!」プログラムを書き、
rustcを直接使って実行する - Cargoにおける習慣に従った新しいプロジェクトを作成し、実行する
いまは、より中身のあるプログラムを構築し、Rustコードの読み書きに慣れるのに良いタイミングでしょう。 そこで第2章では、数当てゲームプログラムを構築します。 もし、一般的なプログラミングの概念がRustでどう実現されるか学ぶことから始めたいのであれば、第3章を読んで、それから第2章に戻ってください。
数当てゲームのプログラミング
ハンズオン形式のプロジェクトに一緒に取り組むことで、Rustの世界に飛び込んでみましょう!
この章ではRustの一般的な概念を、実際のプログラムでの使い方を示しながら紹介します。
let、match、メソッド、関連関数、外部クレートの使いかたなどについて学びます!
これらについての詳細は後続の章で取り上げますので、この章では基本的なところを練習します。
プログラミング初心者向けの定番問題である「数当てゲーム」を実装してみましょう。 これは次のように動作します。 プログラムは1から100までのランダムな整数を生成します。 そして、プレーヤーに予想(した数字)を入力するように促します。 予想が入力されると、プログラムはその予想が小さすぎるか大きすぎるかを表示します。 予想が当たっているなら、お祝いのメッセージを表示し、ゲームを終了します。
新規プロジェクトの立ち上げ
新しいプロジェクトを立ち上げましょう。 第1章で作成したprojectsディレクトリに移動し、以下のようにCargoを使って新規プロジェクトを作成します。
$ cargo new guessing_game
$ cd guessing_game
最初のコマンドcargo newは、第1引数としてプロジェクト名 (guessing_game) を取ります。
2番目のコマンドは新規プロジェクトのディレクトリに移動します。
生成されたCargo.tomlファイルを見てみましょう。
ファイル名:Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
第1章で見たようにcargo newは「Hello, world!」プログラムを生成してくれます。
src/main.rsファイルをチェックしてみましょう。
ファイル名:src/main.rs
fn main() { println!("Hello, world!"); }
さて、cargo runコマンドを使って、この「Hello, world!」プログラムのコンパイルと実行を一気に行いましょう。
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Hello, world!
このゲーム(の開発)では各イテレーションを素早くテストしてから、次のイテレーションに移ります。
runコマンドは、今回のようにプロジェクトのイテレーションを素早く回したいときに便利です。
訳注:ここでのイテレーションは、アジャイルな開発手法で用いられている用語にあたります。
イテレーションとは開発工程の「一回のサイクル」のことで、サイクルには、設計、実装、テスト、改善(リリース後の振り返り)が含まれます。 アジャイル開発ではイテレーションを数週間の短いスパンで一通り回し、それを繰り返すことで開発を進めていきます。
この章では「実装」→「テスト」のごく短いサイクルを繰り返すことで、プログラムに少しずつ機能を追加していきます。
src/main.rsファイルを開き直しましょう。 このファイルにすべてのコードを書いていきます。
予想を処理する
数当てゲームプログラムの最初の部分は、ユーザに入力を求め、その入力を処理し、期待した形式になっていることを確認することです。 手始めに、プレーヤーが予想を入力できるようにしましょう。 リスト2-1のコードをsrc/main.rsに入力してください。
ファイル名: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); // 次のように予想しました: {}
}
リスト2-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ライブラリをuseすると、ユーザ入力を受け付ける機能など(入出力に関する)多くの便利な機能が利用できるようになります。
第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); // 次のように予想しました: {}
}
プログラムが少し興味深いものになってきました。
この小さな行の中でいろいろなことが起きています。
let文を使って変数を作っています。
別の例も見てみましょう。
let apples = 5;
この行ではapplesという名前の新しい変数を作成し5という値に束縛しています。
Rustでは変数はデフォルトで不変(immutable)になります。
この概念については第3章の「変数と可変性」の節で詳しく説明します。
変数を可変(mutable)にするには、変数名の前にmutをつけます。
let apples = 5; // immutable
// 不変
let mut bananas = 5; // mutable
// 可変
注:
//構文は行末まで続くコメントを開始し、Rustはコメント内のすべて無視します。 コメントについては第3章で詳しく説明します。
数当てゲームのプログラムに戻りましょう。
ここまでの話でlet mut guessがguessという名前の可変変数を導入することがわかったと思います。
等号記号(=)はRustに、いまこの変数を何かに束縛したいことを伝えます。
等号記号の右側にはguessが束縛される値があります。
これはString::new関数を呼び出すことで得られた値で、この関数はString型の新しいインスタンスを返します。
Stringは標準ライブラリによって提供される文字列型で、サイズが拡張可能な、UTF-8でエンコードされたテキスト片になります。
::newの行にある::構文はnewがString型の関連関数であることを示しています。
関連関数とは、ある型(ここではString)に対して実装される関数のことです。
この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行目になりますが、まだ一つの論理的な行の一部分に過ぎません。 次の部分はこのメソッドです。
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");
しかし、長い行は読みづらいので分割したほうがよいでしょう。
.method_name()構文でメソッドを呼び出すとき、長い行を改行と空白で分割するのが賢明なことがよくあります。
それでは、この行(expect()メソッド)が何をするのか説明します。
前述したように、read_lineメソッドは渡された文字列にユーザが入力したものを入れます。
しかし、同時に値(この場合はio::Result)も返します。
Rustの標準ライブラリにはResultという名前の型がいくつかあります。
汎用のResultと、io::Resultといったサブモジュール用の特殊な型などです。
これらのResult型は列挙型になります。
列挙型はenumとも呼ばれ、取りうる値として決まった数の列挙子(variant)を持ちます。
列挙型はよくmatchと一緒に使われます。
これは条件式の一種で、評価時に、列挙型の値がどの列挙子であるかに基づいて異なるコードを実行できるという便利なものです。
enumについては第6章で詳しく説明します。
これらのResult型の目的は、エラー処理に関わる情報を符号化(エンコード)することです。
Resultの列挙子はOkかErrです。
Ok列挙子は処理が成功したことを示し、Okの中には正常に生成された値が入っています。
Err列挙子は処理が失敗したことを意味し、Errには処理が失敗した過程や理由についての情報が含まれています。
Result型の値にも、他の型と同様にメソッドが定義されています。
io::Resultのインスタンスにはexpectメソッドがありますので、これを呼び出せます。
このio::ResultインスタンスがErrの値の場合、expectメソッドはプログラムをクラッシュさせ、引数として渡されたメッセージを表示します。
read_lineメソッドがErrを返したら、それはおそらく基礎となるオペレーティング・システムに起因するものでしょう。
もしこのio::ResultオブジェクトがOk値の場合、expectメソッドはOk列挙子が保持する戻り値を取り出して、その値だけを返してくれます。
こうして私たちはその値を使うことができるわけです。
今回の場合、その値はユーザ入力のバイト数になります。
もしexpectメソッドを呼び出さなかったら、コンパイルはできるものの警告が出るでしょう。
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
(警告: 使用されなければならない`std::result::Result`が使用されていません)
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished dev [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); // 次のように予想しました: {}
}
この行はユーザの入力を現在保持している文字列を表示します。
一組の波括弧の{}はプレースホルダーです。
{}は値を所定の場所に保持する小さなカニのはさみだと考えてください。
波括弧をいくつか使えば複数の値を表示できます。
最初の波括弧の組はフォーマット文字列のあとに並んだ最初の値に対応し、2組目は2番目の値、というように続いていきます。
一回のprintln!の呼び出しで複数の値を表示するなら次のようになります。
#![allow(unused)] fn main() { let x = 5; let y = 10; println!("x = {} and y = {}", x, y); }
このコードはx = 5 and y = 10と表示するでしょう。
最初の部分をテストする
数当てゲームの最初の部分をテストしてみましょう。
cargo runで走らせてください。
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [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クレートを提供してくれています。
クレートを使用して機能を追加する
クレートはRustソースコードを集めたものであることを思い出してください。
私たちがここまで作ってきたプロジェクトはバイナリクレートであり、これは実行可能ファイルになります。
randクレートはライブラリクレートです。
他のプログラムで使用するためのコードが含まれており、単独で実行することはできません。
Cargoがその力を発揮するのは外部クレートと連携するときです。
randを使ったコードを書く前に、Cargo.tomlファイルを編集してrandクレートを依存関係に含める必要があります。
そのファイルを開いて、Cargoが作ってくれた[dependencies]セクションヘッダの下に次の行を追加してください。
バージョンナンバーを含め、ここに書かれている通り正確にrandを指定してください。
そうしないと、このチュートリアルのコード例が動作しないかもしれません。
ファイル名:Cargo.toml
rand = "0.8.3"
Cargo.tomlファイルでは、ヘッダに続くものはすべて、他のセクションが始まるまで続くセクションの一部になります。
(訳注:Cargo.tomlファイル内には複数のセクションがあり、各セクションは[ ]で囲まれたヘッダ行から始まります)
[dependecies]はプロジェクトが依存する外部クレートと必要とするバージョンをCargoに伝えます。
今回はrandクレートを0.8.3というセマンティックバージョン指定子で指定します。
Cargoはセマンティックバージョニング(SemVerと呼ばれることもあります)を理解しており、これはバージョンナンバーを記述するための標準です。
0.8.3という数字は実際には^0.8.3の省略記法で、0.8.3以上0.9.0未満の任意のバージョンを意味します。
Cargoはこれらのバージョンを、バージョン0.8.3と互換性のある公開APIを持つものとみなします。
この仕様により、この章のコードが引き続きコンパイルできるようにしつつ、最新のパッチリリースを取得できるようになります。
0.9.0以降のバージョンは、以下の例で使用しているものと同じAPIを持つことを保証しません。
さて、コードを一切変えずに、次のリスト2-2のようにプロジェクトをビルドしてみましょう。
$ cargo build
Updating crates.io index
(crates.ioインデックスを更新しています)
Downloaded rand v0.8.3
(rand v0.8.3をダウンロードしています)
Downloaded libc v0.2.86
Downloaded getrandom v0.2.2
Downloaded cfg-if v1.0.0
Downloaded ppv-lite86 v0.2.10
Downloaded rand_chacha v0.3.0
Downloaded rand_core v0.6.2
Compiling rand_core v0.6.2
(rand_core v0.6.2をコンパイルしています)
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_chacha v0.3.0
Compiling rand v0.8.3
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
(guessing_game v0.1.0をコンパイルしています)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
リスト2-2:randクレートを依存として追加した後のcargo buildコマンドの出力
もしかしたら異なるバージョンナンバー(とはいえ、SemVerのおかげですべてのコードに互換性があります)や、 異なる行(オペレーティングシステムに依存します)が表示されるかもしれません。 また、行の順序も違うかもしれません。
外部依存を持つようになると、Cargoはその依存関係が必要とするすべてについて最新のバージョンをレジストリから取得します。 レジストリとはCrates.ioのデータのコピーです。 Crates.ioは、Rustのエコシステムにいる人たちがオープンソースのRustプロジェクトを投稿し、他の人が使えるようにする場所です。
レジストリの更新後、Cargoは[dependencies]セクションにリストアップされているクレートをチェックし、まだ取得していないものがあればダウンロードします。
ここでは依存関係としてrandだけを書きましたが、randが動作するために依存している他のクレートも取り込まれています。
クレートをダウンロードしたあと、Rustはそれらをコンパイルし、依存関係が利用できる状態でプロジェクトをコンパイルします。
何も変更せずにすぐにcargo buildコマンドを再度実行すると、Finishedの行以外は何も出力されないでしょう。
Cargoはすでに依存関係をダウンロードしてコンパイル済みであることを認識しており、また、あなたがCargo.tomlファイルを変更していないことも知っているからです。
さらに、Cargoはあなたがコードを何も変更していないことも知っているので、再コンパイルもしません。
何もすることがないので単に終了します。
src/main.rsファイルを開いて些細な変更を加え、それを保存して再度ビルドすると2行しか表示されません。
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
これらの行はCargoがsrc/main.rsファイルへの小さな変更に対して、ビルドを更新していることを示しています。 依存関係は変わっていないので、Cargoは既にダウンロードしてコンパイルしたものが再利用できることを知っています。
Cargo.lockファイルで再現可能なビルドを確保する
Cargoはあなたや他の人があなたのコードをビルドするたびに、同じ生成物をリビルドできるようにするしくみを備えています。
Cargoは何も指示されない限り、指定したバージョンの依存のみを使用します。
たとえば来週randクレートのバージョン0.8.4が出て、そのバージョンには重要なバグ修正が含まれていますが、同時にあなたのコードを破壊するリグレッションも含まれているとします。
これに対応するため、Rustはcargo buildを最初に実行したときにCargo.lockファイルを作成します。
(いまのguessing_gameディレクトリにもあるはずです)
プロジェクトを初めてビルドするとき、Cargoは条件に合うすべての依存関係のバージョンを計算しCargo.lockファイルに書き込みます。
次にプロジェクトをビルドすると、CargoはCargo.lockファイルが存在することを確認し、バージョンを把握するすべての作業を再び行う代わりに、そこで指定されているバージョンを使います。
これにより再現性のあるビルドを自動的に行えます。
言い換えれば、Cargo.lockファイルのおかげで、あなたが明示的にアップグレードするまで、プロジェクトは0.8.3を使い続けます。
クレートを更新して新バージョンを取得する
クレートを本当にアップグレードしたくなったときのために、Cargoはupdateコマンドを提供します。
このコマンドはCargo.lockファイルを無視して、Cargo.tomlファイル内の全ての指定に適合する最新バージョンを算出します。
成功したらCargoはそれらのバージョンをCargo.lockファイルに記録します。
ただし、デフォルトでCargoは0.8.3以上、0.9.0未満のバージョンのみを検索します。
もしrandクレートの新しいバージョンとして0.8.4と0.9.0の二つがリリースされていたなら、cargo updateを実行したときに以下のようなメッセージが表示されるでしょう。
$ cargo update
Updating crates.io index
(crates.ioインデックスを更新しています)
Updating rand v0.8.3 -> v0.8.4
(randクレートをv0.8.3 -> v0.8.4に更新しています)
Cargoは0.9.0リリースを無視します。
またそのとき、Cargo.lockファイルが変更され、randクレートの現在使用中のバージョンが0.8.4になったことにも気づくでしょう。
そうではなく、randのバージョン0.9.0か、0.9.x系のどれかを使用するには、Cargo.tomlファイルを以下のように変更する必要があります。
[dependencies]
rand = "0.9.0"
次にcargo buildコマンドを実行したとき、Cargoは利用可能なクレートのレジストリを更新し、あなたが指定した新しいバージョンに従ってrandの要件を再評価します。
Cargoとそのエコシステムについては、まだ伝えたいことが山ほどありますが、それらについては第14章で説明します。 いまのところは、これだけ知っていれば十分です。 Cargoはライブラリの再利用をとても簡単にしてくれるので、Rustaceanが数多くのパッケージから構成された小さなプロジェクトを書くことが可能になっています。
乱数を生成する
randクレートを使って予想する数字を生成しましょう。
次のステップはsrc/main.rsファイルをリスト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..101);
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);
}
リスト2-3:乱数を生成するコードの追加
まずuse rand::Rngという行を追加します。
Rngトレイトは乱数生成器が実装すべきメソッドを定義しており、それらのメソッドを使用するには、このトレイトがスコープ内になければなりません。
トレイトについて詳しくは第10章で解説します。
次に、途中に2行を追加しています。
最初の行ではrand::thread_rng関数を呼び出して、これから使う、ある特定の乱数生成器を取得しています。
なお、この乱数生成器は現在のスレッドに固有で、オペレーティングシステムからシード値を得ています。
そして、この乱数生成器のgen_rangeメソッドを呼び出しています。
このメソッドはuse rand::Rng文でスコープに導入したRngトレイトで定義されています。
gen_rangeメソッドは範囲式を引数にとり、その範囲内の乱数を生成してくれます。
ここで使っている範囲式の種類は開始..終了という形式で、下限値は含みますが上限値は含みません。
そのため、1から100までの数をリクエストするには1..101と指定する必要があります。
あるいは、これと同等の1..=100という範囲を渡すこともできます。
注:クレートのどのトレイトを
useするかや、どのメソッドや関数を呼び出すかを知るために、各クレートにはその使い方を説明したドキュメントが用意されています。 Cargoのもう一つの素晴らしい機能は、cargo doc --openコマンドを走らせると、すべての依存クレートが提供するドキュメントをローカルでビルドして、ブラウザで開いてくれることです。 たとえばrandクレートの他の機能に興味があるなら、cargo doc --openコマンドを実行して、左側のサイドバーにあるrandをクリックしてください。
コードに追加した2行目は秘密の数字を表示します。 これはプログラムを開発している間のテストに便利ですが、最終版からは削除する予定です。 プログラムが始まってすぐに答えが表示されたらゲームになりませんからね!
試しにプログラムを何回か走らせてみてください。
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
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 [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に示します。 これから説明するように、このコードはまだコンパイルできないことに注意してください。
ファイル名:src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// --snip--
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
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!"), //やったね!
}
}
リスト2-4:二つの数値を比較したときに返される可能性のある値を処理する
まずuse文を追加して標準ライブラリからstd::cmp::Orderingという型をスコープに導入しています。
Orderingもenumの一つでLess、Greater、Equalという列挙子を持っています。
これらは二つの値を比較したときに得られる3種類の結果です。
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
それからOrdering型を使用する新しい5行をいちばん下に追加してしています。
cmpメソッドは二つの値の比較を行い、比較できるものになら何に対しても呼び出せます。
比較対象への参照をとり、ここではguessとsecret_numberを比較しています。
そしてuse文でスコープに導入したOrdering列挙型の列挙子を返します。
ここではmatch式を使用しており、guessとsecret_numberの値に対してcmpを呼んだ結果返されたOrderingの列挙子に基づき、次の動作を決定しています。
match式は複数のアーム(腕)で構成されます。
各アームはマッチさせるパターンと、matchに与えられた値がそのアームのパターンにマッチしたときに実行されるコードで構成されます。
Rustはmatchに与えられた値を受け取って、各アームのパターンを順に照合していきます。
パターンとmatch式はRustの強力な機能で、コードか遭遇する可能性のあるさまざまな状況を表現し、それらすべてを確実に処理できるようにします。
これらの機能については、それぞれ第6章と第18章で詳しく説明します。
ここで使われている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.3
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types (型が合いません)
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| ^^^^^^^^^^^^^^ expected struct `String`, found integer
| (構造体`std::string::String`を予期したけど、整数型変数が見つかりました)
|
= note: expected reference `&String`
found reference `&{integer}`
error[E0283]: type annotations needed for `{integer}`
--> src/main.rs:8:44
|
8 | let secret_number = rand::thread_rng().gen_range(1..101);
| ------------- ^^^^^^^^^ cannot infer type for type `{integer}`
| |
| consider giving `secret_number` a type
|
= note: multiple `impl`s satisfying `{integer}: SampleUniform` found in the `rand` crate:
- impl SampleUniform for i128;
- impl SampleUniform for i16;
- impl SampleUniform for i32;
- impl SampleUniform for i64;
and 8 more
note: required by a bound in `gen_range`
--> /Users/carolnichols/.cargo/registry/src/github.com-1ecc6299db9ec823/rand-0.8.3/src/rng.rs:129:12
|
129 | T: SampleUniform,
| ^^^^^^^^^^^^^ required by this bound in `gen_range`
help: consider specifying the type arguments in the function call
|
8 | let secret_number = rand::thread_rng().gen_range::<T, R>(1..101);
| ++++++++
Some errors have detailed explanations: E0283, E0308.
For more information about an error, try `rustc --explain E0283`.
error: could not compile `guessing_game` due to 2 previous errors (先の2つのエラーのため、`guessing_game`をコンパイルできませんでした)
このエラーの核心は型の不一致があると述べていることです。
Rustは強い静的型システムを持ちますが、型推論も備えています。
let guess = String::new()と書いたとき、RustはguessがString型であるべきと推論したので、私たちはその型を書かずに済みました。
一方でsecret_numberは数値型です。
Rustのいくつかの数値型は1から100までの値を表現でき、それらの型には32ビット数値のi32、符号なしの32ビット数値のu32、64ビット数値のi64などがあります。
Rustのデフォルトはi32型で、型情報をどこかに追加してRustに異なる数値型だと推論させない限りsecret_numberの型はこれになります。
エラーの原因はRustが文字列と数値型を比較できないためです。
最終的にはプログラムが入力として読み込んだStringを実数型に変換し、秘密の数字と数値として比較できるようにしたいわけです。
そのためにはmain関数の本体に次の行を追加します。
ファイル名:src/main.rs
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..101);
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の前の値を新しい値で覆い隠す(shadowする)ことが許されているのです。
シャドーイング(shadowing)は、guess_strとguessのような重複しない変数を二つ作る代わりに、guessという変数名を再利用させてくれるのです。
これについては第3章で詳しく説明しますが、今のところ、この機能はある型から別の型に値を変換するときによく使われることを知っておいてください。
この新しい変数をguess.trim().parse()という式に束縛しています。
式の中にあるguessは、入力が文字列として格納されたオリジナルのguess変数を指しています。
Stringインスタンスのtrimメソッドは文字列の先頭と末尾の空白をすべて削除します。
これは数値データのみを表現できる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型であるべきだと推論しています。
つまり、いまでは二つの同じ型の値を比較することになるわけです!
parseメソッドは論理的に数値に変換できる文字にしか使えないので、よくエラーになります。
たとえば文字列にA👍%が含まれていたら数値に変換する術はありません。
解析に失敗する可能性があるため、parseメソッドはread_lineメソッドと同様にResult型を返します
(「Result型で失敗の可能性を扱う」で説明しました)
今回もexpectメソッドを使用してResult型を同じように扱います。
parseメソッドが文字列から数値を作成できなかったためにResult型のErr列挙子を返したら、expectの呼び出しはゲームをクラッシュさせ、私たちが与えたメッセージを表示します。
parseが文字列をうまく数値へ変換できたときはResult型のOk列挙子を返し、expectはOk値から欲しい数値を返してくれます。
さあ、プログラムを走らせましょう!
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
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 rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
// --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 [unoptimized + debuginfo] target(s) in 1.50s
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 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
(スレッド'main'は'数字を入力してください!:ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:785でパニックしました)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
(注:`RUST_BACKTRACE=1`で走らせるとバックトレースを見れます)
quitと入力すればゲームが終了しますが、数字以外の入力でもそうなります。
これは控えめに言っても最適ではありません。
私たちは正しい数字が予想されたときにゲームが停止するようにしたいのです。
正しい予想をした後に終了する
break文を追加して、ユーザが勝ったらゲームが終了するようにプログラムしましょう。
ファイル名:src/main.rs
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..101);
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のように、Stringからu32にguessを変換する行を変えることで実現できます。
ファイル名:src/main.rs
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..101);
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;
}
}
}
}
リスト2-5:数値以外の予想を無視し、プログラムをクラッシュさせるのではなく、もう1回予想してもらう
expectの呼び出しからmatch式に切り替えて、エラーによるクラッシュからエラー処理へと移行します。
parseがResult型を返すことと、ResultがOkとErrの列挙子を持つ列挙型であることを思い出してください。
ここではmatch式を、cmpメソッドから返されるOrderingを処理したときと同じように使っています。
もし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 [unoptimized + debuginfo] target(s) in 4.45s
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!
素晴らしい!
最後にほんの少し手を加えれば数当てゲームは完成です。
このプログラムはまだ秘密の数字を表示していることを思い出してください。
テストには便利でしたが、これではゲームが台無しです。
秘密の数字を表示しているprintln!を削除しましょう。
最終的なコードをリスト2-6に示します。
ファイル名:src/main.rs
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..101);
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;
}
}
}
}
リスト2-6:数当てゲームの完全なコード
まとめ
数当てゲームを無事に作り上げることができました。 おめでとうございます!
このプロジェクトではハンズオンを通して、let、match、メソッド、関連関数、外部クレートの使いかたなど、多くの新しいRustの概念に触れました。
以降の章では、これらの概念についてより詳しく学びます。
第3章では変数、データ型、関数など多くのプログラミング言語が持つ概念を取り上げ、Rustでの使い方を説明します。
第4章ではRustを他の言語とは異なるものに特徴づける、所有権について説明します。
第5章では構造体とメソッドの構文について説明し、第6章では列挙型がどのように動くのかについて説明します。
一般的なプログラミングの概念
この章では、ほとんど全てのプログラミング言語で見られる概念を講義し、それらがRustにおいて、 どう動作するかを見ていきます。多くのプログラミング言語は、その核心において、いろいろなものを共有しています。 この章で提示する概念は、全てRustに固有のものではありませんが、Rustの文脈で議論し、 これらの概念を使用することにまつわる仕様を説明します。
具体的には、変数、基本的な型、関数、コメント、そして制御フローについて学びます。 これらの基礎は全てのRustプログラムに存在するものであり、それらを早期に学ぶことにより、強力な基礎を築くことになるでしょう。
キーワード
Rust言語にも他の言語同様、キーワードが存在し、これらは言語だけが使用できるようになっています。 これらの単語は、変数や関数名には使えないことを弁えておいてください。ほとんどのキーワードは、特別な意味を持っており、 自らのRustプログラムにおいて、様々な作業をこなすために使用することができます; いくつかは、紐付けられた機能がないものの、将来Rustに追加されるかもしれない機能用に予約されています。 キーワードの一覧は、付録Aで確認できます。
変数と可変性
第2章で触れた通り、変数は標準で不変になります。これは、 Rustが提供する安全性や簡便な並行性の利点を享受する形でコードを書くための選択の1つです。 ところが、まだ変数を可変にするという選択肢も残されています。 どのように、そしてなぜRustは不変性を推奨するのか、さらには、なぜそれとは違う道を選びたくなることがあるのか見ていきましょう。
変数が不変であると、値が一旦名前に束縛されたら、その値を変えることができません。
これを具体的に説明するために、projectsディレクトリにcargo new --bin variablesコマンドを使って、
variablesという名前のプロジェクトを生成しましょう。
それから、新規作成したvariablesディレクトリで、src/main.rsファイルを開き、 その中身を以下のコードに置き換えましょう。このコードはまだコンパイルできません:
ファイル名: src/main.rs
fn main() {
let x = 5;
println!("The value of x is: {}", x); // 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`
(不変変数`x`に2回代入できません)
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| (`x`への最初の代入)
| help: consider making this binding mutable: `mut x`
3 | println!("The value of x is: {}", x);
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` due to previous error
この例では、コンパイラがプログラムに潜むエラーを見つけ出す手助けをしてくれることが示されています。 コンパイルエラーは、イライラすることもあるものですが、まだプログラムにしてほしいことを安全に行えていないだけということなのです。 エラーが出るからといって、あなたがいいプログラマではないという意味ではありません! 経験豊富なRustaceanでも、コンパイルエラーを出すことはあります。
このエラーは、エラーの原因が不変変数xに2回代入できないであると示しています。不変なxという変数に別の値を代入しようとしたからです。
以前に不変と指定された値を変えようとした時に、コンパイルエラーが出るのは重要なことです。 なぜなら、この状況はまさしく、バグに繋がるからです。コードのある部分は、 値が変わることはないという前提のもとに処理を行い、別の部分がその値を変更していたら、 最初の部分が目論見通りに動いていない可能性があるのです。このようなバグは、発生してしまってからでは原因が追いかけづらいものです。 特に第2のコード片が、値を時々しか変えない場合、尚更です。
Rustでは、値が不変であると宣言したら、本当に変わらないことをコンパイラが担保してくれます。 つまり、コードを読み書きする際に、どこでどうやって値が変化しているかを追いかける必要がなくなります。 故にコードを通して正しいことを確認するのが簡単になるのです。
しかし、可変性は時として非常に有益なこともあります。変数は、標準でのみ、不変です。つまり、
第2章のように変数名の前にmutキーワードを付けることで、可変にできるわけです。この値が変化できるようにするとともに、
mutにより、未来の読者に対してコードの別の部分がこの変数の値を変える可能性を示すことで、その意図を汲ませることができるのです。
例として、src/main.rsファイルを以下のように書き換えてください:
ファイル名: src/main.rs
fn main() { let mut x = 5; println!("The value of x is: {}", x); x = 6; println!("The value of x is: {}", x); }
今、このプログラムを走らせると、以下のような出力が得られます:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5 (xの値は5です)
The value of x is: 6
mutキーワードが使われると、xが束縛している値を5から6に変更できます。
変数を可変にする方が、不変変数だけがあるよりも書きやすくなるので、変数を可変にしたくなることもあるでしょう。
考えるべきトレードオフはバグの予防以外にも、いくつかあります。例えば、大きなデータ構造を使う場合などです。 インスタンスを可変にして変更できるようにする方が、いちいちインスタンスをコピーして新しくメモリ割り当てされたインスタンスを返すよりも速くなります。 小規模なデータ構造なら、新規インスタンスを生成して、もっと関数型っぽいコードを書く方が通して考えやすくなるため、 低パフォーマンスは、その簡潔性を得るのに足りうるペナルティになるかもしれません。
変数と定数(constants)の違い
変数の値を変更できないようにするといえば、他の多くの言語も持っている別のプログラミング概念を思い浮かべるかもしれません: 定数です。不変変数のように、定数は名前に束縛され、変更することが叶わない値のことですが、 定数と変数の間にはいくつかの違いがあります。
まず、定数にはmutキーワードは使えません: 定数は標準で不変であるだけでなく、常に不変なのです。
定数はletキーワードの代わりに、constキーワードで宣言し、値の型は必ず注釈しなければなりません。
型と型注釈については次のセクション、「データ型」で講義しますので、その詳細について気にする必要はありません。
ただ単に型は常に注釈しなければならないのだと思っていてください。
定数はどんなスコープでも定義できます。グローバルスコープも含めてです。なので、 いろんなところで使用される可能性のある値を定義するのに役に立ちます。
最後の違いは、定数は定数式にしかセットできないことです。関数呼び出し結果や、実行時に評価される値にはセットできません。
定数の名前がMAX_POINTSで、値が100,000にセットされた定数定義の例をご覧ください。(Rustの定数の命名規則は、
全て大文字でアンダースコアで単語区切りすることです):
#![allow(unused)] fn main() { const MAX_POINTS: u32 = 100_000; }
定数は、プログラムが走る期間、定義されたスコープ内でずっと有効です。従って、 プログラムのいろんなところで使用される可能性のあるアプリケーション空間の値を定義するのに有益な選択肢になります。 例えば、ゲームでプレイヤーが取得可能なポイントの最高値や、光速度などですね。
プログラム中で使用されるハードコードされた値に対して、定数として名前付けすることは、 コードの将来的な管理者にとって値の意味を汲むのに役に立ちます。将来、ハードコードされた値を変える必要が出た時に、 たった1箇所を変更するだけで済むようにもしてくれます。
シャドーイング
第2章の数当てゲームのチュートリアル、「予想と秘密の数字を比較する」節で見たように、前に定義した変数と同じ名前の変数を新しく宣言でき、
新しい変数は、前の変数を覆い隠します。Rustaceanはこれを最初の変数は、
2番目の変数に覆い隠されたと言い、この変数を使用した際に、2番目の変数の値が現れるということです。
以下のようにして、同じ変数名を用いて変数を覆い隠し、letキーワードの使用を繰り返します:
ファイル名: src/main.rs
fn main() { let x = 5; let x = x + 1; { let x = x * 2; println!("The value of x in the inner scope is: {}", x); } println!("The value of x is: {}", x); }
このプログラムはまず、xを5という値に束縛します。それからlet x =を繰り返すことでxを覆い隠し、
元の値に1を加えることになるので、xの値は6になります。
3番目のlet文もxを覆い隠し、以前の値に2をかけることになるので、xの最終的な値は12になります。
括弧を抜けるとシャドーイングは終了し、xの値は元の6に戻ります。
このプログラムを走らせたら、以下のように出力するでしょう:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [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と上書きのもう一つの違いは、再度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`
| (&str型を予期しましたが、usizeが見つかりました)
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` due to previous error
さあ、変数が動作する方法を見てきたので、今度は変数が取りうるデータ型について見ていきましょう。
データ型
Rustにおける値は全て、何らかのデータ型になり、コンパイラがどんなデータが指定されているか知れるので、 そのデータの取り扱い方も把握できるというわけです。2種のデータ型のサブセットを見ましょう: スカラー型と複合型です。
Rustは静的型付き言語であることを弁えておいてください。つまり、
コンパイル時に全ての変数の型が判明している必要があるということです。コンパイラは通常、値と使用方法に基づいて、
使用したい型を推論してくれます。複数の型が推論される可能性がある場合、例えば、
第2章の「予想と秘密の数字を比較する」節でparseメソッドを使ってString型を数値型に変換した時のように、
複数の型が可能な場合には、型注釈をつけなければいけません。以下のようにですね:
#![allow(unused)] fn main() { let guess: u32 = "42".parse().expect("Not a number!"); // 数字ではありません! }
ここで型注釈を付けなければ、コンパイラは以下のエラーを表示し、これは可能性のある型のうち、 どの型を使用したいのかを知るのに、コンパイラがプログラマからもっと情報を得る必要があることを意味します:
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
(型注釈が必要です)
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ consider giving `guess` a type
| (`guess`に型を与えることを検討してください)
For more information about this error, try `rustc --explain E0282`.
error: could not compile `no_type_annotations` due to previous error
他のデータ型についても、様々な型注釈を目にすることになるでしょう。
スカラー型
スカラー型は、単独の値を表します。Rustには主に4つのスカラー型があります: 整数、浮動小数点数、論理値、最後に文字です。他のプログラミング言語でも、これらの型を見かけたことはあるでしょう。 Rustでの動作方法に飛び込みましょう。
整数型
整数とは、小数部分のない数値のことです。第2章で一つの整数型を使用しましたね。u32型です。
この型定義は、紐付けられる値が、符号なし整数(符号付き整数はuではなく、iで始まります)になり、
これは、32ビット分のサイズを取ります。表3-1は、Rustの組み込み整数型を表示しています。
符号付きと符号なし欄の各バリアント(例: i16)を使用して、整数値の型を宣言することができます。
表3-1: Rustの整数型
| 大きさ | 符号付き | 符号なし |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| arch | 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)に付加することができます。
表3-2: Rustの整数リテラル
| 数値リテラル | 例 |
|---|---|
| 10進数 | 98_222 |
| 16進数 | 0xff |
| 8進数 | 0o77 |
| 2進数 | 0b1111_0000 |
バイト (u8だけ) | b'A' |
では、どの整数型を使うべきかはどう把握すればいいのでしょうか?もし確信が持てないのならば、
Rustの基準型は一般的にいい選択肢になります。整数型の基準はi32型です: 64ビットシステム上でも、
この型が普通最速になります。isizeとusizeを使う主な状況は、何らかのコレクションにアクセスすることです。
浮動小数点型
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規格に従って表現されています。f32が単精度浮動小数点数、
f64が倍精度浮動小数点数です。
数値演算
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 floored = 2 / 3; // Results in 0 // 結果は0 // remainder // 余り let remainder = 43 % 5; }
これらの文の各式は、数学演算子を使用しており、一つの値に評価され、そして、変数に束縛されます。 付録BにRustで使える演算子の一覧が載っています。
論理値型
他の多くの言語同様、Rustの論理値型も取りうる値は二つしかありません: trueとfalseです。
Rustの論理値型は、boolと指定されます。
例です:
ファイル名: src/main.rs
fn main() { let t = true; let f: bool = false; // with explicit type annotation // 明示的型注釈付きで }
論理値を使う主な手段は、条件式です。例えば、if式などですね。if式のRustでの動作方法については、
「制御フロー」節で講義します。
文字型
ここまで、数値型のみ扱ってきましたが、Rustには文字も用意されています。Rustのchar型は、
言語の最も基本的なアルファベット型であり、以下のコードでその使用方法の一例を見ることができます。
(charは、ダブルクォーテーションマークを使用する文字列に対して、シングルクォートで指定されることに注意してください。)
ファイル名: src/main.rs
fn main() { let c = 'z'; let z = 'ℤ'; let heart_eyed_cat = '😻'; //ハート目の猫 }
Rustのchar型は、ユニコードのスカラー値を表します。これはつまり、アスキーよりもずっとたくさんのものを表せるということです。
アクセント文字; 中国語、日本語、韓国語文字;
絵文字; ゼロ幅スペースは、全てRustでは、有効なchar型になります。ユニコードスカラー値は、
U+0000からU+D7FFまでとU+E000からU+10FFFFまでの範囲になります。
ところが、「文字」は実はユニコードの概念ではないので、文字とは何かという人間としての直観は、
Rustにおけるchar値が何かとは合致しない可能性があります。この話題については、第8章の「文字列」で詳しく議論しましょう。
複合型
複合型により、複数の値を一つの型にまとめることができます。Rustには、 2種類の基本的な複合型があります: タプルと配列です。
タプル型
タプルは、複数の型の何らかの値を一つの複合型にまとめ上げる一般的な手段です。
タプルは、丸かっこの中にカンマ区切りの値リストを書くことで生成します。タプルの位置ごとに型があり、 タプル内の値はそれぞれ全てが同じ型である必要はありません。今回の例では、型注釈をあえて追加しました:
ファイル名: src/main.rs
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); }
変数tupは、タプル全体に束縛されています。なぜなら、タプルは、一つの複合要素と考えられるからです。
タプルから個々の値を取り出すには、パターンマッチングを使用して分解することができます。以下のように:
ファイル名: 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ですね)に変換しています。
この過程は、分配と呼ばれます。単独のタプルを破壊して三分割しているからです。最後に、
プログラムは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です。
配列型
配列によっても、複数の値のコレクションを得ることができます。タプルと異なり、配列の全要素は、 同じ型でなければなりません。Rustの配列は、他の言語と異なっています。Rustの配列は、 固定長なのです: 一度宣言されたら、サイズを伸ばすことも縮めることもできません。
Rustでは、配列に入れる要素は、角かっこ内にカンマ区切りリストとして記述します:
ファイル名: src/main.rs
fn main() { let a = [1, 2, 3, 4, 5]; }
配列は、ヒープよりもスタック(スタックとヒープについては第4章で詳らかに議論します)にデータのメモリを確保したい時、 または、常に固定長の要素があることを確認したい時に有効です。 ただ、配列は、ベクタ型ほど柔軟ではありません。ベクタは、標準ライブラリによって提供されている配列と似たようなコレクション型で、 こちらは、サイズを伸縮させることができます。配列とベクタ型、どちらを使うべきか確信が持てない時は、 おそらくベクタ型を使うべきです。第8章でベクタについて詳細に議論します。
ベクタ型よりも配列を使いたくなるかもしれない例は、1年の月の名前を扱うプログラムです。そのようなプログラムで、 月を追加したり削除したりすることまずないので、配列を使用できます。常に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という名前の配列は3という値が5つあるものです。これはlet a = [3, 3, 3, 3, 3];と書くのと同じですが、より簡潔になります。
配列の要素にアクセスする
配列は、スタック上に確保される一塊のメモリです。添え字によって、 配列の要素にこのようにアクセスすることができます:
ファイル名: src/main.rs
fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; let second = a[1]; }
この例では、firstという名前の変数には1という値が格納されます。配列の[0]番目にある値が、
それだからですね。secondという名前の変数には、配列の[1]番目の値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 {} is: {}",
// {}番目の要素の値は{}です
index, element
);
}
このコードはコンパイルされます。cargo runで走らせ、0, 1, 2, 3, または4をこのプログラムに入力すると配列の対応する値を出力します。もし配列の末尾を超えるような、例えば10などの数字を与えると、次のような出力が表示されます。
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
スレッド'main'は'範囲外アクセス: 長さは5ですが、添え字は10でした', src/main.rs:19:19
でパニックしました
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
コンパイルでは何もエラーが出なかったものの、プログラムは実行時エラーに陥り、 正常終了しませんでした。要素に添え字アクセスを試みると、言語は、 指定されたその添え字が配列長よりも小さいかを確認してくれます。添え字が配列長よりも大きければ、言語はパニックします。 パニックとは、プログラムがエラーで終了したことを表すRust用語です。
これは、実際に稼働しているRustの安全機構の最初の例になります。低レベル言語の多くでは、 この種のチェックは行われないため、間違った添え字を与えると、無効なメモリにアクセスできてしまいます。 Rustでは、メモリアクセスを許可し、処理を継続する代わりに即座にプログラムを終了することで、 この種のエラーからプログラマを保護しています。Rustのエラー処理については、第9章でもっと議論します。
関数
関数は、Rustのコードにおいてよく見かける存在です。既に、言語において最も重要な関数のうちの一つを目撃していますね:
そう、main関数です。これは、多くのプログラムのエントリーポイント(訳注: プログラム実行時に最初に走る関数のこと)になります。
fnキーワードもすでに見かけましたね。これによって新しい関数を宣言することができます。
Rustの関数と変数の命名規則は、スネークケース(訳注: some_variableのような命名規則)を使うのが慣例です。
スネークケースとは、全文字を小文字にし、単語区切りにアンダースコアを使うことです。
以下のプログラムで、サンプルの関数定義をご覧ください:
ファイル名: src/main.rs
fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); // 別の関数 }
Rustにおいて関数定義は、fnキーワードで始まり、関数名の後に丸かっこの組が続きます。
波かっこが、コンパイラに関数本体の開始と終了の位置を伝えます。
定義した関数は、名前に丸かっこの組を続けることで呼び出すことができます。
another_function関数がプログラム内で定義されているので、main関数内から呼び出すことができるわけです。
ソースコード中でanother_functionをmain関数の後に定義していることに注目してください;
勿論、main関数の前に定義することもできます。コンパイラは、関数がどこで定義されているかは気にしません。
どこかで定義されていることのみ気にします。
functionsという名前の新しいバイナリ生成プロジェクトを始めて、関数についてさらに深く探究していきましょう。
another_functionの例をsrc/main.rsファイルに配置して、走らせてください。
以下のような出力が得られるはずです:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/functions`
Hello, world!
Another function.
行出力は、main関数内に書かれた順序で実行されています。最初に"Hello, world"メッセージが出て、
それからanother_functionが呼ばれて、こちらのメッセージが出力されています。
関数の引数
関数は、引数を持つようにも定義できます。引数とは、関数シグニチャの一部になる特別な変数のことです。
関数に引数があると、引数の位置に実際の値を与えることができます。技術的にはこの実際の値は
実引数と呼ばれますが、普段の会話では、仮引数("parameter")と実引数("argument")を関数定義の変数と関数呼び出し時に渡す実際の値、
両方の意味に区別なく使います(訳注: 日本語では、特別区別する意図がない限り、どちらも単に引数と呼ぶことが多いでしょう)。
以下の書き直したanother_functionでは、Rustの仮引数がどのようなものかを示しています:
ファイル名: src/main.rs
fn main() { another_function(5); } fn another_function(x: i32) { println!("The value of x is: {}", x); // xの値は{}です }
このプログラムを走らせてみてください; 以下のような出力が得られるはずです:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
another_functionの宣言には、xという名前の仮引数があります。xの型は、
i32と指定されています。値5がanother_functionに渡されると、println!マクロにより、
フォーマット文字列中の1組の波かっこがあった位置に値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); }
この例では、2引数の関数を生成しています。そして、引数はどちらもi32型です。それからこの関数は、
仮引数の値を両方出力します。関数引数は、全てが同じ型である必要はありません。今回は、
偶然同じになっただけです。
このコードを走らせてみましょう。今、functionプロジェクトのsrc/main.rsファイルに記載されているプログラムを先ほどの例と置き換えて、
cargo runで走らせてください:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
xに対して値5、yに対して値6を渡して関数を呼び出したので、この二つの文字列は、
この値で出力されました。
関数本体は、文と式を含む
関数本体は、文が並び、最後に式を置くか文を置くという形で形成されます。現在までには、 式で終わらない関数だけを見てきたわけですが、式が文の一部になっているものなら見かけましたね。Rustは、式指向言語なので、 これは理解しておくべき重要な差異になります。他の言語にこの差異はありませんので、文と式がなんなのかと、 その違いが関数本体にどんな影響を与えるかを見ていきましょう。
実のところ、もう文と式は使っています。文とは、なんらかの動作をして値を返さない命令です。 式は結果値に評価されます。ちょっと例を眺めてみましょう。
letキーワードを使用して変数を生成し、値を代入することは文になります。
リスト3-1でlet y = 6;は文です。
ファイル名: src/main.rs
fn main() { let y = 6; }
リスト3-1: 1文を含むmain関数宣言
関数定義も文になります。つまり、先の例は全体としても文になるわけです。
文は値を返しません。故に、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 statement (`let`)
(エラー: 式を予期しましたが、文が見つかりました (`let`))
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^^^^^^^
|
= note: variable declaration using `let` is a statement
(注釈: `let`を使う変数宣言は、文です)
error[E0658]: `let` expressions in this position are experimental
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^^^^^^^
|
= note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information
= help: you can write `matches!(<expr>, <pattern>)` instead of `let <pattern> = <expr>`
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;
|
For more information about this error, try `rustc --explain E0658`.
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` due to 2 previous errors; 1 warning emitted
このlet y = 6という文は値を返さないので、xに束縛するものがないわけです。これは、
CやRubyなどの言語とは異なる動作です。CやRubyでは、代入は代入値を返します。これらの言語では、
x = y = 6と書いて、xもyも値6になるようにできるのですが、Rustにおいては、
そうは問屋が卸さないわけです。
式は何かに評価され、これからあなたが書くRustコードの多くを構成します。
簡単な数学演算(5 + 6など)を思い浮かべましょう。この例は、値11に評価される式です。式は文の一部になりえます:
リスト3-1において、let y = 6という文の6は値6に評価される式です。関数呼び出しも式です。マクロ呼び出しも式です。
新しいスコープを作る際に使用するブロック({})も式です:
ファイル名: src/main.rs
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 [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; }
2番目に、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: consider removing this semicolon
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` due to previous error
メインのエラーメッセージである「型が合いません」でこのコードの根本的な問題が明らかになるでしょう。
関数plus_oneの定義では、i32型を返すと言っているのに、文は値に評価されないからです。このことは、
()、つまり空のタプルとして表現されています。それゆえに、何も戻り値がなく、これが関数定義と矛盾するので、
結果としてエラーになるわけです。この出力内で、コンパイラは問題を修正する手助けになりそうなメッセージも出していますね:
セミコロンを削除するよう提言しています。そして、そうすれば、エラーは直るわけです。
コメント
全プログラマは、自分のコードがわかりやすくなるよう努めますが、時として追加の説明が許されることもあります。 このような場合、プログラマは注釈またはコメントをソースコードに残し、コメントをコンパイラは無視しますが、 ソースコードを読む人間には有益なものと思えるでしょう。
こちらが単純なコメントです:
#![allow(unused)] fn main() { // hello, world }
Rustでは、コメントは2連スラッシュで始め、行の終わりまで続きます。コメントが複数行にまたがる場合、
各行に//を含める必要があります。こんな感じに:
#![allow(unused)] fn main() { // So we’re doing something complicated here, long enough that we need // multiple lines of comments to do it! Whew! Hopefully, this comment will // explain what’s going on. // ここで何か複雑なことをしていて、長すぎるから複数行のコメントが必要なんだ。 // ふう!願わくば、このコメントで何が起きているか説明されていると嬉しい。 }
コメントは、コードが書かれた行の末尾にも配置することができます:
Filename: src/main.rs
fn main() { let lucky_number = 7; // I’m feeling lucky today(今日はラッキーな気がするよ) }
しかし、こちらの形式のコメントの方が見かける機会は多いでしょう。注釈しようとしているコードの1行上に書く形式です:
ファイル名: src/main.rs
fn main() { // I’m feeling lucky today // 今日はラッキーな気がするよ let lucky_number = 7; }
Rustには他の種類のコメント、ドキュメントコメントもあり、それについては第14章で議論します。
制御フロー
条件が真かどうかによってコードを走らせるかどうかを決定したり、
条件が真の間繰り返しコードを走らせるか決定したりすることは、多くのプログラミング言語において、基本的な構成ブロックです。
Rustコードの実行フローを制御する最も一般的な文法要素は、if式とループです。
if式
if式によって、条件に依存して枝分かれをさせることができます。条件を与え、以下のように宣言します。 「もし条件が合ったら、この一連のコードを実行しろ。条件に合わなければ、この一連のコードは実行するな」と。
projectsディレクトリにbranchesという名のプロジェクトを作ってif式について掘り下げていきましょう。
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未満の値になっているかどうかをチェックします。
条件が真の時に実行したい一連のコードを条件式の直後に波かっこで包んで配置します。if式の条件式と紐付けられる一連のコードは、
時としてアームと呼ばれることがあります。
第2章の「予想と秘密の数字を比較する」の節で議論したmatch式のアームと同じです。
オプションとして、else式を含むこともでき(ここではそうしています)、これによりプログラムは、
条件式が偽になった時に実行するコードを与えられることになります。仮に、else式を与えずに条件式が偽になったら、
プログラムは単にifブロックを飛ばして次のコードを実行しにいきます。
このコードを走らせてみましょう; 以下のような出力を目の当たりにするはずです:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was true
numberの値を条件がfalseになるような値に変更してどうなるか確かめてみましょう:
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 [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"); // 数値は3です
}
}
今回、ifの条件式は3という値に評価され、コンパイラがエラーを投げます:
$ 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
| (bool型を予期したのに、整数変数が見つかりました)
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error
このエラーは、コンパイラは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"); // 数値は0以外の何かです } }
このコードを実行したら、number was something other than zeroと表示されるでしょう。
else ifで複数の条件を扱う
ifとelseを組み合わせてelse if式にすることで複数の条件を持たせることもできます。例です:
ファイル名: src/main.rs
fn main() { let number = 6; if number % 4 == 0 { // 数値は4で割り切れます println!("number is divisible by 4"); } else if number % 3 == 0 { // 数値は3で割り切れます println!("number is divisible by 3"); } else if number % 2 == 0 { // 数値は2で割り切れます println!("number is divisible by 2"); } else { // 数値は4、3、2で割り切れません println!("number is not divisible by 4, 3, or 2"); } }
このプログラムには、通り道が4つあります。実行後、以下のような出力を目の当たりにするはずです:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
number is divisible by 3
このプログラムを実行すると、if式が順番に吟味され、最初に条件が真になった本体が実行されます。
6は2で割り切れるものの、number is devisible by 2や、
elseブロックのnumber is not divisible by 4, 3, or 2という出力はされないことに注目してください。
それは、Rustが最初の真条件のブロックのみを実行し、
条件に合ったものが見つかったら、残りはチェックすらしないからです。
else if式を使いすぎると、コードがめちゃくちゃになってしまうので、1つ以上あるなら、
コードをリファクタリングしたくなるかもしれません。これらのケースに有用なmatchと呼ばれる、
強力なRustの枝分かれ文法要素については第6章で解説します。
let文内でif式を使う
ifは式なので、let文の右辺に持ってくることができます。リスト3-2のようにですね。
ファイル名: src/main.rs
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; // numberの値は、{}です println!("The value of number is: {}", number); }
リスト3-2: if式の結果を変数に代入する
このnumber変数は、if式の結果に基づいた値に束縛されます。このコードを走らせてどうなるか確かめてください:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [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アームは互換性のない値の型になり、
コンパイラがプログラム内で問題の見つかった箇所をズバリ指摘してくれます:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
(ifとelseの型に互換性がありません)
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| (整数変数を予期しましたが、&strが見つかりました)
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error
ifブロックの式は整数に評価され、elseブロックの式は文字列に評価されます。これでは動作しません。
変数は単独の型でなければならないからです。コンパイラは、コンパイル時にnumber変数の型を確実に把握する必要があるため、
コンパイル時にnumberが使われている箇所全部で型が有効かどうか検査することができるのです。
numberの型が実行時にしか決まらないのであれば、コンパイラはそれを実行することができなくなってしまいます;
どの変数に対しても、架空の複数の型があることを追いかけなければならないのであれば、コンパイラはより複雑になり、
コードに対して行える保証が少なくなってしまうでしょう。
ループでの繰り返し
一連のコードを1回以上実行できると、しばしば役に立ちます。この作業用に、 Rustにはいくつかのループが用意されています。ループは、本体内のコードを最後まで実行し、 直後にまた最初から処理を開始します。 ループを試してみるのに、loopsという名の新プロジェクトを作りましょう。
Rustには3種類のループが存在します: loopとwhileとforです。それぞれ試してみましょう。
loopでコードを繰り返す
loopキーワードを使用すると、同じコードを何回も何回も永遠に、明示的にやめさせるまで実行します。
例として、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 [unoptimized + debuginfo] target(s) in 0.29 secs
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
^Cという記号が出た場所が、ctrl-cを押した場所です。^Cの後にはagain!と表示されたり、
されなかったりします。ストップシグナルをコードが受け取った時にループのどこにいたかによります。
幸いなことに、Rustにはループを抜け出す別のより信頼できる手段があります。
ループ内にbreakキーワードを配置することで、プログラムに実行を終了すべきタイミングを教えることができます。
第2章の「正しい予想をした後に終了する」節の数当てゲーム内でこれをして、ユーザが予想を的中させ、
ゲームに勝った時にプログラムを終了させたことを思い出してください。
数当てゲームでcontinueを使用しました。continueはループの中で残っているコードをスキップして次のループに移るためのものです。
ループ内にループがある場合、breakとcontinueは最も内側のループに適用されます。
ループラベルを使用することで、breakやcontinueが適用されるループを指定することができます。
以下に例を示します。
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 [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で条件付きループ
プログラムにとってループ内で条件式を評価できると、有益なことがしばしばあります。条件が真の間、
ループが走るわけです。条件が真でなくなった時にプログラムはbreakを呼び出し、ループを終了します。
このタイプのループは、loop、if、else、breakを組み合わせることでも実装できます;
お望みなら、プログラムで今、試してみるのもいいでしょう。
しかし、このパターンは頻出するので、Rustにはそれ用の文法要素が用意されていて、whileループと呼ばれます。
リスト3-3は、whileを使用しています: プログラムは3回ループし、それぞれカウントダウンします。
それから、ループ後に別のメッセージを表示して終了します:
ファイル名: src/main.rs
fn main() { let mut number = 3; while number != 0 { println!("{}!", number); number -= 1; } // 発射! println!("LIFTOFF!!!"); }
リスト3-3: 条件が真の間、コードを走らせるwhileループを使用する
この文法要素により、loop、if、else、breakを使った時に必要になるネストがなくなり、
より明確になります。条件が真の間、コードは実行されます; そうでなければ、ループを抜けます.
forでコレクションを覗き見る
while要素を使って配列などのコレクションの要素を覗き見ることができます。例えば、リスト3-4を見ましょう。
ファイル名: src/main.rs
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { // 値は{}です println!("the value is: {}", a[index]); index += 1; } }
リスト3-4: whileループでコレクションの各要素を覗き見る
ここで、コードは配列の要素を順番にカウントアップして覗いています。番号0から始まり、
配列の最終番号に到達するまでループします(つまり、index < 5が真でなくなる時です)。
このコードを走らせると、配列内の全要素が出力されます:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [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番目の値を拾おうとする前にループは実行を終了します。
しかし、このアプローチは間違いが発生しやすいです; 添え字の長さが間違っていれば、 プログラムはパニックしてしまいます。また遅いです。 コンパイラが実行時にループの各回ごとに境界値チェックを行うようなコードを追加するからです。
より効率的な対立案として、forループを使ってコレクションの各アイテムに対してコードを実行することができます。
forループはリスト3-5のこんな見た目です。
ファイル名: src/main.rs
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {}", element); } }
リスト3-5: forループを使ってコレクションの各要素を覗き見る
このコードを走らせたら、リスト3-4と同じ出力が得られるでしょう。より重要なのは、 コードの安全性を向上させ、配列の終端を超えてアクセスしたり、 終端に届く前にループを終えてアイテムを見逃してしまったりするバグの可能性を完全に排除したことです。
例えば、リスト3-4のコードで、a配列からアイテムを1つ削除したのに、条件式をwhile index < 4にするのを忘れていたら、
コードはパニックします。forループを使っていれば、配列の要素数を変えても、
他のコードをいじることを覚えておく必要はなくなるわけです。
forループのこの安全性と簡潔性により、Rustで使用頻度の最も高いループになっています。
リスト3-3でwhileループを使ったカウントダウンサンプルのように、一定の回数、同じコードを実行したいような状況であっても、
多くのRustaceanは、forループを使うでしょう。どうやってやるかといえば、
Range型を使うのです。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では第3の選択肢を取っています: メモリは、コンパイラがコンパイル時にチェックする一定の規則とともに所有権システムを通じて管理されています。 どの所有権機能も、実行中にプログラムの動作を遅くすることはありません。
所有権は多くのプログラマにとって新しい概念なので、慣れるまでに時間がかかります。 嬉しいことに、Rustと、所有権システムの規則の経験を積むと、より自然に安全かつ効率的なコードを構築できるようになります。 その調子でいきましょう!
所有権を理解した時、Rustを際立たせる機能の理解に対する強固な礎を得ることになるでしょう。この章では、 非常に一般的なデータ構造に着目した例を取り扱うことで所有権を学んでいきます: 文字列です。
スタックとヒープ
多くのプログラミング言語において、スタックとヒープについて考える機会はそう多くないでしょう。 しかし、Rustのようなシステムプログラミング言語においては、値がスタックに積まれるかヒープに置かれるかは、 言語の振る舞い方や、特定の決断を下す理由などに影響以上のものを与えるのです。 この章の後半でスタックとヒープを交えて所有権の一部が解説されるので、ここでちょっと予行演習をしておきましょう。
スタックもヒープも、実行時にコードが使用できるメモリの一部になりますが、異なる手段で構成されています。 スタックは、得た順番に値を並べ、逆の順で値を取り除いていきます。これは、 last in, first out(
訳注: あえて日本語にするなら、「最後に入れたものが最初に出てくる」といったところでしょうか)と呼ばれます。 お皿の山を思い浮かべてください: お皿を追加する時には、山の一番上に置き、お皿が必要になったら、一番上から1枚を取り去りますよね。 途中や一番下に追加したり、取り除いたりすることもできません。データを追加することは、 スタックにpushするといい、データを取り除くことは、スタックからpopすると表現します(訳注: 日本語では単純に英語をそのまま活用してプッシュ、ポップと表現するでしょう)。データへのアクセス方法のおかげで、スタックは高速です: 新しいデータを置いたり、 データを取得する場所を探す必要が絶対にないわけです。というのも、その場所は常に一番上だからですね。 スタックを高速にする特性は他にもあり、それはスタック上のデータは全て既知の固定サイズでなければならないということです。
コンパイル時にサイズがわからなかったり、サイズが可変のデータについては、代わりにヒープに格納することができます。 ヒープは、もっとごちゃごちゃしています: ヒープにデータを置く時、あるサイズのスペースを求めます。 OSはヒープ上に十分な大きさの空の領域を見つけ、使用中にし、ポインタを返します。ポインタとは、その場所へのアドレスです。 この過程は、ヒープに領域を確保する(allocating on the heap)と呼ばれ、時としてそのフレーズを単にallocateするなどと省略したりします。 (
訳注: こちらもこなれた日本語訳はないでしょう。allocateは「メモリを確保する」と訳したいところですが) スタックに値を積むことは、メモリ確保とは考えられません。ポインタは、既知の固定サイズなので、 スタックに保管することができますが、実データが必要になったら、ポインタを追いかける必要があります。レストランで席を確保することを考えましょう。入店したら、グループの人数を告げ、 店員が全員座れる空いている席を探し、そこまで誘導します。もしグループの誰かが遅れて来るのなら、 着いた席の場所を尋ねてあなたを発見することができます。
ヒープへのデータアクセスは、スタックのデータへのアクセスよりも低速です。 ポインタを追って目的の場所に到達しなければならないからです。現代のプロセッサは、メモリをあちこち行き来しなければ、 より速くなります。似た例えを続けましょう。レストランで多くのテーブルから注文を受ける給仕人を考えましょう。最も効率的なのは、 次のテーブルに移らずに、一つのテーブルで全部の注文を受け付けてしまうことです。テーブルAで注文を受け、 それからテーブルBの注文、さらにまたA、それからまたBと渡り歩くのは、かなり低速な過程になってしまうでしょう。 同じ意味で、プロセッサは、 データが隔離されている(ヒープではそうなっている可能性がある)よりも近くにある(スタックではこうなる)ほうが、 仕事をうまくこなせるのです。ヒープに大きな領域を確保する行為も時間がかかることがあります。
コードが関数を呼び出すと、関数に渡された値(ヒープのデータへのポインタも含まれる可能性あり)と、 関数のローカル変数がスタックに載ります。関数の実行が終了すると、それらの値はスタックから取り除かれます。
どの部分のコードがどのヒープ上のデータを使用しているか把握すること、ヒープ上の重複するデータを最小化すること、 メモリ不足にならないようにヒープ上の未使用のデータを掃除することは全て、所有権が解決する問題です。 一度所有権を理解したら、あまり頻繁にスタックとヒープに関して考える必要はなくなるでしょうが、 ヒープデータを管理することが所有権の存在する理由だと知っていると、所有権がありのままで動作する理由を 説明するのに役立つこともあります。
所有権規則
まず、所有権のルールについて見ていきましょう。 この規則を具体化する例を扱っていく間もこれらのルールを肝に銘じておいてください:
- Rustの各値は、所有者と呼ばれる変数と対応している。
- いかなる時も所有者は一つである。
- 所有者がスコープから外れたら、値は破棄される。
変数スコープ
第2章で、Rustプログラムの例はすでに見ています。もう基本的な記法は通り過ぎたので、
fn main() {というコードはもう例に含みません。従って、例をなぞっているなら、
これからの例はmain関数に手動で入れ込まなければいけなくなるでしょう。結果的に、例は少々簡潔になり、
定型コードよりも具体的な詳細に集中しやすくなります。
所有権の最初の例として、何らかの変数のスコープについて見ていきましょう。スコープとは、 要素が有効になるプログラム内の範囲のことです。以下のような変数があるとしましょう:
#![allow(unused)] fn main() { let s = "hello"; }
変数sは、文字列リテラルを参照し、ここでは、文字列の値はプログラムのテキストとしてハードコードされています。
この変数は、宣言された地点から、現在のスコープの終わりまで有効になります。リスト4-1には、
変数sが有効な場所に関する注釈がコメントで付記されています。
#![allow(unused)] fn main() { { // sは、ここでは有効ではない。まだ宣言されていない let s = "hello"; // sは、ここから有効になる // sで作業をする } // このスコープは終わり。もうsは有効ではない }
リスト4-1: 変数と有効なスコープ
言い換えると、ここまでに重要な点は二つあります:
sがスコープに入ると、有効になる- スコープを抜けるまで、有効なまま
ここで、スコープと変数が有効になる期間の関係は、他の言語に類似しています。さて、この理解のもとに、
String型を導入して構築していきましょう。
String型
所有権の規則を具体化するには、第3章の「データ型」節で講義したものよりも、より複雑なデータ型が必要になります。 以前講義した型は全てスタックに保管され、スコープが終わるとスタックから取り除かれますが、 ヒープに確保されるデータ型を観察して、 コンパイラがどうそのデータを掃除すべきタイミングを把握しているかを掘り下げていきたいと思います。
ここでは、例としてString型を使用し、String型の所有権にまつわる部分に着目しましょう。
また、この観点は、標準ライブラリや自分で生成する他の複雑なデータ型にも適用されます。
String型については、第8章でより深く議論します。
既に文字列リテラルは見かけましたね。文字列リテラルでは、文字列の値はプログラムにハードコードされます。
文字列リテラルは便利ですが、テキストを使いたいかもしれない場面全てに最適なわけではありません。一因は、
文字列リテラルが不変であることに起因します。別の原因は、コードを書く際に、全ての文字列値が判明するわけではないからです:
例えば、ユーザ入力を受け付け、それを保持したいとしたらどうでしょうか?このような場面用に、Rustには、
2種類目の文字列型、String型があります。この型はヒープにメモリを確保するので、
コンパイル時にはサイズが不明なテキストも保持することができるのです。from関数を使用して、
文字列リテラルからString型を生成できます。以下のように:
#![allow(unused)] fn main() { let s = String::from("hello"); }
この二重コロンは、string_fromなどの名前を使うのではなく、
String型直下のfrom関数を特定する働きをする演算子です。この記法について詳しくは、
第5章の「メソッド記法」節と、第7章の「モジュール定義」でモジュールを使った名前空間分けについて話をするときに議論します。
この種の文字列は、可変化することができます:
#![allow(unused)] fn main() { let mut s = String::from("hello"); s.push_str(", world!"); // push_str()関数は、リテラルをStringに付け加える println!("{}", s); // これは`hello, world!`と出力する }
では、ここでの違いは何でしょうか?なぜ、String型は可変化できるのに、リテラルはできないのでしょうか?
違いは、これら二つの型がメモリを扱う方法にあります。
メモリと確保
文字列リテラルの場合、中身はコンパイル時に判明しているので、テキストは最終的なバイナリファイルに直接ハードコードされます。 このため、文字列リテラルは、高速で効率的になるのです。しかし、これらの特性は、 その文字列リテラルの不変性にのみ端を発するものです。残念なことに、コンパイル時にサイズが不明だったり、 プログラム実行に合わせてサイズが可変なテキスト片用に一塊のメモリをバイナリに確保しておくことは不可能です。
String型では、可変かつ伸長可能なテキスト破片をサポートするために、コンパイル時には不明な量のメモリを
ヒープに確保して内容を保持します。つまり:
- メモリは、実行時にOSに要求される。
String型を使用し終わったら、OSにこのメモリを返還する方法が必要である。
この最初の部分は、既にしています: String::from関数を呼んだら、その実装が必要なメモリを要求するのです。
これは、プログラミング言語において、極めて普遍的です。
しかしながら、2番目の部分は異なります。ガベージコレクタ(GC)付きの言語では、GCがこれ以上、
使用されないメモリを検知して片付けるため、プログラマは、そのことを考慮する必要はありません。
GCがないなら、メモリがもう使用されないことを見計らって、明示的に返還するコードを呼び出すのは、
プログラマの責任になります。ちょうど要求の際にしたようにですね。これを正確にすることは、
歴史的にも難しいプログラミング問題の一つであり続けています。もし、忘れていたら、メモリを無駄にします。
タイミングが早すぎたら、無効な変数を作ってしまいます。2回解放してしまっても、バグになるわけです。
allocateとfreeは完璧に1対1対応にしなければならないのです。
Rustは、異なる道を歩んでいます: ひとたび、メモリを所有している変数がスコープを抜けたら、
メモリは自動的に返還されます。こちらの例は、
リスト4-1のスコープ例を文字列リテラルからString型を使うものに変更したバージョンになります:
#![allow(unused)] fn main() { { let s = String::from("hello"); // sはここから有効になる // sで作業をする } // このスコープはここでおしまい。sは // もう有効ではない }
String型が必要とするメモリをOSに返還することが自然な地点があります: s変数がスコープを抜ける時です。
変数がスコープを抜ける時、Rustは特別な関数を呼んでくれます。この関数は、dropと呼ばれ、
ここにString型の書き手はメモリを返還するコードを配置することができます。Rustは、閉じ波括弧で自動的にdrop関数を呼び出します。
注釈: C++では、要素の生存期間の終了地点でリソースを解放するこのパターンを時に、 RAII(Resource Aquisition Is Initialization: リソースの獲得は、初期化である)と呼んだりします。 Rustの
drop関数は、あなたがRAIIパターンを使ったことがあれば、馴染み深いものでしょう。
このパターンは、Rustコードの書かれ方に甚大な影響をもたらします。現状は簡単そうに見えるかもしれませんが、 ヒープ上に確保されたデータを複数の変数に使用させるようなもっと複雑な場面では、コードの振る舞いは、 予期しないものになる可能性もあります。これから、そのような場面を掘り下げてみましょう。
変数とデータの相互作用法: ムーブ
Rustにおいては、複数の変数が同じデータに対して異なる手段で相互作用することができます。 整数を使用したリスト4-2の例を見てみましょう。
#![allow(unused)] fn main() { let x = 5; let y = x; }
リスト4-2: 変数xの整数値をyに代入する
もしかしたら、何をしているのか予想することができるでしょう:
「値5をxに束縛する; それからxの値をコピーしてyに束縛する。」これで、
二つの変数(xとy)が存在し、両方、値は5になりました。これは確かに起こっている現象を説明しています。
なぜなら、整数は既知の固定サイズの単純な値で、これら二つの5という値は、スタックに積まれるからです。
では、Stringバージョンを見ていきましょう:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; }
このコードは先ほどのコードに酷似していますので、動作方法も同じだと思い込んでしまうかもしれません:
要するに、2行目でs1の値をコピーし、s2に束縛するということです。ところが、
これは全く起こることを言い当てていません。
図4-1を見て、ベールの下でStringに何が起きているかを確かめてください。
String型は、左側に示されているように、3つの部品でできています:
文字列の中身を保持するメモリへのポインタと長さ、そして、許容量です。この種のデータは、スタックに保持されます。
右側には、中身を保持したヒープ上のメモリがあります。
図4-1: s1に束縛された"hello"という値を保持するStringのメモリ上の表現
長さは、String型の中身が現在使用しているメモリ量をバイトで表したものです。許容量は、
String型がOSから受け取った全メモリ量をバイトで表したものです。長さと許容量の違いは問題になることですが、
この文脈では違うので、とりあえずは、許容量を無視しても構わないでしょう。
s1をs2に代入すると、String型のデータがコピーされます。つまり、スタックにあるポインタ、長さ、
許容量をコピーするということです。ポインタが指すヒープ上のデータはコピーしません。言い換えると、
メモリ上のデータ表現は図4-2のようになるということです。
図4-2: s1のポインタ、長さ、許容量のコピーを保持する変数s2のメモリ上での表現
メモリ上の表現は、図4-3のようにはなりません。これは、
Rustが代わりにヒープデータもコピーするという選択をしていた場合のメモリ表現ですね。Rustがこれをしていたら、
ヒープ上のデータが大きい時にs2 = s1という処理の実行時性能がとても悪くなっていた可能性があるでしょう。
図4-3: Rustがヒープデータもコピーしていた場合にs2 = s1という処理が行なった可能性のあること
先ほど、変数がスコープを抜けたら、Rustは自動的にdrop関数を呼び出し、
その変数が使っていたヒープメモリを片付けると述べました。しかし、図4-2は、
両方のデータポインタが同じ場所を指していることを示しています。これは問題です: s2とs1がスコープを抜けたら、
両方とも同じメモリを解放しようとします。これは二重解放エラーとして知られ、以前触れたメモリ安全性上のバグの一つになります。
メモリを2回解放することは、memory corruption (訳注: メモリの崩壊。意図せぬメモリの書き換え) につながり、
セキュリティ上の脆弱性を生む可能性があります。
メモリ安全性を保証するために、Rustにおいてこの場面で起こることの詳細がもう一つあります。
確保されたメモリをコピーしようとする代わりに、コンパイラは、s1が最早有効ではないと考え、
故にs1がスコープを抜けた際に何も解放する必要がなくなるわけです。s2の生成後にs1を使用しようとしたら、
どうなるかを確認してみましょう。動かないでしょう:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
コンパイラが無効化された参照は使用させてくれないので、以下のようなエラーが出るでしょう:
error[E0382]: use of moved value: `s1`
(ムーブされた値の使用: `s1`)
--> src/main.rs:5:28
|
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value used here after move
| (ムーブ後にここで使用されています)
|
= note: move occurs because `s1` has type `std::string::String`, which does
not implement the `Copy` trait
(注釈: ムーブが起きたのは、`s1`が`std::string::String`という
`Copy`トレイトを実装していない型だからです)
他の言語を触っている間に"shallow copy"と"deep copy"という用語を耳にしたことがあるなら、
データのコピーなしにポインタと長さ、許容量をコピーするという概念は、shallow copyのように思えるかもしれません。
ですが、コンパイラは最初の変数をも無効化するので、shallow copyと呼ばれる代わりに、
ムーブとして知られているわけです。この例では、s1はs2にムーブされたと表現するでしょう。
以上より、実際に起きることを図4-4に示してみました。
図4-4: s1が無効化された後のメモリ表現
これにて一件落着です。s2だけが有効なので、スコープを抜けたら、それだけがメモリを解放して、
終わりになります。
付け加えると、これにより暗示される設計上の選択があります: Rustでは、 自動的にデータの"deep copy"が行われることは絶対にないわけです。それ故に、あらゆる自動コピーは、実行時性能の観点で言うと、 悪くないと考えてよいことになります。
変数とデータの相互作用法: クローン
仮に、スタック上のデータだけでなく、本当にString型のヒープデータのdeep copyが必要ならば、
cloneと呼ばれるよくあるメソッドを使うことができます。メソッド記法については第5章で議論しますが、
メソッドは多くのプログラミング言語に見られる機能なので、以前に見かけたこともあるんじゃないでしょうか。
これは、cloneメソッドの動作例です:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); }
これは問題なく動作し、図4-3で示した動作を明示的に生み出します。ここでは、 ヒープデータが実際にコピーされています。
cloneメソッドの呼び出しを見かけたら、何らかの任意のコードが実行され、その実行コストは高いと把握できます。
何か違うことが起こっているなと見た目でわかるわけです。
スタックのみのデータ: コピー
まだ話題にしていない別の問題があります。 この整数を使用したコードは、一部をリスト4-2で示しましたが、うまく動作する有効なものです:
#![allow(unused)] fn main() { let x = 5; let y = x; println!("x = {}, y = {}", x, y); }
ですが、このコードは一見、今学んだことと矛盾しているように見えます:
cloneメソッドの呼び出しがないのに、xは有効で、yにムーブされませんでした。
その理由は、整数のようなコンパイル時に既知のサイズを持つ型は、スタック上にすっぽり保持されるので、
実際の値をコピーするのも高速だからです。これは、変数yを生成した後にもxを無効化したくなる理由がないことを意味します。
換言すると、ここでは、shallow copyとdeep copyの違いがないことになり、
cloneメソッドを呼び出しても、一般的なshallow copy以上のことをしなくなり、
そのまま放置しておけるということです。
RustにはCopyトレイトと呼ばれる特別な注釈があり、
整数のようなスタックに保持される型に対して配置することができます(トレイトについては第10章でもっと詳しく話します)。
型がCopyトレイトに適合していれば、代入後も古い変数が使用可能になります。コンパイラは、
型やその一部分でもDropトレイトを実装している場合、Copyトレイトによる注釈をさせてくれません。
型の値がスコープを外れた時に何か特別なことを起こす必要がある場合に、Copy注釈を追加すると、コンパイルエラーが出ます。
型にCopy注釈をつける方法について学ぶには、付録Cの「導出可能なトレイト」をご覧ください。
では、どの型がCopyなのでしょうか?ある型について、ドキュメントをチェックすればいいのですが、
一般規則として、単純なスカラー値の集合は何でもCopyであり、メモリ確保が必要だったり、
何らかの形態のリソースだったりするものはCopyではありません。ここにCopyの型の一部を並べておきます。
- あらゆる整数型。
u32など。 - 論理値型である
bool。trueとfalseという値がある。 - あらゆる浮動小数点型、
f64など。 - 文字型である
char。 - タプル。ただ、
Copyの型だけを含む場合。例えば、(i32, i32)はCopyだが、(i32, String)は違う。
所有権と関数
意味論的に、関数に値を渡すことと、値を変数に代入することは似ています。関数に変数を渡すと、 代入のようにムーブやコピーされます。リスト4-3は変数がスコープに入ったり、 抜けたりする地点について注釈してある例です。
ファイル名: src/main.rs
fn main() { let s = String::from("hello"); // sがスコープに入る takes_ownership(s); // sの値が関数にムーブされ... // ... ここではもう有効ではない let x = 5; // xがスコープに入る makes_copy(x); // xも関数にムーブされるが、 // i32はCopyなので、この後にxを使っても // 大丈夫 } // ここでxがスコープを抜け、sもスコープを抜ける。ただし、sの値はムーブされているので、何も特別なことは起こらない。 // fn takes_ownership(some_string: String) { // some_stringがスコープに入る。 println!("{}", some_string); } // ここでsome_stringがスコープを抜け、`drop`が呼ばれる。後ろ盾してたメモリが解放される。 // fn makes_copy(some_integer: i32) { // some_integerがスコープに入る println!("{}", some_integer); } // ここでsome_integerがスコープを抜ける。何も特別なことはない。
リスト4-3: 所有権とスコープが注釈された関数群
takes_ownershipの呼び出し後にsを呼び出そうとすると、コンパイラは、コンパイルエラーを投げるでしょう。
これらの静的チェックにより、ミスを犯さないでいられます。sやxを使用するコードをmainに追加してみて、
どこで使えて、そして、所有権規則により、どこで使えないかを確認してください。
戻り値とスコープ
値を返すことでも、所有権は移動します。リスト4-4は、リスト4-3と似た注釈のついた例です。
ファイル名: src/main.rs
fn main() { let s1 = gives_ownership(); // gives_ownershipは、戻り値をs1に // ムーブする let s2 = String::from("hello"); // s2がスコープに入る let s3 = takes_and_gives_back(s2); // s2はtakes_and_gives_backにムーブされ // 戻り値もs3にムーブされる } // ここで、s3はスコープを抜け、ドロップされる。s2もスコープを抜けるが、ムーブされているので、 // 何も起きない。s1もスコープを抜け、ドロップされる。 fn gives_ownership() -> String { // gives_ownershipは、戻り値を // 呼び出した関数にムーブする let some_string = String::from("hello"); // some_stringがスコープに入る some_string // some_stringが返され、呼び出し元関数に // ムーブされる } // takes_and_gives_backは、Stringを一つ受け取り、返す。 fn takes_and_gives_back(a_string: String) -> String { // a_stringがスコープに入る。 a_string // a_stringが返され、呼び出し元関数にムーブされる }
リスト4-4: 戻り値の所有権を移動する
変数の所有権は、毎回同じパターンを辿っています: 別の変数に値を代入すると、ムーブされます。
ヒープにデータを含む変数がスコープを抜けると、データが別の変数に所有されるようムーブされていない限り、
dropにより片付けられるでしょう。
所有権を取り、またその所有権を戻す、ということを全ての関数でしていたら、ちょっとめんどくさいですね。 関数に値は使わせるものの所有権を取らないようにさせるにはどうするべきでしょうか。 返したいと思うかもしれない関数本体で発生したあらゆるデータとともに、再利用したかったら、渡されたものをまた返さなきゃいけないのは、 非常に煩わしいことです。
タプルで、複数の値を返すことは可能です。リスト4-5のようにですね。
ファイル名: src/main.rs
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); //'{}'の長さは、{}です println!("The length of '{}' is {}.", s2, len); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len()メソッドは、Stringの長さを返します (s, length) }
リスト4-5: 引数の所有権を返す
でも、これでは、大袈裟すぎますし、ありふれているはずの概念に対して、作業量が多すぎます。 私たちにとって幸運なことに、Rustにはこの概念に対する機能があり、参照と呼ばれます。
参照と借用
リスト4-5のタプルコードの問題は、String型を呼び出し元の関数に戻さないと、calculate_lengthを呼び出した後に、
Stringオブジェクトが使えなくなることであり、これはStringオブジェクトがcalculate_lengthにムーブされてしまうためでした。
ここで、値の所有権をもらう代わりに引数としてオブジェクトへの参照を取るcalculate_length関数を定義し、
使う方法を見てみましょう:
ファイル名: src/main.rs
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // '{}'の長さは、{}です println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
まず、変数宣言と関数の戻り値にあったタプルコードは全てなくなったことに気付いてください。
2番目に、&s1をcalcuate_lengthに渡し、その定義では、String型ではなく、&Stringを受け取っていることに注目してください。
これらのアンド記号が参照であり、これのおかげで所有権をもらうことなく値を参照することができるのです。 図4-5はその図解です。
図4-5: String s1を指す&String sの図表
注釈:
&による参照の逆は、参照外しであり、参照外し演算子の*で達成できます。 第8章で参照外し演算子の使用例を眺め、第15章で参照外しについて詳しく議論します。
ここの関数呼び出しについて、もっと詳しく見てみましょう:
#![allow(unused)] fn main() { fn calculate_length(s: &String) -> usize { s.len() } let s1 = String::from("hello"); let len = calculate_length(&s1); }
この&s1という記法により、s1の値を参照する参照を生成することができますが、これを所有することはありません。
所有してないということは、指している値は、参照がスコープを抜けてもドロップされないということです。
同様に、関数のシグニチャでも、&を使用して引数sの型が参照であることを示しています。
説明的な注釈を加えてみましょう:
#![allow(unused)] fn main() { fn calculate_length(s: &String) -> usize { // sはStringへの参照 s.len() } // ここで、sはスコープ外になる。けど、参照しているものの所有権を持っているわけではないので // 何も起こらない }
変数sが有効なスコープは、あらゆる関数の引数のものと同じですが、所有権はないので、sがスコープを抜けても、
参照が指しているものをドロップすることはありません。関数が実際の値の代わりに参照を引数に取ると、
所有権をもらわないので、所有権を返す目的で値を返す必要はありません。
関数の引数に参照を取ることを借用と呼びます。現実生活のように、誰かが何かを所有していたら、 それを借りることができます。用が済んだら、返さなきゃいけないわけです。
では、借用した何かを変更しようとしたら、どうなるのでしょうか?リスト4-6のコードを試してください。 ネタバレ注意: 動きません!
ファイル名: src/main.rs
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
リスト4-6: 借用した値を変更しようと試みる
これがエラーです:
error[E0596]: cannot borrow immutable borrowed content `*some_string` as mutable
(エラー: 不変な借用をした中身`*some_string`を可変で借用できません)
--> error.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- use `&mut String` here to make mutable
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ cannot borrow as mutable
変数が標準で不変なのと全く同様に、参照も不変なのです。参照している何かを変更することは叶わないわけです。
可変な参照
一捻り加えるだけでリスト4-6のコードのエラーは解決します:
ファイル名: src/main.rs
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
始めに、sをmutに変えなければなりませんでした。そして、&mut sで可変な参照を生成し、
some_string: &mut Stringで可変な参照を受け入れなければなりませんでした。
ところが、可変な参照には大きな制約が一つあります: 特定のスコープで、ある特定のデータに対しては、 一つしか可変な参照を持てないことです。こちらのコードは失敗します:
ファイル名: src/main.rs
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
(エラー: 一度に`s`を可変として2回以上借用することはできません)
--> 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
error: aborting due to previous error
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership`
To learn more, run the command again with --verbose.
この制約は、可変化を許可するものの、それを非常に統制の取れた形で行えます。これは、新たなRustaceanにとっては、 壁です。なぜなら、多くの言語では、いつでも好きな時に可変化できるからです。
この制約がある利点は、コンパイラがコンパイル時にデータ競合を防ぐことができる点です。 データ競合とは、競合条件と類似していて、これら3つの振る舞いが起きる時に発生します:
- 2つ以上のポインタが同じデータに同時にアクセスする。
- 少なくとも一つのポインタがデータに書き込みを行っている。
- データへのアクセスを同期する機構が使用されていない。
データ競合は未定義の振る舞いを引き起こし、実行時に追いかけようとした時に特定し解決するのが難しい問題です。 しかし、Rustは、データ競合が起こるコードをコンパイルさえしないので、この問題が発生しないようにしてくれるわけです。
いつものように、波かっこを使って新しいスコープを生成し、同時並行なものでなく、複数の可変な参照を作ることができます。
#![allow(unused)] fn main() { let mut s = String::from("hello"); { let r1 = &mut s; } // r1はここでスコープを抜けるので、問題なく新しい参照を作ることができる let r2 = &mut s; }
可変と不変な参照を組み合わせることに関しても、似たような規則が存在しています。このコードはエラーになります:
let mut s = String::from("hello");
let r1 = &s; // 問題なし
let r2 = &s; // 問題なし
let r3 = &mut s; // 大問題!
これがエラーです:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
(エラー: `s`は不変で借用されているので、可変で借用できません)
--> borrow_thrice.rs:6:19
|
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 | }
| - immutable borrow ends here
ふう!さらに不変な参照をしている間は、可変な参照をすることはできません。不変参照の使用者は、 それ以降に値が突然変わることなんて予想してません!しかしながら、複数の不変参照をすることは可能です。 データを読み込んでいるだけの人に、他人がデータを読み込むことに対して影響を与える能力はないからです。
これらのエラーは、時としてイライラするものではありますが、Rustコンパイラがバグの可能性を早期に指摘してくれ(それも実行時ではなくコンパイル時に)、 問題の発生箇所をズバリ示してくれるのだと覚えておいてください。そうして想定通りにデータが変わらない理由を追いかける必要がなくなります。
宙に浮いた参照
ポインタのある言語では、誤ってダングリングポインタを生成してしまいやすいです。ダングリングポインタとは、 他人に渡されてしまった可能性のあるメモリを指すポインタのことであり、その箇所へのポインタを保持している間に、 メモリを解放してしまうことで発生します。対照的にRustでは、コンパイラが、 参照がダングリング参照に絶対ならないよう保証してくれます: つまり、何らかのデータへの参照があったら、 コンパイラは参照がスコープを抜けるまで、データがスコープを抜けることがないよう確認してくれるわけです。
ダングリング参照作りを試してみますが、コンパイラはこれをコンパイルエラーで阻止します:
ファイル名: src/main.rs
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
こちらがエラーです:
error[E0106]: missing lifetime specifier
(エラー: ライフタイム指定子がありません)
--> main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected 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 giving it a 'static lifetime
('staticライフタイムを与えることを考慮してみてください)
このエラーメッセージは、まだ講義していない機能について触れています: ライフタイムです。 ライフタイムについては第10章で詳しく議論しますが、ライフタイムに関する部分を無視すれば、 このメッセージは、確かにこのコードが問題になる理由に関する鍵を握っています:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from.
dangleコードの各段階で一体何が起きているのかを詳しく見ていきましょう:
ファイル名: src/main.rs
fn dangle() -> &String { // dangleはStringへの参照を返す
let s = String::from("hello"); // sは新しいString
&s // String sへの参照を返す
} // ここで、sはスコープを抜け、ドロップされる。そのメモリは消される。
// 危険だ
sは、dangle内で生成されているので、dangleのコードが終わったら、sは解放されてしまいますが、
そこへの参照を返そうとしました。つまり、この参照は無効なStringを指していると思われるのです。
よくないことです!コンパイラは、これを阻止してくれるのです。
ここでの解決策は、Stringを直接返すことです:
#![allow(unused)] fn main() { fn no_dangle() -> String { let s = String::from("hello"); s } }
これは何の問題もなく動きます。所有権はムーブされ、何も解放されることはありません。
参照の規則
参照について議論したことを再確認しましょう:
- 任意のタイミングで、一つの可変参照か不変な参照いくつでものどちらかを行える。
- 参照は常に有効でなければならない。
次は、違う種類の参照を見ていきましょう: スライスです。
スライス型
所有権のない別のデータ型は、スライスです。スライスにより、コレクション全体ではなく、 その内の一連の要素を参照することができます。
ちょっとしたプログラミングの問題を考えてみましょう: 文字列を受け取って、その文字列中の最初の単語を返す関数を書いてください。 関数が文字列中に空白を見つけられなかったら、文字列全体が一つの単語に違いないので、文字列全体が返されるべきです。
この関数のシグニチャについて考えてみましょう:
fn first_word(s: &String) -> ?
この関数、first_wordは引数に&Stringをとります。所有権はいらないので、これで十分です。
ですが、何を返すべきでしょうか?文字列の一部について語る方法が全くありません。しかし、
単語の終端の添え字を返すことができますね。リスト4-7に示したように、その方法を試してみましょう。
ファイル名: src/main.rs
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() {}
リスト4-7: String引数へのバイト数で表された添え字を返すfirst_word関数
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から返ってくるタプルの第1要素は、添え字であり、2番目の要素は、(コレクションの)要素への参照になります。
これは、手動で添え字を計算するよりも少しだけ便利です。
enumerateメソッドがタプルを返すので、Rustのあらゆる場所同様、パターンを使って、そのタプルを分配できます。
従って、forループ内で、タプルの添え字に対するiとタプルの1バイトに対応する&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のプログラムを考えてください。
ファイル名: src/main.rs
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 // wordの中身は、値5になる s.clear(); // this empties the String, making it equal to "" // Stringを空にする。つまり、""と等しくする // word still has the value 5 here, but there's no more string that // we could meaningfully use the value 5 with. word is now totally invalid! // wordはまだ値5を保持しているが、もうこの値を正しい意味で使用できる文字列は存在しない。 // wordは今や完全に無効なのだ! }
リスト4-8: first_word関数の呼び出し結果を保持し、Stringの中身を変更する
このプログラムは何のエラーもなくコンパイルが通り、wordをs.clear()の呼び出し後に使用しても、
コンパイルが通ります。wordはsの状態に全く関連づけられていないので、その中身はまだ値5のままです。
その値5を変数sに使用し、最初の単語を取り出そうとすることはできますが、これはバグでしょう。
というのも、sの中身は、5をwordに保存した後変わってしまったからです。
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全体への参照を取ることに似ていますが、余計な[0..5]という部分が付いています。
String全体への参照ではなく、Stringの一部への参照です。
[starting_index..ending_index]と指定することで、角かっこに範囲を使い、スライスを生成できます。
ここで、starting_indexはスライスの最初の位置、ending_indexはスライスの終端位置よりも、
1大きい値です。内部的には、スライスデータ構造は、開始地点とスライスの長さを保持しており、
スライスの長さはending_indexからstarting_indexを引いたものに対応します。以上より、
let world = &s[6..11];の場合には、worldはsの添え字6のバイトへのポインタと5という長さを持つスライスになるでしょう。
図4-6は、これを図解しています。
図4-6: Stringオブジェクトの一部を参照する文字列スライス
Rustの..という範囲記法で、最初の番号(ゼロ)から始めたければ、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文字境界に置かなければなりません。 マルチバイト文字の真ん中で文字列スライスを生成しようとしたら、エラーでプログラムは落ちるでしょう。 この節では文字列スライスを導入することが目的なので、ASCIIのみを想定しています; UTF-8に関するより徹底した議論は、 第8章の「文字列でUTF-8エンコードされたテキストを格納する」節で行います。
これらの情報を念頭に、first_wordを書き直してスライスを返すようにしましょう。
文字列スライスを意味する型は、&strと記述します:
ファイル名: src/main.rs
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 {
これで、ずっと混乱しにくい素直なAPIになりました。なぜなら、Stringへの参照が有効なままであることをコンパイラが、
保証してくれるからです。最初の単語の終端添え字を得た時に、
文字列を空っぽにして先ほどの添え字が無効になってしまったリスト4-8のプログラムのバグを覚えていますか?
そのコードは、論理的に正しくないのですが、即座にエラーにはなりませんでした。問題は後になってから発生し、
それは空の文字列に対して、最初の単語の添え字を使用し続けようとした時でした。スライスならこんなバグはあり得ず、
コードに問題があるなら、もっと迅速に判明します。スライスバージョンのfirst_wordを使用すると、
コンパイルエラーが発生します:
ファイル名: src/main.rs
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
(エラー: 不変として借用されているので、`s`を可変で借用できません)
--> 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
(不変借用はその後ここで使われています)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership`.
To learn more, run the command again with --verbose.
借用規則から、何かへの不変な参照がある時、さらに可変な参照を得ることはできないことを思い出してください。
clearはStringを切り詰める必要があるので、可変な参照を得る必要があります。Rustはこれを認めないので、コンパイルが失敗します。
RustのおかげでAPIが使いやすくなるだけでなく、ある種のエラー全てを完全にコンパイル時に排除してくれるのです!
文字列リテラルはスライスである
文字列は、バイナリに埋め込まれると話したことを思い出してください。今やスライスのことを知ったので、 文字列リテラルを正しく理解することができます。
#![allow(unused)] fn main() { let s = "Hello, world!"; }
ここでのsの型は、&strです: バイナリのその特定の位置を指すスライスです。
これは、文字列が不変である理由にもなっています。要するに、&strは不変な参照なのです。
引数としての文字列スライス
リテラルやString値のスライスを得ることができると知ると、first_wordに対して、もう一つ改善点を見出すことができます。
シグニチャです:
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
// first_wordは`String`のスライスに対して機能する
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word works on slices of string literals
// first_wordは文字列リテラルのスライスに対して機能する
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);
}
リスト4-9: s引数の型に文字列スライスを使用してfirst_word関数を改善する
もし、文字列スライスがあるなら、それを直接渡せます。Stringがあるなら、
そのString全体のスライスを渡せます。Stringへの参照の代わりに文字列スライスを取るよう関数を定義すると、
何も機能を失うことなくAPIをより一般的で有益なものにできるのです。
Filename: src/main.rs
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 // first_wordは`String`のスライスに対して機能する let word = first_word(&my_string[..]); let my_string_literal = "hello world"; // first_word works on slices of string literals // first_wordは文字列リテラルのスライスに対して機能する 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]; }
このスライスは、&[i32]という型になります。これも文字列スライスと同じように動作します。
つまり、最初の要素への参照と長さを保持するのです。
この種のスライスは、他のすべての種類のコレクションに対して使用することになるでしょう。
それらのコレクションについて、詳しくは、第8章でベクタについて話すときに議論します。
まとめ
所有権、借用、スライスの概念は、Rustプログラムにおいて、コンパイル時にメモリ安全性を保証します。 Rust言語も他のシステムプログラミング言語と同じように、メモリの使用法について制御させてくれるわけですが、 データの所有者がスコープを抜けたときに、所有者に自動的にデータを片付けさせることは、この制御をするために、 余計なコードを書いたりデバッグしたりする必要がないことを意味します。
所有権は、Rustの他のいろんな部分が動作する方法に影響を与えるので、これ以降もこれらの概念についてさらに語っていく予定です。
第5章に移って、structでデータをグループ化することについて見ていきましょう。
構造体を使用して関係のあるデータを構造化する
structまたは、構造体は、意味のあるグループを形成する複数の関連した値をまとめ、名前付けできる独自のデータ型です。 あなたがオブジェクト指向言語に造詣が深いなら、structはオブジェクトのデータ属性みたいなものです。 この章では、タプルと構造体を対照的に比較し、構造体の使用法をデモし、メソッドや関連関数を定義して、 構造体のデータに紐付く振る舞いを指定する方法について議論します。構造体とenum(第6章で議論します)は、 自分のプログラム領域で新しい型を定義し、Rustのコンパイル時型精査機能をフル活用する構成要素になります。
構造体を定義し、インスタンス化する
構造体は第3章で議論したタプルと似ています。タプル同様、構造体の一部を異なる型にできます。 一方タプルとは違って、各データ片には名前をつけるので、値の意味が明確になります。 この名前のおかげで、構造体はタプルに比して、より柔軟になるわけです: データの順番に頼って、 インスタンスの値を指定したり、アクセスしたりする必要がないのです。
構造体の定義は、structキーワードを入れ、構造体全体に名前を付けます。構造体名は、
一つにグループ化されるデータ片の意義を表すものであるべきです。そして、波かっこ内に、
データ片の名前と型を定義し、これはフィールドと呼ばれます。例えば、リスト5-1では、
ユーザアカウントに関する情報を保持する構造体を示しています。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } }
リスト5-1: User構造体定義
構造体を定義した後に使用するには、各フィールドに対して具体的な値を指定して構造体のインスタンスを生成します。
インスタンスは、構造体名を記述し、key: valueペアを含む波かっこを付け加えることで生成します。
ここで、キーはフィールド名、値はそのフィールドに格納したいデータになります。フィールドは、
構造体で宣言した通りの順番に指定する必要はありません。換言すると、構造体定義とは、
型に対する一般的な雛形のようなものであり、インスタンスは、その雛形を特定のデータで埋め、その型の値を生成するわけです。
例えば、リスト5-2で示されたように特定のユーザを宣言することができます。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; }
リスト5-2: User構造体のインスタンスを生成する
構造体から特定の値を得るには、ドット記法が使えます。このユーザのEメールアドレスだけが欲しいなら、
この値を使いたかった場所全部でuser1.emailが使えます。インスタンスが可変であれば、
ドット記法を使い特定のフィールドに代入することで値を変更できます。リスト5-3では、
可変なUserインスタンスのemailフィールド値を変更する方法を示しています。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } let mut user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; user1.email = String::from("anotheremail@example.com"); }
リスト5-3: あるUserインスタンスのemailフィールド値を変更する
インスタンス全体が可変でなければならないことに注意してください; Rustでは、一部のフィールドのみを可変にすることはできないのです。 また、あらゆる式同様、構造体の新規インスタンスを関数本体の最後の式として生成して、 そのインスタンスを返すことを暗示できます。
リスト5-4は、与えられたemailとusernameでUserインスタンスを生成するbuild_user関数を示しています。
activeフィールドにはtrue値が入り、sign_in_countには値1が入ります。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } fn build_user(email: String, username: String) -> User { User { email: email, username: username, active: true, sign_in_count: 1, } } }
リスト5-4: Eメールとユーザ名を取り、Userインスタンスを返すbuild_user関数
構造体のフィールドと同じ名前を関数の引数にもつけることは筋が通っていますが、
emailとusernameというフィールド名と変数を繰り返さなきゃいけないのは、ちょっと面倒です。
構造体にもっとフィールドがあれば、名前を繰り返すことはさらに煩わしくなるでしょう。
幸運なことに、便利な省略記法があります!
フィールドと変数が同名の時にフィールド初期化省略記法を使う
仮引数名と構造体のフィールド名がリスト5-4では、全く一緒なので、フィールド初期化省略記法を使ってbuild_userを書き換えても、
振る舞いは全く同じにしつつ、リスト5-5に示したようにemailとusernameを繰り返さなくてもよくなります。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } fn build_user(email: String, username: String) -> User { User { email, username, active: true, sign_in_count: 1, } } }
リスト5-5: emailとusername引数が構造体のフィールドと同名なので、
フィールド初期化省略法を使用するbuild_user関数
ここで、emailというフィールドを持つUser構造体の新規インスタンスを生成しています。
emailフィールドをbuild_user関数のemail引数の値にセットしたいわけです。
emailフィールドとemail引数は同じ名前なので、email: emailと書くのではなく、
emailと書くだけで済むのです。
構造体更新記法で他のインスタンスからインスタンスを生成する
多くは前のインスタンスの値を使用しつつ、変更する箇所もある形で新しいインスタンスを生成できるとしばしば有用です。 構造体更新記法でそうすることができます。
まず、リスト5-6では、更新記法なしでuser2に新しいUserインスタンスを生成する方法を示しています。
emailとusernameには新しい値をセットしていますが、それ以外にはリスト5-2で生成したuser1の値を使用しています。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } 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"), username: String::from("anotherusername567"), active: user1.active, sign_in_count: user1.sign_in_count, }; }
リスト5-6: user1の一部の値を使用しつつ、新しいUserインスタンスを生成する
構造体更新記法を使用すると、リスト5-7に示したように、コード量を減らしつつ、同じ効果を達成できます。..という記法により、
明示的にセットされていない残りのフィールドが、与えられたインスタンスのフィールドと同じ値になるように指定します。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } 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"), username: String::from("anotherusername567"), ..user1 }; }
リスト5-7: 構造体更新記法を使用して、新しいUserインスタンス用の値に新しいemailとusernameをセットしつつ、
残りの値は、user1変数のフィールド値を使う
リスト5-7のコードも、emailとusernameについては異なる値、activeとsign_in_countフィールドについては、
user1と同じ値になるインスタンスをuser2に生成します。
異なる型を生成する名前付きフィールドのないタプル構造体を使用する
構造体名により追加の意味を含むものの、フィールドに紐づけられた名前がなく、むしろフィールドの型だけのタプル構造体と呼ばれる、 タプルに似た構造体を定義することもできます。タプル構造体は、構造体名が提供する追加の意味は含むものの、 フィールドに紐付けられた名前はありません; むしろ、フィールドの型だけが存在します。タプル構造体は、タプル全体に名前をつけ、 そのタプルを他のタプルとは異なる型にしたい場合に有用ですが、普通の構造体のように各フィールド名を与えるのは、 冗長、または余計になるでしょう。
タプル構造体を定義するには、structキーワードの後に構造体名、さらにタプルに含まれる型を続けます。
例えば、こちらは、ColorとPointという2種類のタプル構造体の定義と使用法です:
#![allow(unused)] fn main() { struct Color(i32, i32, i32); struct Point(i32, i32, i32); let black = Color(0, 0, 0); let origin = Point(0, 0, 0); }
blackとoriginの値は、違う型であることに注目してください。これらは、異なるタプル構造体のインスタンスだからですね。
定義された各構造体は、構造体内のフィールドが同じ型であっても、それ自身が独自の型になります。
例えば、Color型を引数に取る関数は、Pointを引数に取ることはできません。たとえ、両者の型が、
3つのi32値からできていてもです。それ以外については、タプル構造体のインスタンスは、
タプルと同じように振る舞います: 分配して個々の部品にしたり、.と添え字を使用して個々の値にアクセスするなどです。
フィールドのないユニット様構造体
また、一切フィールドのない構造体を定義することもできます!これらは、()、ユニット型と似たような振る舞いをすることから、
ユニット様構造体と呼ばれます。ユニット様構造体は、ある型にトレイトを実装するけれども、
型自体に保持させるデータは一切ない場面に有効になります。トレイトについては第10章で議論します。
構造体データの所有権
リスト5-1の
User構造体定義において、&str文字列スライス型ではなく、所有権のあるString型を使用しました。 これは意図的な選択です。というのも、この構造体のインスタンスには全データを所有してもらう必要があり、 このデータは、構造体全体が有効な間はずっと有効である必要があるのです。構造体に、他の何かに所有されたデータへの参照を保持させることもできますが、 そうするにはライフタイムという第10章で議論するRustの機能を使用しなければなりません。 ライフタイムのおかげで構造体に参照されたデータが、構造体自体が有効な間、ずっと有効であることを保証してくれるのです。 ライフタイムを指定せずに構造体に参照を保持させようとしたとしましょう。以下の通りですが、これは動きません:
ファイル名: src/main.rs
struct User { username: &str, email: &str, sign_in_count: u64, active: bool, } fn main() { let user1 = User { email: "someone@example.com", username: "someusername123", active: true, sign_in_count: 1, }; }コンパイラは、ライフタイム指定子が必要だと怒るでしょう:
error[E0106]: missing lifetime specifier (エラー: ライフタイム指定子がありません) --> | 2 | username: &str, | ^ expected lifetime parameter (ライフタイム引数を予期しました) error[E0106]: missing lifetime specifier --> | 3 | email: &str, | ^ expected lifetime parameter第10章で、これらのエラーを解消して構造体に参照を保持する方法について議論しますが、 当面、今回のようなエラーは、
&strのような参照の代わりに、Stringのような所有された型を使うことで修正します。
構造体を使ったプログラム例
構造体を使用したくなる可能性のあるケースを理解するために、長方形の面積を求めるプログラムを書きましょう。 単一の変数から始め、代わりに構造体を使うようにプログラムをリファクタリングします。
Cargoでrectanglesという新規バイナリプロジェクトを作成しましょう。このプロジェクトは、 長方形の幅と高さをピクセルで指定し、その面積を求めます。リスト5-8に、プロジェクトのsrc/main.rsで、 正にそうする一例を短いプログラムとして示しました。
ファイル名: 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 }
リスト5-8: 個別の幅と高さ変数を指定して長方形の面積を求める
では、cargo runでこのプログラムを走らせてください:
The area of the rectangle is 1500 square pixels.
(長方形の面積は、1500平方ピクセルです)
タプルでリファクタリングする
リスト5-8のコードはうまく動き、各寸法を与えてarea関数を呼び出すことで長方形の面積を割り出しますが、
改善点があります。幅と高さは、組み合わせると一つの長方形を表すので、相互に関係があるわけです。
このコードの問題点は、areaのシグニチャから明らかです:
fn area(width: u32, height: u32) -> u32 {
area関数は、1長方形の面積を求めるものと考えられますが、今書いた関数には、引数が2つあります。
引数は関連性があるのに、このプログラム内のどこにもそのことは表現されていません。
幅と高さを一緒にグループ化する方が、より読みやすく、扱いやすくなるでしょう。
それをする一つの方法については、第3章の「タプル型」節ですでに議論しました: タプルを使うのです。
タプルでリファクタリングする
リスト5-9は、タプルを使う別バージョンのプログラムを示しています。
ファイル名: src/main.rs
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 }
リスト5-9: タプルで長方形の幅と高さを指定する
ある意味では、このプログラムはマシです。タプルのおかげで少し構造的になり、一引数を渡すだけになりました。 しかし別の意味では、このバージョンは明確性を失っています: タプルは要素に名前を付けないので、 計算が不明瞭になったのです。なぜなら、タプルの一部に添え字アクセスする必要があるからです。
面積計算で幅と高さを混在させるのなら問題はないのですが、長方形を画面に描画したいとなると、問題になるのです!
タプルの添え字0が幅で、添え字1が高さであることを肝に銘じておかなければなりません。
他人がこのコードをいじることになったら、このことを割り出し、同様に肝に銘じなければならないでしょう。
容易く、このことを忘れたり、これらの値を混ぜこぜにしたりしてエラーを発生させてしまうでしょう。
データの意味をコードに載せていないからです。
構造体でリファクタリングする: より意味付けする
データのラベル付けで意味を付与するために構造体を使います。現在使用しているタプルを全体と一部に名前のあるデータ型に、 変形することができます。そう、リスト5-10に示したように。
ファイル名: src/main.rs
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 }
リスト5-10: Rectangle構造体を定義する
ここでは、構造体を定義し、Rectangleという名前にしています。波括弧の中でwidthとheightというフィールドを定義し、
u32という型にしました。それからmain内でRectangleの特定のインスタンスを生成し、
幅を30、高さを50にしました。
これでarea関数は引数が一つになり、この引数は名前がrectangle、型はRectangle構造体インスタンスへの不変借用になりました。
第4章で触れたように、構造体の所有権を奪うよりも借用する必要があります。こうすることでmainは所有権を保って、
rect1を使用し続けることができ、そのために関数シグニチャと関数呼び出し時に&を使っているわけです。
area関数は、Rectangleインスタンスのwidthとheightフィールドにアクセスしています。
これで、areaの関数シグニチャは、我々の意図をズバリ示すようになりました: widthとheightフィールドを使って、
Rectangleの面積を計算します。これにより、幅と高さが相互に関係していることが伝わり、
タプルの0や1という添え字を使うよりも、これらの値に説明的な名前を与えられるのです。プログラムの意図が明瞭になりました。
トレイトの導出で有用な機能を追加する
プログラムのデバッグをしている間に、Rectangleのインスタンスを出力し、フィールドの値を確認できると、
素晴らしいわけです。リスト5-11では、以前の章のように、println!マクロを試しに使用しようとしていますが、動きません。
ファイル名: src/main.rs
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
// rect1は{}です
println!("rect1 is {}", rect1);
}
リスト5-11: Rectangleのインスタンスを出力しようとする
このコードを走らせると、こんな感じのエラーが出ます:
error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied
(エラー: トレイト境界`Rectangle: std::fmt::Display`が満たされていません)
println!マクロには、様々な整形があり、標準では、波括弧はDisplayとして知られる整形をするよう、
println!に指示するのです: 直接エンドユーザ向けの出力です。これまでに見てきた基本型は、
標準でDisplayを実装しています。というのも、1や他の基本型をユーザに見せる方法は一つしかないからです。
しかし構造体では、println!が出力を整形する方法は自明ではなくなります。出力方法がいくつもあるからです:
カンマは必要なの?波かっこを出力する必要はある?全フィールドが見えるべき?この曖昧性のため、
Rustは必要なものを推測しようとせず、構造体にはDisplay実装が提供されないのです。
エラーを読み下すと、こんな有益な注意書きがあります:
`Rectangle` cannot be formatted with the default formatter; try using
`:?` instead if you are using a format string
(注釈: `Rectangle`は、デフォルト整形機では、整形できません; フォーマット文字列を使うのなら
代わりに`:?`を試してみてください)
試してみましょう!pritnln!マクロ呼び出しは、println!("rect1 is {:?}", rect1);という見た目になるでしょう。
波括弧内に:?という指定子を書くと、println!にDebugと呼ばれる出力整形を使いたいと指示するのです。
Debugトレイトは、開発者にとって有用な方法で構造体を出力させてくれるので、
コードをデバッグしている最中に、値を確認することができます。
変更してコードを走らせてください。なに!まだエラーが出ます:
error[E0277]: the trait bound `Rectangle: std::fmt::Debug` is not satisfied
(エラー: トレイト境界`Rectangle: std::fmt::Debug`が満たされていません)
しかし今回も、コンパイラは有益な注意書きを残してくれています:
`Rectangle` cannot be formatted using `:?`; if it is defined in your
crate, add `#[derive(Debug)]` or manually implement it
(注釈: `Rectangle`は`:?`を使って整形できません; 自分のクレートで定義しているのなら
`#[derive(Debug)]`を追加するか、手動で実装してください)
確かにRustにはデバッグ用の情報を出力する機能が備わっていますが、この機能を構造体で使えるようにするには、
明示的な選択をしなければならないのです。そうするには、構造体定義の直前に#[derive(Debug)]という注釈を追加します。
そう、リスト5-12で示されている通りです。
ファイル名: src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; println!("rect1 is {:?}", rect1); }
リスト5-12: Debugトレイトを導出する注釈を追加し、
Rectangleインスタンスをデバッグ用整形機で出力する
これでプログラムを実行すれば、エラーは出ず、以下のような出力が得られるでしょう:
rect1 is Rectangle { width: 30, height: 50 }
素晴らしい!最善の出力ではないものの、このインスタンスの全フィールドの値を出力しているので、
デバッグ中には間違いなく役に立つでしょう。より大きな構造体があるなら、もう少し読みやすい出力の方が有用です;
そのような場合には、println!文字列中の{:?}の代わりに{:#?}を使うことができます。
この例で{:#?}というスタイルを使用したら、出力は以下のようになるでしょう:
rect1 is Rectangle {
width: 30,
height: 50
}
Rustには、derive注釈で使えるトレイトが多く提供されており、独自の型に有用な振る舞いを追加することができます。
そのようなトレイトとその振る舞いは、付録Cで一覧になっています。
これらのトレイトを独自の動作とともに実装する方法だけでなく、独自のトレイトを生成する方法については、第10章で解説します。
area関数は、非常に特殊です: 長方形の面積を算出するだけです。Rectangle構造体とこの動作をより緊密に結び付けられると、
役に立つでしょう。なぜなら、他のどんな型でもうまく動作しなくなるからです。
area関数をRectangle型に定義されたareaメソッドに変形することで、
このコードをリファクタリングし続けられる方法について見ていきましょう。
メソッド記法
メソッドは関数に似ています: fnキーワードと名前で宣言されるし、引数と返り値があるし、
どこか別の場所で呼び出された時に実行されるコードを含みます。ところが、
メソッドは構造体の文脈(あるいはenumかトレイトオブジェクトの。これらについては各々第6章と17章で解説します)で定義されるという点で、
関数とは異なり、最初の引数は必ずselfになり、これはメソッドが呼び出されている構造体インスタンスを表します。
メソッドを定義する
Rectangleインスタンスを引数に取るarea関数を変え、代わりにRectangle構造体上にareaメソッドを作りましょう。
リスト5-13に示した通りですね。
ファイル名: src/main.rs
#[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() ); }
リスト5-13: Rectangle構造体上にareaメソッドを定義する
Rectangleの文脈内で関数を定義するには、impl(implementation; 実装)ブロックを始めます。
それからarea関数をimplの波かっこ内に移動させ、最初の(今回は唯一の)引数をシグニチャ内と本体内全てでselfに変えます。
area関数を呼び出し、rect1を引数として渡すmainでは、代替としてメソッド記法を使用して、
Rectangleインスタンスのareaメソッドを呼び出せます。メソッド記法は、インスタンスの後に続きます:
ドット、メソッド名、かっこ、そして引数と続くわけです。
areaのシグニチャでは、rectangle: &Rectangleの代わりに&selfを使用しています。
というのも、コンパイラは、このメソッドがimpl Rectangleという文脈内に存在するために、
selfの型がRectangleであると把握しているからです。&Rectangleと同様に、
selfの直前に&を使用していることに注意してください。メソッドは、selfの所有権を奪ったり、
ここでしているように不変でselfを借用したり、可変でselfを借用したりできるのです。
他の引数と全く同じですね。
ここで&selfを選んでいるのは、関数バージョンで&Rectangleを使用していたのと同様の理由です:
所有権はいらず、構造体のデータを読み込みたいだけで、書き込む必要はないわけです。
メソッドの一部でメソッドを呼び出したインスタンスを変更したかったら、第1引数に&mut selfを使用するでしょう。
selfだけを第1引数にしてインスタンスの所有権を奪うメソッドを定義することは稀です; このテクニックは通常、
メソッドがselfを何か別のものに変形し、変形後に呼び出し元が元のインスタンスを使用できないようにしたい場合に使用されます。
関数の代替としてメソッドを使う主な利点は、メソッド記法を使用して全メソッドのシグニチャでselfの型を繰り返す必要がなくなる以外だと、
体系化です。コードの将来的な利用者にRectangleの機能を提供しているライブラリ内の各所でその機能を探させるのではなく、
この型のインスタンスでできることを一つのimplブロックにまとめあげています。
->演算子はどこに行ったの?CとC++では、メソッド呼び出しには2種類の異なる演算子が使用されます: オブジェクトに対して直接メソッドを呼び出すのなら、
.を使用するし、オブジェクトのポインタに対してメソッドを呼び出し、 先にポインタを参照外しする必要があるなら、->を使用するわけです。 言い換えると、objectがポインタなら、object->something()は、(*object).something()と同等なのです。Rustには
->演算子の代わりとなるようなものはありません; その代わり、Rustには、 自動参照および参照外しという機能があります。Rustにおいてメソッド呼び出しは、 この動作が行われる数少ない箇所なのです。動作方法はこうです:
object.something()とメソッドを呼び出すと、 コンパイラはobjectがメソッドのシグニチャと合致するように、自動で&か&mut、*を付与するのです。 要するに、以下のコードは同じものです:#![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); }前者の方がずっと明確です。メソッドには自明な受け手(
selfの型)がいるので、この自動参照機能は動作するのです。 受け手とメソッド名が与えられれば、コンパイラは確実にメソッドが読み込み専用(&self)か、書き込みもする(&mut self)のか、 所有権を奪う(self)のか判断できるわけです。メソッドの受け手に関して借用が明示されないというのが、 所有権を実際に使うのがRustにおいて簡単である大きな理由です。
より引数の多いメソッド
Rectangle構造体に2番目のメソッドを実装して、メソッドを使う鍛錬をしましょう。今回は、Rectangleのインスタンスに、
別のRectangleのインスタンスを取らせ、2番目のRectangleがselfに完全にはめ込まれたら、trueを返すようにしたいのです;
そうでなければ、falseを返すべきです。つまり、一旦can_holdメソッドを定義したら、
リスト5-14のようなプログラムを書けるようになりたいのです。
ファイル名: src/main.rs
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 10, height: 40 };
let rect3 = Rectangle { width: 60, height: 45 };
// rect1にrect2ははまり込む?
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
リスト5-14: まだ書いていないcan_holdメソッドを使用する
そして、予期される出力は以下のようになります。なぜなら、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メソッドを呼び出した後にもrect2が使えるよう、所有権をmainに残したままにしたいからです。
can_holdの返り値は、booleanになり、メソッドの中身は、selfの幅と高さがもう一つのRectangleの幅と高さよりも、
それぞれ大きいことを確認します。リスト5-13のimplブロックに新しいcan_holdメソッドを追記しましょう。
リスト5-15に示した通りです。
ファイル名: src/main.rs
#![allow(unused)] fn main() { #[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 } } }
リスト5-15: 別のRectangleのインスタンスを引数として取るcan_holdメソッドを、
Rectangleに実装する
このコードをリスト5-14のmain関数と合わせて実行すると、望み通りの出力が得られます。
メソッドは、self引数の後にシグニチャに追加した引数を複数取ることができ、
その引数は、関数の引数と同様に動作するのです。
関連関数
implブロックの別の有益な機能は、implブロック内にselfを引数に取らない関数を定義できることです。
これは、構造体に関連付けられているので、関連関数と呼ばれます。それでも、関連関数は関数であり、メソッドではありません。
というのも、対象となる構造体のインスタンスが存在しないからです。もうString::fromという関連関数を使用したことがありますね。
関連関数は、構造体の新規インスタンスを返すコンストラクタによく使用されます。例えば、一つの寸法を引数に取り、
長さと幅両方に使用する関連関数を提供することができ、その結果、同じ値を2回指定する必要なく、
正方形のRectangleを生成しやすくすることができます。
ファイル名: src/main.rs
#![allow(unused)] fn main() { #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size } } } }
この関連関数を呼び出すために、構造体名と一緒に::記法を使用します; 一例はlet sq = Rectangle::square(3);です。
この関数は、構造体によって名前空間分けされています: ::という記法は、関連関数とモジュールによって作り出される名前空間両方に使用されます。
モジュールについては第7章で議論します。
複数のimplブロック
各構造体には、複数のimplブロックを存在させることができます。例えば、リスト5-15はリスト5-16に示したコードと等価で、
リスト5-16では、各メソッドごとにimplブロックを用意しています。
#![allow(unused)] fn main() { #[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 } } }
リスト5-16: 複数のimplブロックを使用してリスト5-15を書き直す
ここでこれらのメソッドを個々のimplブロックに分ける理由はないのですが、合法な書き方です。
複数のimplブロックが有用になるケースは第10章で見ますが、そこではジェネリック型と、トレイトについて議論します。
まとめ
構造体により、自分の領域で意味のある独自の型を作成することができます。構造体を使用することで、 関連のあるデータ片を相互に結合させたままにし、各部品に名前を付け、コードを明確にすることができます。 メソッドにより、構造体のインスタンスが行う動作を指定することができ、関連関数により、 構造体に特有の機能をインスタンスを利用することなく、名前空間分けすることができます。
しかし、構造体だけが独自の型を作成する手段ではありません: Rustのenum機能に目を向けて、 別の道具を道具箱に追加しましょう。
Enumとパターンマッチング
この章では、列挙型について見ていきます。列挙型は、enumとも称されます。enumは、取りうる値を列挙することで、
型を定義させてくれます。最初に、enumを定義し、使用して、enumがデータとともに意味をコード化する方法を示します。
次に、特別に有用なenumであるOptionについて掘り下げていきましょう。この型は、
値が何かかなんでもないかを表現します。それから、match式のパターンマッチングにより、
どうenumの色々な値に対して異なるコードを走らせやすくなるかを見ます。最後に、if let文法要素も、
如何にenumをコードで扱う際に使用可能な便利で簡潔な慣用句であるかを解説します。
enumは多くの言語に存在する機能ですが、その能力は言語ごとに異なります。Rustのenumは、F#、OCaml、Haskellなどの、 関数型言語に存在する代数的データ型に最も酷似しています。
Enumを定義する
コードで表現したくなるかもしれない場面に目を向けて、enumが有用でこの場合、構造体よりも適切である理由を確認しましょう。 IPアドレスを扱う必要が出たとしましょう。現在、IPアドレスの規格は二つあります: バージョン4とバージョン6です。 これらは、プログラムが遭遇するIPアドレスのすべての可能性です: 列挙型は、取りうる値をすべて列挙でき、 これが列挙型の名前の由来です。
どんなIPアドレスも、バージョン4かバージョン6のどちらかになりますが、同時に両方にはなり得ません。 IPアドレスのその特性により、enumデータ構造が適切なものになります。というのも、 enumの値は、その列挙子のいずれか一つにしかなり得ないからです。バージョン4とバージョン6のアドレスは、 どちらも根源的にはIPアドレスですから、コードがいかなる種類のIPアドレスにも適用される場面を扱う際には、 同じ型として扱われるべきです。
この概念をコードでは、IpAddrKind列挙型を定義し、IPアドレスがなりうる種類、V4とV6を列挙することで、
表現できます。これらは、enumの列挙子として知られています:
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } }
これで、IpAddrKindはコードの他の場所で使用できる独自のデータ型になります。
Enumの値
以下のようにして、IpAddrKindの各列挙子のインスタンスは生成できます:
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } let four = IpAddrKind::V4; let six = IpAddrKind::V6; }
enumの列挙子は、その識別子の元に名前空間分けされていることと、
2連コロンを使ってその二つを区別していることに注意してください。
これが有効な理由は、こうすることで、値IpAddrKind::V4とIpAddrKind::V6という値は両方とも、
同じ型IpAddrKindになったからです。そうしたら、例えば、どんなIpAddrKindを取る関数も定義できるようになります。
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } fn route(ip_type: IpAddrKind) { } }
そして、この関数をどちらの列挙子に対しても呼び出せます:
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } fn route(ip_type: IpAddrKind) { } route(IpAddrKind::V4); route(IpAddrKind::V6); }
enumの利用には、さらなる利点さえもあります。このIPアドレス型についてもっと考えてみると、現状では、 実際のIPアドレスのデータを保持する方法がありません。つまり、どんな種類であるかを知っているだけです。 構造体について第5章で学んだばっかりとすると、この問題に対して、あなたはリスト6-1のように対処するかもしれません。
#![allow(unused)] 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"), }; }
リスト6-1: IPアドレスのデータとIpAddrKindの列挙子をstructを使って保持する
ここでは、二つのフィールドを持つIpAddrという構造体を定義しています: IpAddrKind型(先ほど定義したenumですね)のkindフィールドと、
String型のaddressフィールドです。この構造体のインスタンスが2つあります。最初のインスタンス、
homeにはkindとしてIpAddrKind::V4があり、紐付けられたアドレスデータは127.0.0.1です。
2番目のインスタンス、loopbackには、kindの値として、IpAddrKindのもう一つの列挙子、V6があり、
アドレス::1が紐付いています。構造体を使ってkindとaddress値を一緒に包んだので、
もう列挙子は値と紐付けられています。
各enumの列挙子に直接データを格納して、enumを構造体内に使うというよりもenumだけを使って、
同じ概念をもっと簡潔な方法で表現することができます。この新しいIpAddrの定義は、
V4とV6列挙子両方にString値が紐付けられていることを述べています。
#![allow(unused)] 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を使うことには、別の利点もあります: 各列挙子に紐付けるデータの型と量は、異なってもいいのです。
バージョン4のIPアドレスには、常に0から255の値を持つ4つの数値があります。V4のアドレスは、4つのu8型の値として格納するけれども、
V6のアドレスは引き続き、単独のString型の値で格納したかったとしても、構造体では不可能です。
enumなら、こんな場合も容易に対応できます:
#![allow(unused)] 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と列挙子がありますが、アドレスデータを二種の異なる構造体の形で列挙子に埋め込み、
この構造体は各列挙子用に異なる形で定義されています。
#![allow(unused)] fn main() { struct Ipv4Addr { // 省略 } struct Ipv6Addr { // 省略 } enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr), } }
このコードは、enum列挙子内にいかなる種類のデータでも格納できることを描き出しています: 例を挙げれば、文字列、数値型、構造体などです。他のenumを含むことさえできます!また、 標準ライブラリの型は、あなたの想像するよりも複雑ではないことがしばしばあります。
標準ライブラリにIpAddrに対する定義は含まれるものの、標準ライブラリの定義をまだ我々のスコープに導入していないので、
干渉することなく自分自身の定義を生成して使用できることに注意してください。型をスコープに導入することについては、
第7章でもっと詳しく言及します。
リスト6-2でenumの別の例を見てみましょう: 今回のコードは、幅広い種類の型が列挙子に埋め込まれています。
#![allow(unused)] fn main() { enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } }
リスト6-2: 列挙子各々が異なる型と量の値を格納するMessage enum
このenumには、異なる型の列挙子が4つあります:
Quitには紐付けられたデータは全くなし。Moveは、中に匿名構造体を含む。Writeは、単独のStringオブジェクトを含む。ChangeColorは、3つのi32値を含む。
リスト6-2のような列挙子を含むenumを定義することは、enumの場合、structキーワードを使わず、
全部の列挙子がMessage型の元に分類される点を除いて、異なる種類の構造体定義を定義するのと類似しています。
以下の構造体も、先ほどのenumの列挙子が保持しているのと同じデータを格納することができるでしょう:
#![allow(unused)] fn main() { struct QuitMessage; // ユニット構造体 struct MoveMessage { x: i32, y: i32, } struct WriteMessage(String); // タプル構造体 struct ChangeColorMessage(i32, i32, i32); // タプル構造体 }
ですが、異なる構造体を使っていたら、各々、それ自身の型があるので、単独の型になるリスト6-2で定義したMessage enumほど、
これらの種のメッセージいずれもとる関数を簡単に定義することはできないでしょう。
enumと構造体にはもう1点似通っているところがあります: implを使って構造体にメソッドを定義できるのと全く同様に、
enumにもメソッドを定義することができるのです。こちらは、Message enum上に定義できるcallという名前のメソッドです:
#![allow(unused)] 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が表す値になります。
非常に一般的で有用な別の標準ライブラリのenumを見てみましょう: Optionです。
Option enumとNull値に勝る利点
前節で、IpAddr enumがRustの型システムを使用して、プログラムにデータ以上の情報をコード化できる方法を目撃しました。
この節では、Optionのケーススタディを掘り下げていきます。この型も標準ライブラリにより定義されているenumです。
このOption型はいろんな箇所で使用されます。なぜなら、値が何かかそうでないかという非常に一般的な筋書きをコード化するからです。
この概念を型システムの観点で表現することは、コンパイラが、プログラマが処理すべき場面全てを処理していることをチェックできることを意味します;
この機能は、他の言語において、究極的にありふれたバグを阻止することができます。
プログラミング言語のデザインは、しばしばどの機能を入れるかという観点で考えられるが、 除いた機能も重要なのです。Rustには、他の多くの言語にはあるnull機能がありません。 nullとはそこに何も値がないことを意味する値です。nullのある言語において、 変数は常に二者択一どちらかの状態になります: nullかそうでないかです。
nullの開発者であるトニー・ホーア(Tony Hoare)の2009年のプレゼンテーション、 "Null References: The Billion Dollar Mistake"(Null参照: 10億ドルの間違い)では、こんなことが語られています。
私はそれを10億ドルの失敗と呼んでいます。その頃、私は、オブジェクト指向言語の参照に対する、 最初のわかりやすい型システムを設計していました。私の目標は、 どんな参照の使用も全て完全に安全であるべきことを、コンパイラにそのチェックを自動で行ってもらって保証することだったのです。 しかし、null参照を入れるという誘惑に打ち勝つことができませんでした。それは、単純に実装が非常に容易だったからです。 これが無数のエラーや脆弱性、システムクラッシュにつながり、過去40年で10億ドルの苦痛や損害を引き起こしたであろうということなのです。
null値の問題は、nullの値をnullでない値のように使用しようとしたら、何らかの種類のエラーが出ることです。 このnullかそうでないかという特性は広く存在するので、この種の間違いを大変犯しやすいのです。
しかしながら、nullが表現しようとしている概念は、それでも役に立つものです: nullは、 何らかの理由で現在無効、または存在しない値のことなのです。
問題は、全く概念にあるのではなく、特定の実装にあるのです。そんな感じなので、Rustにはnullがありませんが、
値が存在するか不在かという概念をコード化するenumならあります。このenumがOption<T>で、
以下のように標準ライブラリに定義されています。
#![allow(unused)] fn main() { enum Option<T> { Some(T), None, } }
Option<T>は有益すぎて、初期化処理(prelude)にさえ含まれています。つまり、明示的にスコープに導入する必要がないのです。
さらに、列挙子もそうなっています: SomeとNoneをOption::の接頭辞なしに直接使えるわけです。
ただ、Option<T>はそうは言っても、普通のenumであり、Some(T)とNoneもOption<T>型のただの列挙子です。
<T>という記法は、まだ語っていないRustの機能です。これは、ジェネリック型引数であり、ジェネリクスについて詳しくは、
第10章で解説します。とりあえず、知っておく必要があることは、<T>は、Option enumのSome列挙子が、
あらゆる型のデータを1つだけ持つことができることを意味していることだけです。こちらは、
Option値を使って、数値型や文字列型を保持する例です。
#![allow(unused)] fn main() { let some_number = Some(5); let some_string = Some("a string"); let absent_number: Option<i32> = None; }
Someではなく、Noneを使ったら、コンパイラにOption<T>の型が何になるかを教えなければいけません。
というのも、None値を見ただけでは、Some列挙子が保持する型をコンパイラが推論できないからです。
Some値がある時、値が存在するとわかり、その値は、Someに保持されています。None値がある場合、
ある意味、nullと同じことを意図します: 有効な値がないのです。では、なぜOption<T>の方が、
nullよりも少しでも好ましいのでしょうか?
簡潔に述べると、Option<T>とT(ここでTはどんな型でもいい)は異なる型なので、
コンパイラがOption<T>値を確実に有効な値かのようには使用させてくれません。
例えば、このコードはi8をOption<i8>に足そうとしているので、コンパイルできません。
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
このコードを動かしたら、以下のようなエラーメッセージが出ます。
error[E0277]: the trait bound `i8: std::ops::Add<std::option::Option<i8>>` is
not satisfied
(エラー: `i8: std::ops::Add<std::option::Option<i8>>`というトレイト境界が満たされていません)
-->
|
5 | let sum = x + y;
| ^ no implementation for `i8 + std::option::Option<i8>`
|
なんて強烈な!実際に、このエラーメッセージは、i8とOption<i8>が異なる型なので、
足し合わせる方法がコンパイラにはわからないことを意味します。Rustにおいて、i8のような型の値がある場合、
コンパイラが常に有効な値であることを確認してくれます。この値を使う前にnullであることをチェックする必要なく、
自信を持って先に進むことができるのです。Option<i8>がある時(あるいはどんな型を扱おうとしていても)のみ、
値を保持していない可能性を心配する必要があるわけであり、
コンパイラはプログラマが値を使用する前にそのような場面を扱っているか確かめてくれます。
言い換えると、T型の処理を行う前には、Option<T>をTに変換する必要があるわけです。一般的に、
これにより、nullの最もありふれた問題の一つを捕捉する一助になります: 実際にはnullなのに、
そうでないかのように想定することです。
不正確にnullでない値を想定する心配をしなくてもよいということは、コード内でより自信を持てることになります。
nullになる可能性のある値を保持するには、その値の型をOption<T>にすることで明示的に同意しなければなりません。
それからその値を使用する際には、値がnullである場合を明示的に処理する必要があります。
値がOption<T>以外の型であるところ全てにおいて、値がnullでないと安全に想定することができます。
これは、Rustにとって、意図的な設計上の決定であり、nullの普遍性を制限し、Rustコードの安全性を向上させます。
では、Option<T>型の値がある時、その値を使えるようにするには、どのようにSome列挙子からT型の値を取り出せばいいのでしょうか?
Option<T>には様々な場面で有効に活用できる非常に多くのメソッドが用意されています;
ドキュメントでそれらを確認できます。Option<T>のメソッドに馴染むと、
Rustの旅が極めて有益になるでしょう。
一般的に、Option<T>値を使うには、各列挙子を処理するコードが欲しくなります。
Some(T)値がある時だけ走る何らかのコードが欲しくなり、このコードが内部のTを使用できます。
None値があった場合に走る別のコードが欲しくなり、そちらのコードはT値は使用できない状態になります。
match式が、enumとともに使用した時にこれだけの動作をする制御フロー文法要素になります:
enumの列挙子によって、違うコードが走り、そのコードがマッチした値の中のデータを使用できるのです。
match制御フロー演算子
Rustには、一連のパターンに対して値を比較し、マッチしたパターンに応じてコードを実行させてくれるmatchと呼ばれる、
非常に強力な制御フロー演算子があります。パターンは、リテラル値、変数名、ワイルドカードやその他多数のもので構成することができます;
第18章で、全ての種類のパターンと、その目的については解説します。matchのパワーは、
パターンの表現力とコンパイラが全てのありうるパターンを処理しているかを確認してくれるという事実に由来します。
match式をコイン並べ替え装置のようなものと考えてください: コインは、様々なサイズの穴が空いた通路を流れ落ち、
各コインは、サイズのあった最初の穴に落ちます。同様に、値はmatchの各パターンを通り抜け、値が「適合する」最初のパターンで、
値は紐付けられたコードブロックに落ち、実行中に使用されるわけです。
コインについて話したので、それをmatchを使用する例にとってみましょう!数え上げ装置と同じ要領で未知のアメリカコインを一枚取り、
どの種類のコインなのか決定し、その価値をセントで返す関数をリスト6-3で示したように記述することができます。
#![allow(unused)] fn main() { enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } }
リスト6-3: enumとそのenumの列挙子をパターンにしたmatch式
value_in_cents関数内のmatchを噛み砕きましょう。まず、matchキーワードに続けて式を並べています。
この式は今回の場合、値coinです。ifで使用した式と非常に酷似しているみたいですね。しかし、大きな違いがあります:
ifでは、式は論理値を返す必要がありますが、ここでは、どんな型でも構いません。この例におけるcoinの型は、
1行目で定義したCoin enumです。
次は、matchアームです。一本のアームには2つの部品があります: パターンと何らかのコードです。
今回の最初のアームはCoin::Pennyという値のパターンであり、パターンと動作するコードを区別する=>演算子が続きます。
この場合のコードは、ただの値1です。各アームは次のアームとカンマで区切られています。
このmatch式が実行されると、結果の値を各アームのパターンと順番に比較します。パターンに値がマッチしたら、
そのコードに紐付けられたコードが実行されます。パターンが値にマッチしなければ、コイン並べ替え装置と全く同じように、
次のアームが継続して実行されます。必要なだけパターンは存在できます: リスト6-3では、matchには4本のアームがあります。
各アームに紐付けられるコードは式であり、マッチしたアームの式の結果がmatch式全体の戻り値になります。
典型的に、アームのコードが短い場合、波かっこは使用されません。リスト6-3では、各アームが値を返すだけなので、
これに倣っています。マッチのアームで複数行のコードを走らせたいのなら、波かっこを使用することができます。
例えば、以下のコードは、メソッドがCoin::Pennyとともに呼び出されるたびに「Lucky penny!」と表示しつつ、
ブロックの最後の値、1を返すでしょう。
#![allow(unused)] fn main() { enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 }, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } }
値に束縛されるパターン
マッチのアームの別の有益な機能は、パターンにマッチした値の一部に束縛できる点です。こうして、 enumの列挙子から値を取り出すことができます。
例として、enumの列挙子の一つを中にデータを保持するように変えましょう。1999年から2008年まで、
アメリカは、片側に50の州それぞれで異なるデザインをしたクォーターコインを鋳造していました。
他のコインは州のデザインがなされることはなかったので、クォーターだけがこのおまけの値を保持します。
Quarter列挙子を変更して、UsState値が中に保持されるようにすることでenumにこの情報を追加でき、
それをしたのがリスト6-4のコードになります。
#![allow(unused)] fn main() { #[derive(Debug)] // すぐに州を点検できるように enum UsState { Alabama, Alaska, // ... などなど } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } }
リスト6-4: Quarter列挙子がUsStateの値も保持するCoin enum
友人の一人が50州全部のクォーターコインを収集しようとしているところを想像しましょう。コインの種類で小銭を並べ替えつつ、 友人が持っていない種類だったら、コレクションに追加できるように、各クォーターに関連した州の名前を出力します。
このコードのmatch式では、Coin::Quarter列挙子の値にマッチするstateという名の変数をパターンに追加します。
Coin::Quarterがマッチすると、state変数はそのクォーターのstateの値に束縛されます。それから、
stateをそのアームのコードで使用できます。以下のようにですね:
#![allow(unused)] fn main() { #[derive(Debug)] enum UsState { Alabama, Alaska, } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {:?}!", state); 25 }, } } }
value_in_cents(Coin::Quarter(UsState::Alaska))と呼び出すつもりだったなら、coinは
Coin::Quarter(UsState::Alaska)になります。その値をmatchの各アームと比較すると、
Coin::Quarter(state)に到達するまで、どれにもマッチしません。その時に、stateに束縛されるのは、
UsState::Alaskaという値です。そして、println!式でその束縛を使用することができ、
そのため、Coin enumの列挙子からQuarterに対する中身のstateの値を取得できたわけです。
Option<T>とのマッチ
前節では、Option<T>を使用する際に、Someケースから中身のTの値を取得したくなりました。要するに、
Coin enumに対して行ったように、matchを使ってOption<T>を扱うこともできるというわけです!
コインを比較する代わりに、Option<T>の列挙子を比較するのですが、match式の動作の仕方は同じままです。
Option<i32>を取る関数を書きたくなったとし、中に値があったら、その値に1を足すことにしましょう。
中に値がなければ、関数はNone値を返し、何も処理を試みるべきではありません。
matchのおかげで、この関数は大変書きやすく、リスト6-5のような見た目になります。
#![allow(unused)] 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); }
リスト6-5: Option<i32>にmatch式を使う関数
plus_oneの最初の実行についてもっと詳しく検証しましょう。plus_one(five)と呼び出した時、
plus_oneの本体の変数xはSome(5)になります。そして、これをマッチの各アームと比較します。
None => None,
Some(5)という値は、Noneというパターンにはマッチしませんので、次のアームに処理が移ります。
Some(i) => Some(i + 1),
Some(5)はSome(i)にマッチしますか?なんと、します!列挙子が同じです。iはSomeに含まれる値に束縛されるので、
iは値5になります。それから、このマッチのアームのコードが実行されるので、iの値に1を足し、
合計の6を中身にした新しいSome値を生成します。
さて、xがNoneになるリスト6-5の2回目のplus_oneの呼び出しを考えましょう。matchに入り、
最初のアームと比較します。
None => None,
マッチします!足し算する値がないので、プログラムは停止し、=>の右辺にあるNone値が返ります。
最初のアームがマッチしたため、他のアームは比較されません。
matchとenumの組み合わせは、多くの場面で有効です。Rustコードにおいて、このパターンはよく見かけるでしょう:
enumに対しmatchし、内部のデータに変数を束縛させ、それに基づいたコードを実行します。最初はちょっと巧妙ですが、
一旦慣れてしまえば、全ての言語にあってほしいと願うことになるでしょう。一貫してユーザのお気に入りなのです。
マッチは包括的
もう一つ議論する必要のあるmatchの観点があります。一点バグがありコンパイルできないこんなバージョンのplus_one関数を考えてください:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
Noneの場合を扱っていないため、このコードはバグを生みます。幸い、コンパイラが捕捉できるバグです。
このコードのコンパイルを試みると、こんなエラーが出ます:
error[E0004]: non-exhaustive patterns: `None` not covered
(エラー: 包括的でないパターン: `None`がカバーされてません)
-->
|
6 | match x {
| ^ pattern `None` not covered
全可能性を網羅していないことをコンパイラは検知しています。もっと言えば、どのパターンを忘れているかさえ知っているのです。
Rustにおけるマッチは、包括的です: 全てのあらゆる可能性を網羅し尽くさなければ、コードは有効にならないのです。
特にOption<T>の場合には、私達が明示的にNoneの場合を処理するのを忘れないようにしてくれます。
nullになるかもしれないのに値があると思い込まないよう、すなわち前に議論した10億ドルの失敗を犯さないよう、
コンパイラが保護してくれるわけです。
_というプレースホルダー
Rustには、全ての可能性を列挙したくない時に使用できるパターンもあります。例えば、u8は、有効な値として、
0から255までを取ります。1、3、5、7の値にだけ興味があったら、0、2、4、6、8、9と255までの数値を列挙する必要に迫られたくはないです。
幸運なことに、する必要はありません: 代わりに特別なパターンの_を使用できます:
#![allow(unused)] fn main() { let some_u8_value = 0u8; match some_u8_value { 1 => println!("one"), 3 => println!("three"), 5 => println!("five"), 7 => println!("seven"), _ => (), } }
_というパターンは、どんな値にもマッチします。他のアームの後に記述することで、_は、
それまでに指定されていない全ての可能性にマッチします。()は、ただのユニット値なので、_の場合には、
何も起こりません。結果として、_プレースホルダーの前に列挙していない可能性全てに対しては、
何もしたくないと言えるわけです。
ですが、一つのケースにしか興味がないような場面では、match式はちょっと長ったらしすぎます。
このような場面用に、Rustには、if letが用意されています。
if letで簡潔な制御フロー
if let記法でifとletをより冗長性の少ない方法で組み合わせ、残りを無視しつつ、一つのパターンにマッチする値を扱うことができます。
Option<u8>にマッチするけれど、値が3の時にだけコードを実行したい、リスト6-6のプログラムを考えてください。
#![allow(unused)] fn main() { let some_u8_value = Some(0u8); match some_u8_value { Some(3) => println!("three"), _ => (), } }
リスト6-6: 値がSome(3)の時だけコードを実行するmatch
Some(3)にマッチした時だけ何かをし、他のSome<u8>値やNone値の時には何もしたくありません。
match式を満たすためには、列挙子を一つだけ処理した後に_ => ()を追加しなければなりません。
これでは、追加すべき定型コードが多すぎます。
その代わり、if letを使用してもっと短く書くことができます。以下のコードは、
リスト6-6のmatchと同じように振る舞います:
#![allow(unused)] fn main() { let some_u8_value = Some(0u8); if let Some(3) = some_u8_value { println!("three"); } }
if letという記法は等号記号で区切られたパターンと式を取り、式がmatchに与えられ、パターンが最初のアームになったmatchと、
同じ動作をします。
if letを使うと、タイプ数が減り、インデントも少なくなり、定型コードも減ります。しかしながら、
matchでは強制された包括性チェックを失ってしまいます。matchかif letかの選択は、
特定の場面でどんなことをしたいかと簡潔性を得ることが包括性チェックを失うのに適切な代償となるかによります。
言い換えると、if letは値が一つのパターンにマッチした時にコードを走らせ、
他は無視するmatchへの糖衣構文と考えることができます。
if letでは、elseを含むこともできます。elseに入るコードブロックは、
if letとelseに等価なmatch式の_の場合に入るコードブロックと同じになります。
リスト6-4のCoin enum定義を思い出してください。ここでは、Quarter列挙子は、
UsStateの値も保持していましたね。クォーターコインの状態を告げつつ、
見かけたクォーター以外のコインの枚数を数えたいなら、以下のようにmatch式で実現することができるでしょう:
#![allow(unused)] fn main() { #[derive(Debug)] enum UsState { Alabama, Alaska, } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } let coin = Coin::Penny; let mut count = 0; match coin { // {:?}州のクォーターコイン Coin::Quarter(state) => println!("State quarter from {:?}!", state), _ => count += 1, } }
または、以下のようにif letとelseを使うこともできるでしょう:
#![allow(unused)] fn main() { #[derive(Debug)] enum UsState { Alabama, Alaska, } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } let coin = Coin::Penny; let mut count = 0; if let Coin::Quarter(state) = coin { println!("State quarter from {:?}!", state); } else { count += 1; } }
matchを使って表現するには冗長的すぎるロジックがプログラムにあるようなシチュエーションに遭遇したら、
if letもRust道具箱にあることを思い出してください。
まとめ
これで、enumを使用してワンセットの列挙された値のどれかになりうる独自の型を生成する方法を講義しました。
標準ライブラリのOption<T>が型システムを使用して、エラーを回避する際に役立つ方法についても示しました。
enumの値がデータを内部に含む場合、処理すべきケースの数に応じて、matchかif letを使用して値を取り出し、
使用できます。
もうRustプログラムで構造体とenumを使用して、自分の領域の概念を表現できます。API内で使用するために独自の型を生成することで、 型安全性を保証することができます: コンパイラが、各関数の予期する型の値のみを関数が得ることを確かめてくれるのです。
使用するのに率直な整理整頓されたAPIをユーザに提供し、ユーザが必要とするものだけを公開するために、 今度は、Rustのモジュールに目を向けてみましょう。
肥大化していくプロジェクトをパッケージ、クレート、モジュールを利用して管理する
大きなプログラムを書く時、そのすべてを頭の中に入れておくのは不可能になるため、コードのまとまりを良くすることが重要になります。 関係した機能をまとめ、異なる特徴を持つコードを分割することにより、特定の機能を実装しているコードを見つけたり、機能を変更したりするためにどこを探せば良いのかを明確にできます。
私達がこれまでに書いてきたプログラムは、一つのファイル内の一つのモジュール内にありました。 プロジェクトが大きくなるにつれて、これを複数のモジュールに、ついで複数のファイルに分割することで、プログラムを整理することができます。 パッケージは複数のバイナリクレートからなり、またライブラリクレートを1つもつこともできます。 パッケージが大きくなるにつれて、その一部を抜き出して分離したクレートにし、外部依存とするのもよいでしょう。 この章ではそれらのテクニックすべてを学びます。 相互に関係し合い、同時に成長するパッケージの集まりからなる巨大なプロジェクトには、 Cargoがワークスペースという機能を提供します。これは14章のCargoワークスペースで解説します。
機能をグループにまとめられることに加え、実装の詳細がカプセル化されることにより、コードをより高いレベルで再利用できるようになります: 手続きを実装し終えてしまえば、他のコードはそのコードの公開されたインターフェースを通じて、実装の詳細を知ることなくそのコードを呼び出すことができるのです。 コードをどう書くかによって、どの部分が他のコードにも使える公開のものになるのか、それとも自分だけが変更できる非公開のものになるのかが決定されます。 これもまた、記憶しておくべき細部を制限してくれる方法のひとつです。
関係する概念にスコープがあります: コードが記述されているネストされた文脈には、「スコープ内」として定義される名前の集合があります。 コードを読んだり書いたりコンパイルしたりする時には、プログラマーやコンパイラは特定の場所にある特定の名前が、変数・関数・構造体・enum・モジュール・定数・その他のどの要素を表すのか、そしてその要素は何を意味するのかを知る必要があります。 そこでスコープを作り、どの名前がスコープ内/スコープ外にあるのかを変更することができます。 同じ名前のものを2つ同じスコープ内に持つことはできません。そこで、名前の衝突を解決するための方法があります。
Rustには、どの詳細を公開するか、どの詳細を非公開にするか、どの名前がプログラムのそれぞれのスコープにあるか、といったコードのまとまりを保つためのたくさんの機能があります。 これらの機能は、まとめて「モジュールシステム」と呼ばれることがあり、以下のようなものが含まれます。
- パッケージ: クレートをビルドし、テストし、共有することができるCargoの機能
- クレート: ライブラリか実行可能ファイルを生成する、木構造をしたモジュール群
- モジュール と use: これを使うことで、パスの構成、スコープ、公開するか否かを決定できます
- パス: 要素(例えば構造体や関数やモジュール)に名前をつける方法
この章では、これらの機能をすべて学び、これらがどう相互作用するかについて議論し、これらをどう使ってスコープを制御するのかについて説明します。 この章を読み終わる頃には、モジュールシステムをしっかりと理解し、熟練者のごとくスコープを扱うことができるようになっているでしょう!
パッケージとクレート
最初に学ぶモジュールシステムの要素は、パッケージとクレートです。 クレートはバイナリかライブラリのどちらかです。 クレートルート (crate root) とは、Rustコンパイラの開始点となり、クレートのルートモジュールを作るソースファイルのことです(モジュールについて詳しくは「モジュールを定義して、スコープとプライバシーを制御する」のセクションで説明します)。 パッケージ はある機能群を提供する1つ以上のクレートです。 パッケージは Cargo.toml という、それらのクレートをどのようにビルドするかを説明するファイルを持っています。
パッケージが何を持ってよいかはいくつかのルールで決まっています。 パッケージは0個か1個のライブラリクレートを持っていないといけません。それ以上は駄目です。 バイナリクレートはいくらでも持って良いですが、少なくとも(ライブラリでもバイナリでも良いですが)1つのクレートを持っていないといけません。
パッケージを作る時に何が起こるか見てみましょう。
まず、cargo newというコマンドを入力します:
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
このコマンドを入力したとき、Cargoは Cargo.toml ファイルを作り、パッケージを作ってくれました。
Cargo.toml の中身を見ても、src/main.rs については何も書いてありません。これは、Cargoは src/main.rs が、パッケージと同じ名前を持つバイナリクレートのクレートルートであるという慣習に従っているためです。
同じように、Cargoはパッケージディレクトリに src/lib.rs が含まれていたら、パッケージにはパッケージと同じ名前のライブラリクレートが含まれており、src/lib.rs がそのクレートルートなのだと判断します。
Cargoはクレートルートファイルを rustcに渡し、ライブラリやバイナリをビルドします。
今、このパッケージには src/main.rs しか含まれておらず、つまりこのパッケージはmy-projectという名前のバイナリクレートのみを持っているということです。
もしパッケージが src/main.rs と src/lib.rs を持っていたら、クレートは2つになります:どちらもパッケージと同じ名前を持つ、ライブラリクレートとバイナリクレートです。
ファイルを src/bin ディレクトリに置くことで、パッケージは複数のバイナリクレートを持つことができます。それぞれのファイルが別々のバイナリクレートになります。
クレートは、関連した機能を一つのスコープにまとめることで、その機能が複数のプロジェクト間で共有しやすいようにします。
例えば、2章で使ったrandクレートは、乱数を生成する機能を提供します。
randクレートを私達のプロジェクトのスコープに持ち込むことで、この機能を私達のプロジェクトで使うことができます。
randクレートが提供する機能にはすべて、クレートの名前randを使ってアクセスできます。
クレートの機能をそれ自身のスコープの中に入れたままにしておくことは、ある機能が私達のクレートで定義されたのかrandクレートで定義されたのかを明確にし、名前の衝突を予防してくれます。
例えば、randクレートはRngという名前のトレイトを提供しています。
更に、私達のクレートでRngという名前のstructを定義することもできます。
クレートの機能はそのスコープ内の名前空間に位置づけられているので、randを依存先として追加しても、コンパイラはRngという名前が何を意味するのかについて混乱することはないのです。
私達のクレートでは、私達の定義したstruct Rngのことであり、randクレートのRngトレイトにはrand::Rngでアクセスするというわけです。
では、モジュールシステムの話に移りましょう!
モジュールを定義して、スコープとプライバシーを制御する
この節では、モジュールと、その他のモジュールシステムの要素
――すなわち、要素に名前をつけるための パス 、パスをスコープに持ち込むuseキーワード、要素を公開するpubキーワード――
について学びます。
また、asキーワード、外部パッケージ、glob演算子についても話します。
とりあえず、今はモジュールに集中しましょう!
モジュール はクレート内のコードをグループ化し、可読性と再利用性を上げるのに役に立ちます。 モジュールは要素の プライバシー も制御できます。プライバシーとは、要素がコードの外側で使える (公開 public) のか、内部の実装の詳細であり外部では使えない (非公開 private) のかです。
例えば、レストランの機能を提供するライブラリクレートを書いてみましょう。 実際にレストランを実装することではなく、コードの関係性に注目したいので、関数にシグネチャをつけますが中身は空白のままにします。
レストラン業界では、レストランの一部を 接客部門 (front of house) といい、その他を 後方部門 (back of house) といいます。 接客部門とはお客さんがいるところです。接客係がお客様を席に案内し、給仕係が注文と支払いを受け付け、バーテンダーが飲み物を作ります。 後方部門とはシェフや料理人がキッチンで働き、皿洗い係が食器を片付け、マネージャが管理業務をする場所です。
私達のクレートを現実のレストランと同じような構造にするために、関数をネストしたモジュールにまとめましょう。
restaurantという名前の新しいライブラリをcargo new --lib restaurantと実行することで作成し、Listing 7-1 のコードを src/lib.rs に書き込み、モジュールと関数のシグネチャを定義してください。
ファイル名: 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() {} } } fn main() {}
Listing 7-1: front_of_houseモジュールにその他のモジュールが含まれ、さらにそれらが関数を含んでいる
モジュールは、modキーワードを書き、次にモジュールの名前(今回の場合、front_of_house)を指定することで定義されます。
モジュールの中には、今回だとhostingとservingのように、他のモジュールをおくこともできます。
モジュールにはその他の要素の定義も置くことができます。例えば、構造体、enum、定数、トレイト、そして(Listing 7-1のように)関数です。
モジュールを使うことで、関連する定義を一つにまとめ、関連する理由を名前で示せます。 このコードを使うプログラマーは、定義を全部読むことなく、グループ単位でコードを読み進められるので、欲しい定義を見つけ出すのが簡単になるでしょう。 このコードに新しい機能を付け加えるプログラマーは、プログラムのまとまりを保つために、どこにその機能のコードを置けば良いのかがわかるでしょう。
以前、 src/main.rs と src/lib.rs はクレートルートと呼ばれていると言いました。
この名前のわけは、 モジュールツリー と呼ばれるクレートのモジュール構造の根っこ (ルート)にこれら2つのファイルの中身がcrateというモジュールを形成するからです。
Listing 7-2は、Listing 7-1の構造のモジュールツリーを示しています。
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
Listing 7-2: Listing 7-1 のコードのモジュールツリー
このツリーを見ると、どのモジュールがどのモジュールの中にネストしているのかがわかります(例えば、hostingはfront_of_houseの中にネストしています)。
また、いくつかのモジュールはお互いに 兄弟 の関係にある、つまり、同じモジュール内で定義されていることもわかります(例えばhostingとservingはfront_of_houseで定義されています)。
他にも、家族関係の比喩を使って、モジュールAがモジュールBの中に入っている時、AはBの 子 であるといい、BはAの 親 であるといいます。
モジュールツリー全体が、暗黙のうちに作られたcrateというモジュールの下にあることにも注目してください。
モジュールツリーを見ていると、コンピュータのファイルシステムのディレクトリツリーを思い出すかもしれません。その喩えはとても適切です! ファイルシステムのディレクトリのように、モジュールはコードをまとめるのに使われるのです。 そしてディレクトリからファイルを見つけるように、目的のモジュールを見つけ出す方法が必要になります。
モジュールツリーの要素を示すためのパス
ファイルシステムの中を移動する時と同じように、Rustにモジュールツリー内の要素を見つけるためにはどこを探せばいいのか教えるためにパスを使います。 関数を呼び出したいなら、そのパスを知っていなければなりません。
パスは2つの形を取ることができます:
- 絶対パス は、クレートの名前か
crateという文字列を使うことで、クレートルートからスタートします。 - 相対パス は、
self、superまたは今のモジュール内の識別子を使うことで、現在のモジュールからスタートします。
絶対パスも相対パスも、その後に一つ以上の識別子がダブルコロン(::)で仕切られて続きます。
Listing 7-1の例に戻ってみましょう。
add_to_waitlist関数をどうやって呼べばいいでしょうか?
すなわち、add_to_waitlistのパスは何でしょうか?
Listing 7-3 は、モジュールと関数をいくつか取り除いてコードをやや簡潔にしています。
これを使って、クレートルートに定義された新しいeat_at_restaurantという関数から、add_to_waitlist関数を呼びだす2つの方法を示しましょう。
eat_at_restaurant関数はこのライブラリクレートの公開 (public) APIの1つなので、pubキーワードをつけておきます。
pubについては、パスをpubキーワードで公開するの節でより詳しく学びます。
この例はまだコンパイルできないことに注意してください。理由はすぐに説明します。
ファイル名: src/lib.rs
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();
}
Listing 7-3: add_to_waitlist 関数を絶対パスと相対パスで呼び出す
eat_at_restaurantで最初にadd_to_waitlist関数を呼び出す時、絶対パスを使っています。
add_to_waitlist関数はeat_at_restaurantと同じクレートで定義されているので、crateキーワードで絶対パスを始めることができます。
crateの後は、add_to_waitlistにたどり着くまで、後に続くモジュールを書き込んでいます。
同じ構造のファイルシステムを想像すれば、/front_of_house/hosting/add_to_waitlistとパスを指定してadd_to_waitlistを実行していることに相当します。
crateという名前を使ってクレートルートからスタートするというのは、/を使ってファイルシステムのルートからスタートするようなものです。
eat_at_restaurantで2回目にadd_to_waitlist関数を呼び出す時、相対パスを使っています。
パスは、モジュールツリーにおいてeat_at_restaurantと同じ階層で定義されているモジュールであるfront_of_houseからスタートします。
これはファイルシステムで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への絶対パスは同じままですが、相対パスは更新しないといけないでしょう。
コードの定義と、その要素の呼び出しは独立に動かしそうなので、絶対パスのほうが好ましいです。
では、Listing 7-3 をコンパイルしてみて、どうしてこれはまだコンパイルできないのか考えてみましょう! エラーをListing 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();
| ^^^^^^^
error[E0603]: module `hosting` is private
--> src/lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant`.
To learn more, run the command again with --verbose.
Listing 7-4: Listing 7-3のコードをビルドしたときのコンパイルエラー
エラーメッセージは、hostingは非公開 (private) だ、と言っています。
言い換えるなら、hostingモジュールとadd_to_waitlist関数へのパスは正しいが、非公開な部分へのアクセスは許可されていないので、Rustがそれを使わせてくれないということです。
モジュールはコードの整理に役立つだけではありません。 モジュールはRustの プライバシー境界 も定義します。これは、外部のコードが知ったり、呼び出したり、依存したりしてはいけない実装の詳細をカプセル化する線引きです。 なので、関数や構造体といった要素を非公開にしたければ、モジュールに入れればよいのです。
Rustにおけるプライバシーは、「あらゆる要素(関数、メソッド、構造体、enum、モジュールおよび定数)は標準では非公開」というやり方で動いています。 親モジュールの要素は子モジュールの非公開要素を使えませんが、子モジュールの要素はその祖先モジュールの要素を使えます。 これは、子モジュールは実装の詳細を覆い隠しますが、子モジュールは自分の定義された文脈を見ることができるためです。 レストランの喩えを続けるなら、レストランの後方部門になったつもりでプライバシーのルールを考えてみてください。レストランの顧客にはそこで何が起こっているのかは非公開ですが、そこで働くオフィスマネージャには、レストランのことは何でも見えるし何でもできるのです。
Rustは、内部実装の詳細を隠すことが標準であるようにモジュールシステムを機能させることを選択しました。
こうすることで、内部のコードのどの部分が、外部のコードを壊すことなく変更できるのかを知ることができます。
しかし、pubキーワードを使って要素を公開することで、子モジュールの内部部品を外部の祖先モジュールに見せることができます。
パスをpubキーワードで公開する
Listing 7-4の、hostingモジュールが非公開だと言ってきていたエラーに戻りましょう。
親モジュールのeat_at_restaurant関数が子モジュールのadd_to_waitlist関数にアクセスできるようにしたいので、hostingモジュールにpubキーワードをつけます。Listing 7-5のようになります。
ファイル名: src/lib.rs
mod front_of_house {
pub 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();
}
Listing 7-5: hosting モジュールを pub として宣言することでeat_at_restaurantから使う
残念ながら、Listing 7-5 のコードもListing 7-6 に示されるようにエラーとなります。
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:9:37
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:12:30
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant`.
To learn more, run the command again with --verbose.
Listing 7-6: Listing 7-5 のコードをビルドしたときのコンパイルエラー
何が起きたのでしょう?pubキーワードをmod hostingの前に追加したことで、このモジュールは公開されました。
この変更によって、front_of_houseにアクセスできるなら、hostingにもアクセスできるようになりました。
しかしhostingの 中身 はまだ非公開です。モジュールを公開してもその中身は公開されないのです。
モジュールにpubキーワードがついていても、祖先モジュールのコードはモジュールを参照できるようになるだけです。
Listing 7-6 のエラーはadd_to_waitlist関数が非公開だと言っています。
プライバシーのルールは、モジュール同様、構造体、enum、関数、メソッドにも適用されるのです。
add_to_waitlistの定義の前にpubキーワードを追加して、これも公開しましょう。
ファイル名: src/lib.rs
mod front_of_house { pub mod hosting { pub 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(); } fn main() {}
Listing 7-7: pubキーワードをmod hostingとfn add_to_waitlistに追加することで、eat_at_restaurantからこの関数を呼べるようになる
これでこのコードはコンパイルできます!
絶対パスと相対パスをもう一度確認して、どうしてpubキーワードを追加することでadd_to_waitlistのそれらのパスを使えるようになるのか、プライバシールールの観点からもう一度確認してみてみましょう。
絶対パスは、クレートのモジュールツリーのルートである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が付いていますから、残りのパスについても問題はなく、この関数呼び出しは有効というわけです。
相対パスをsuperで始める
親モジュールから始まる相対パスなら、superを最初につけることで構成できます。
ファイルシステムパスを..構文で始めるのに似ています。
どのようなときにこの機能が使いたくなるのでしょう?
シェフが間違った注文を修正し、自分でお客さんに持っていくという状況をモデル化している、Listing 7-8 を考えてみてください。
fix_incorrect_order関数はserve_order関数を呼び出すために、superから始まるserve_order関数へのパスを使っています。
ファイル名: src/lib.rs
fn serve_order() {} mod back_of_house { fn fix_incorrect_order() { cook_order(); super::serve_order(); } fn cook_order() {} } fn main() {}
Listing 7-8: super で始まる相対パスを使って関数を呼び出す
fix_incorrect_order関数はback_of_houseモジュールの中にあるので、superを使ってback_of_houseの親モジュールにいけます。親モジュールは、今回の場合ルートであるcrateです。
そこから、serve_orderを探し、見つけ出します。
成功!
もしクレートのモジュールツリーを再編成することにした場合でも、back_of_houseモジュールとserve_order関数は同じ関係性で有り続け、一緒に動くように思われます。
そのため、superを使うことで、将来このコードが別のモジュールに移動するとしても、更新する場所が少なくて済むようにしました。
構造体とenumを公開する
構造体やenumもpubを使って公開するよう指定できますが、追加の細目がいくつかあります。
構造体定義の前にpubを使うと、構造体は公開されますが、構造体のフィールドは非公開のままなのです。
それぞれのフィールドを公開するか否かを個々に決められます。
Listing 7-9 では、公開のtoastフィールドと、非公開のseasonal_fruitフィールドをもつ公開のback_of_house::Breakfast構造体を定義しました。
これは、例えば、レストランで、お客さんが食事についてくるパンの種類は選べるけれど、食事についてくるフルーツは季節と在庫に合わせてシェフが決める、という状況をモデル化しています。
提供できるフルーツはすぐに変わるので、お客さんはフルーツを選ぶどころかどんなフルーツが提供されるのか知ることもできません。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { 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 // 夏 (summer) にライ麦 (Rye) パン付き朝食を注文 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"); } }
Listing 7-9: 公開のフィールドと非公開のフィールドとを持つ構造体
back_of_house::Breakfastのtoastフィールドは公開されているので、eat_at_restaurantにおいてtoastをドット記法を使って読み書きできます。
seasonal_fruitは非公開なので、eat_at_restaurantにおいてseasonal_fruitは使えないということに注意してください。
seasonal_fruitを修正している行のコメントを外して、どのようなエラーが得られるか試してみてください!
また、back_of_house::Breakfastは非公開のフィールドを持っているので、Breakfastのインスタンスを作成 (construct) する公開された関連関数が構造体によって提供されている必要があります(ここではsummerと名付けました)。
もしBreakfastにそのような関数がなかったら、eat_at_restaurantにおいて非公開であるseasonal_fruitの値を設定できないので、Breakfastのインスタンスを作成できません。
一方で、enumを公開すると、そのヴァリアントはすべて公開されます。
Listing 7-10 に示されているように、pubはenumキーワードの前にだけおけばよいのです。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { 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; } }
Listing 7-10: enumを公開に指定することはそのヴァリアントをすべて公開にする
Appetizerというenumを公開したので、SoupとSaladというヴァリアントもeat_at_restaurantで使えます。
enumはヴァリアントが公開されてないとあまり便利ではないのですが、毎回enumのすべてのヴァリアントにpubをつけるのは面倒なので、enumのヴァリアントは標準で公開されるようになっているのです。
構造体はフィールドが公開されていなくても便利なことが多いので、構造体のフィールドは、pubがついてない限り標準で非公開という通常のルールに従うわけです。
まだ勉強していない、pubの関わるシチュエーションがもう一つあります。モジュールシステムの最後の機能、useキーワードです。
use自体の勉強をした後、pubとuseを組み合わせる方法についてお見せします。
useキーワードでパスをスコープに持ち込む
これまで関数呼び出しのために書いてきたパスは、長く、繰り返しも多くて不便なものでした。
例えば、Listing 7-7 においては、絶対パスを使うか相対パスを使うかにかかわらず、add_to_waitlist関数を呼ぼうと思うたびにfront_of_houseとhostingも指定しないといけませんでした。
ありがたいことに、この手続きを簡単化する方法があります。
useキーワードを使うことで、パスを一度スコープに持ち込んでしまえば、それ以降はパス内の要素がローカルにあるかのように呼び出すことができるのです。
Listing 7-11 では、crate::front_of_house::hostingモジュールをeat_at_restaurant関数のスコープに持ち込むことで、eat_at_restaurantにおいて、hosting::add_to_waitlistと指定するだけでadd_to_waitlist関数を呼び出せるようにしています。
ファイル名: src/lib.rs
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(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); } fn main() {}
Listing 7-11: use でモジュールをスコープに持ち込む
useとパスをスコープに追加することは、ファイルシステムにおいてシンボリックリンクを張ることに似ています。
use crate::front_of_house::hostingをクレートルートに追加することで、hostingはこのスコープで有効な名前となり、まるでhostingはクレートルートで定義されていたかのようになります。
スコープにuseで持ち込まれたパスも、他のパスと同じようにプライバシーがチェックされます。
useと相対パスで要素をスコープに持ち込むこともできます。
Listing 7-12 はListing 7-11 と同じふるまいを得るためにどう相対パスを書けば良いかを示しています。
ファイル名: src/lib.rs
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use self::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); } fn main() {}
Listing 7-12: モジュールをuseと相対パスを使ってスコープに持ち込む
慣例に従ったuseパスを作る
Listing 7-11 を見て、なぜuse crate::front_of_house::hostingと書いてeat_at_restaurant内でhosting::add_to_waitlistと呼び出したのか不思議に思っているかもしれません。Listing 7-13 のように、useでadd_to_waitlistまでのパスをすべて指定しても同じ結果が得られるのに、と。
ファイル名: src/lib.rs
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(); add_to_waitlist(); add_to_waitlist(); } fn main() {}
Listing 7-13: add_to_waitlist 関数をuse でスコープに持ち込む。このやりかたは慣例的ではない
Listing 7-11 も 7-13 もおなじ仕事をしてくれますが、関数をスコープにuseで持ち込む場合、Listing 7-11 のほうが慣例的なやり方です。
関数の親モジュールをuseで持ち込むことで、関数を呼び出す際、毎回親モジュールを指定しなければならないようにすれば、フルパスを繰り返して書くことを抑えつつ、関数がローカルで定義されていないことを明らかにできます。
Listing 7-13 のコードではどこでadd_to_waitlistが定義されたのかが不明瞭です。
一方で、構造体やenumその他の要素をuseで持ち込むときは、フルパスを書くのが慣例的です。
Listing 7-14 は標準ライブラリのHashMap構造体をバイナリクレートのスコープに持ち込む慣例的なやり方を示しています。
ファイル名: src/main.rs
use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }
Listing 7-14: HashMapを慣例的なやり方でスコープに持ち込む
こちらの慣例の背後には、はっきりとした理由はありません。自然に発生した慣習であり、みんなRustのコードをこのやり方で読み書きするのに慣れてしまったというだけです。
同じ名前の2つの要素をuseでスコープに持ち込むのはRustでは許されないので、そのときこの慣例は例外的に不可能です。
Listing 7-15は、同じ名前を持つけれど異なる親モジュールを持つ2つのResult型をスコープに持ち込み、それらを参照するやり方を示しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::fmt; use std::io; fn function1() -> fmt::Result { // --snip-- // (略) Ok(()) } fn function2() -> io::Result<()> { // --snip-- // (略) Ok(()) } }
Listing 7-15: 同じ名前を持つ2つの型を同じスコープに持ち込むには親モジュールを使わないといけない。
このように、親モジュールを使うことで2つのResult型を区別できます。
もしuse std::fmt::Result と use std::io::Resultと書いていたとしたら、2つのResult型が同じスコープに存在することになり、私達がResultを使ったときにどちらのことを意味しているのかRustはわからなくなってしまいます。
新しい名前をasキーワードで与える
同じ名前の2つの型をuseを使って同じスコープに持ち込むという問題には、もう一つ解決策があります。パスの後に、asと型の新しいローカル名、即ちエイリアスを指定すればよいのです。
Listing 7-16 は、Listing 7-15 のコードを、2つのResult型のうち一つをasを使ってリネームするという別のやり方で書いたものを表しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::fmt::Result; use std::io::Result as IoResult; fn function1() -> Result { // --snip-- Ok(()) } fn function2() -> IoResult<()> { // --snip-- Ok(()) } }
Listing 7-16: 型がスコープに持ち込まれた時、asキーワードを使ってその名前を変えている
2つめのuse文では、std::io::Resultに、IoResultという新たな名前を選んでやります。std::fmtのResultもスコープに持ち込んでいますが、この名前はこれとは衝突しません。
Listing 7-15もListing 7-16も慣例的とみなされているので、どちらを使っても構いませんよ!
pub useを使って名前を再公開する
useキーワードで名前をスコープに持ちこんだ時、新しいスコープで使用できるその名前は非公開です。
私達のコードを呼び出すコードが、まるでその名前が私達のコードのスコープで定義されていたかのように参照できるようにするためには、pubとuseを組み合わせればいいです。
このテクニックは、要素を自分たちのスコープに持ち込むだけでなく、他の人がその要素をその人のスコープに持ち込むことも可能にすることから、再公開 (re-exporting) と呼ばれています。
Listing 7-17 は Listing 7-11 のコードのルートモジュールでのuseをpub useに変更したものを示しています。
ファイル名: src/lib.rs
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(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); } fn main() {}
Listing 7-17: pub useで、新たなスコープのコードがその名前を使えるようにする
pub useを使うことで、外部のコードがhosting::add_to_waitlistを使ってadd_to_waitlist関数を呼び出せるようになりました。
pub useを使っていなければ、eat_at_restaurant関数はhosting::add_to_waitlistを自らのスコープ内で使えるものの、外部のコードはこの新しいパスを利用することはできないでしょう。
再公開は、あなたのコードの内部構造と、あなたのコードを呼び出すプログラマーたちのその領域に関しての見方が異なるときに有用です。
例えば、レストランの比喩では、レストランを経営している人は「接客部門 (front of house)」と「後方部門 (back of house)」のことについて考えるでしょう。
しかし、レストランを訪れるお客さんは、そのような観点からレストランの部門について考えることはありません。
pub useを使うことで、ある構造でコードを書きつつも、別の構造で公開するということが可能になります。
こうすることで、私達のライブラリを、ライブラリを開発するプログラマにとっても、ライブラリを呼び出すプログラマにとっても、よく整理されたものとすることができます。
外部のパッケージを使う
2章で、乱数を得るためにrandという外部パッケージを使って、数当てゲームをプログラムしました。
randを私達のプロジェクトで使うために、次の行を Cargo.toml に書き加えましたね:
ファイル名: Cargo.toml
rand = "0.8.3"
randを依存 (dependency) として Cargo.toml に追加すると、randパッケージとそのすべての依存をcrates.ioからダウンロードして、私達のプロジェクトでrandが使えるようにするようCargoに命令します。
そして、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..101);
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言語に同梱されているので、 Cargo.toml を stdを含むように変更する必要はありません。
しかし、その要素をそこから私達のパッケージのスコープに持ち込むためには、useを使って参照する必要はあります。
例えば、HashMapには次の行を使います。
#![allow(unused)] fn main() { use std::collections::HashMap; }
これは標準ライブラリクレートの名前stdから始まる絶対パスです。
巨大なuseのリストをネストしたパスを使って整理する
同じクレートか同じモジュールで定義された複数の要素を使おうとする時、それぞれの要素を一行一行並べると、縦に大量のスペースを取ってしまいます。
例えば、Listing 2-4の数当てゲームで使った次の2つのuse文がstdからスコープへ要素を持ち込みました。
ファイル名: src/main.rs
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, 101);
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行でスコープに持ち込めます。 これをするには、Listing 7-18 に示されるように、パスの共通部分を書き、2つのコロンを続け、そこで波括弧で互いに異なる部分のパスのリストを囲みます。
ファイル名: src/main.rs
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, 101);
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!"),
}
}
Listing 7-18: 同じプレフィックスをもつ複数の要素をスコープに持ち込むためにネストしたパスを指定する
大きなプログラムにおいては、同じクレートやモジュールからのたくさんの要素をネストしたパスで持ち込むようにすれば、独立したuse文の数を大きく減らすことができます!
ネストしたパスはパスのどの階層においても使うことができます。これはサブパスを共有する2つのuse文を合体させるときに有用です。
例えば、Listing 7-19 は2つのuse文を示しています:1つはstd::ioをスコープに持ち込み、もう一つはstd::io::Writeをスコープに持ち込んでいます。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::io; use std::io::Write; }
Listing 7-19: 片方がもう片方のサブパスである2つのuse文
これらの2つのパスの共通部分はstd::ioであり、そしてこれは最初のパスにほかなりません。これらの2つのパスを1つのuse文へと合体させるには、Listing 7-20 に示されるように、ネストしたパスにselfを使いましょう。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::io::{self, Write}; }
Listing 7-20: Listing 7-19 のパスを一つの use 文に合体させる
この行は std::io とstd::io::Write をスコープに持ち込みます。
glob演算子
パスにおいて定義されているすべての公開要素をスコープに持ち込みたいときは、glob演算子 * をそのパスの後ろに続けて書きましょう:
#![allow(unused)] fn main() { use std::collections::*; }
このuse文はstd::collectionsのすべての公開要素を現在のスコープに持ち込みます。
glob演算子を使う際にはご注意を!
globをすると、どの名前がスコープ内にあり、プログラムで使われている名前がどこで定義されたのか分かりづらくなります。
glob演算子はしばしば、テストの際、テストされるあらゆるものをtestsモジュールに持ち込むために使われます。これについては11章テストの書き方の節で話します。
glob演算子はプレリュードパターンの一部としても使われることがあります:そのようなパターンについて、より詳しくは標準ライブラリのドキュメントをご覧ください。
モジュールを複数のファイルに分割する
この章のすべての例において、今までのところ、複数のモジュールを一つのファイルに定義していました。 モジュールが大きくなる時、コードを読み進めやすくするため、それらの定義を別のファイルへ移動させたくなるかもしれません。
例えば、Listing 7-17 のコードからはじめましょう。クレートルートのファイルをListing 7-21 のコードを持つように変更して、front_of_houseモジュールをそれ専用のファイルsrc/front_of_house.rsに動かしましょう。
今回、クレートルートファイルはsrc/lib.rsですが、この手続きはクレートルートファイルがsrc/main.rsであるバイナリクレートでもうまく行きます。
ファイル名: src/lib.rs
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
Listing 7-21: front_of_houseモジュールを宣言する。その中身はsrc/front_of_house.rs内にある
そして、 Listing 7-22 のように、src/front_of_house.rs にはfront_of_house モジュールの中身の定義を与えます。
ファイル名: src/front_of_house.rs
pub mod hosting {
pub fn add_to_waitlist() {}
}
Listing 7-22: src/front_of_house.rsにおける、front_of_houseモジュール内部の定義
mod front_of_houseの後にブロックではなくセミコロンを使うと、Rustにモジュールの中身をモジュールと同じ名前をした別のファイルから読み込むように命令します。
私達の例で、つづけてhostingモジュールをそれ専用のファイルに抽出するには、src/front_of_house.rsがhostingモジュールの宣言のみを含むように変更します:
ファイル名: src/front_of_house.rs
pub mod hosting;
さらにsrc/front_of_house ディレクトリとsrc/front_of_house/hosting.rs ファイルを作って、hostingモジュール内でなされていた定義を持つようにします。
ファイル名: src/front_of_house/hosting.rs
#![allow(unused)] fn main() { pub fn add_to_waitlist() {} }
定義は別のファイルにあるにもかかわらず、モジュールツリーは同じままであり、eat_at_restaurant内での関数呼び出しもなんの変更もなくうまく行きます。
このテクニックのおかげで、モジュールが大きくなってきた段階で新しいファイルへ動かす、ということができます。
src/lib.rs におけるpub use crate::front_of_house::hosting という文も変わっていないし、useはどのファイルがクレートの一部としてコンパイルされるかになんの影響も与えないということに注意してください。
modキーワードがモジュールを宣言したなら、Rustはそのモジュールに挿入するためのコードを求めて、モジュールと同じ名前のファイルの中を探すというわけです。
まとめ
Rustでは、パッケージを複数のクレートに、そしてクレートを複数のモジュールに分割して、あるモジュールで定義された要素を他のモジュールから参照することができます。
これは絶対パスか相対パスを指定することで行なえます。
これらのパスはuse文でスコープに持ち込むことができ、こうすると、そのスコープで要素を複数回使う時に、より短いパスで済むようになります。
モジュールのコードは標準では非公開ですが、pubキーワードを追加することで定義を公開することができます。
次の章では、きちんと整理されたあなたのコードで使うことができる、標準ライブラリのいくつかのコレクションデータ構造を見ていきます。
一般的なコレクション
Rustの標準ライブラリは、コレクションと呼ばれる多くの非常に有益なデータ構造を含んでいます。他の多くのデータ型は、 ある一つの値を表しますが、コレクションは複数の値を含むことができます。組み込みの配列とタプル型とは異なり、 これらのコレクションが指すデータはヒープに確保され、データ量はコンパイル時にわかる必要はなく、 プログラムの実行にあわせて、伸縮可能であることになります。各種のコレクションには異なる能力とコストが存在し、 自分の現在の状況に最適なものを選び取るスキルは、時間とともに育っていきます。この章では、 Rustのプログラムにおいて、非常に頻繁に使用される3つのコレクションについて議論しましょう。
- ベクタ型は、可変長の値を並べて保持できる。
- 文字列は、文字のコレクションである。以前、
String型について触れたが、 この章ではより掘り下げていく。 - ハッシュマップは、値を特定のキーと紐付けさせてくれる。より一般的なデータ構造である、 マップの特定の実装である。
標準ライブラリで提供されている他の種のコレクションについて学ぶには、 ドキュメントを参照されたし。
ベクタ型、文字列、ハッシュマップの生成と更新方法や、各々が特別な点について議論していきましょう。
ベクタで値のリストを保持する
最初に見るコレクション型はVec<T>であり、これはベクタとしても知られています。
ベクタは単体のデータ構造でありながら複数の値を保持でき、それらの値をメモリ上に隣り合わせに並べます。
ベクタには同じ型の値しか保持できません。
要素のリストがある場合にベクタは有用です。
例えば、テキストファイルの各行とか、ショッピングカートのアイテムの価格などです。
新しいベクタを生成する
空のベクタを新たに作るには、リスト8-1に示すようにVec::new関数を呼びます。
fn main() { let v: Vec<i32> = Vec::new(); }
リスト8-1:新しい空のベクタを生成してi32型の値を保持する
ここで、型注釈を付けていることに注目してください。
なぜなら、このベクタに対して何も値を挿入していないので、コンパイラには私たちがどんなデータを保持させるつもりか推測できないからです。
これは重要な点です。
ベクタはジェネリクスを使用して実装されています。
あなた自身の型でどうジェネリクスを使用するかついては第10章で解説します。
現時点では標準ライブラリで提供されるVec<T>型は、どんな型でも保持でき、ある特定のベクタがある型を保持するとき、その型は山かっこ内に指定されることを知っておいてください。
リスト8-1では、コンパイラにvのVec<T>はi32型の要素を保持すると指示しました。
いったん値を挿入すると、多くの場合、コンパイラは保持させたい値の型を推論できるようになります。
ですから、より現実的なコードでは、型注釈を付ける必要はあまりないでしょう。
また、初期値を持つVec<T>を生成する方が一般的ですし、Rustにはvec!という便利なマクロも用意されています。
このマクロは与えた値を保持する新しいベクタを生成します。
リスト8-2では、1、2、3という値を持つ新しいVec<i32>を生成しています。
整数型をi32にしているのは、3章の「データ型」節で学んだように、これが標準の整数型だからです。
fn main() { let v = vec![1, 2, 3]; }
リスト8-2: 値を含む新しいベクタを生成する
初期値のi32値を与えたので、コンパイラは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); }
リスト8-3:pushメソッドを使用してベクタに値を追加する
第3章で説明したとおり、どんな変数でも、その値を変更したかったらmutキーワードで可変にする必要があります。
中に配置する数値は全てi32型であり、Rustはこのことをデータから推論するので、Vec<i32>という注釈は不要です。
ベクタをドロップすれば、要素もドロップする
他のあらゆるstruct(構造体)と同様に、ベクタもスコープを抜ければ解放されます。
その様子をリスト8-4に示します。
fn main() { { let v = vec![1, 2, 3, 4]; // vで作業をする } // <- vはここでスコープを抜け、解放される }
リスト8-4:ベクタとその要素がドロップされる箇所を示す
ベクタがドロップされると、その中身もドロップされます。 つまり、保持されていた整数値が片付けられるということです。 これは一見単純そうですが、ベクタの要素に対する参照を使い始めると少し複雑になり得ます。 次はそれに挑戦しましょう!
ベクタの要素を読む
ベクタを生成し、更新し、破棄する方法がわかったので、次のステップでは中身を読む方法について学ぶのが良いでしょう。 ベクタに保持された値を参照する方法は2つあります。 これから示す例では、理解を助けるために、それらの関数からの戻り値型を注釈しています。
リスト8-5はベクタの値にアクセスする両方の方法として、添え字記法とgetメソッドが示されています。
fn main() { let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; println!("The third element is {}", third); match v.get(2) { // "3つ目の要素は{}です" Some(third) => println!("The third element is {}", third), // "3つ目の要素はありません。" None => println!("There is no third element."), } }
リスト8-5:添え字記法かgetメソッドを使用してベクタの要素にアクセスする
ここでは2つのことに注目してください。
1つ目は、3番目の要素を得るのに2という添え字の値を使用していることです。
ベクタは番号で索引化されますが、その番号は0から始まります。
2つ目は、3番目の要素を得る2つの方法とは、&と[]を使用して参照を得るものと、getメソッドに引数として添え字を渡してOption<&T>を得るものだということです。
Rustのベクタには要素を参照する方法が2通りあるので、ベクタに含まれない要素の添え字を使おうとしたときのプログラムの振る舞いを選択できます。 例として、ベクタに5つ要素があるとして、添え字100の要素にアクセスを試みた場合、プログラムがどうなるのか確認しましょう。 リスト8-6に示します。
fn main() { let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100]; let does_not_exist = v.get(100); }
リスト8-6:5つの要素を含むベクタの添え字100の要素にアクセスしようとする
このコードを走らせると、最初の[]メソッドはプログラムをパニックさせます。
なぜなら存在しない要素を参照しているからです。
このメソッドは、ベクタの終端を超えて要素にアクセスしようとしたときにプログラムをクラッシュさせたい場合に最適です。
getメソッドにベクタ外の添え字を渡すと、パニックすることなくNoneを返します。
普通の状況でもベクタの範囲外にアクセスする可能性があるなら、このメソッドを使用することになるでしょう。
その場合、第6章で説明したように、コードはSome(&element)かNoneを扱うロジックを持つことになります。
例えば、誰かが入力した数値が添え字になるかもしれません。
もし誤って大きすぎる値を入力し、プログラムがNone値を得たなら、いまベクタに何要素あるかをユーザに教え、正しい値を再入力してもらうこともできます。
その方が、ただのタイプミスでプログラムをクラッシュさせるより、ユーザに優しいといえそうです。
プログラムに有効な参照がある場合、借用チェッカー (borrow checker) は、(第4章で解説しましたが)所有権と借用規則を強制し、ベクタの中身へのこの参照や他のいかなる参照も有効であり続けることを保証してくれます。
同一スコープ上では、可変と不変な参照を同時には存在させられないというルールを思い出してください。
このルールはリスト8-7でも適用されています。
リスト8-7ではベクタの最初の要素への不変参照を保持しつつ、終端に要素を追加しようとしています。
関数内のここ以降で、この要素(訳注:firstのこと)を参照しようとすると失敗します。
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);
}
リスト8-7:要素への参照を保持しつつ、ベクタに要素を追加しようとする
このコードをコンパイルすると、こんなエラーになります。
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
(エラー: 不変としても借用されているので、`v`を可変で借用できません)
--> 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
| (その後、不変借用はここで使われています)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections`.
To learn more, run the command again with --verbose.
リスト8-7のコードは、一見動きそうに見えるかもしれません。 なぜ最初の要素への参照が、ベクタの終端への変更を気にかける必要があるのでしょうか? このエラーはベクタが動作するしくみによるものです。 新たな要素をベクタの終端に追加するとき、いまベクタのある場所に全要素を隣り合わせに配置するだけのスペースがないなら、新しいメモリを割り当て、古い要素を新しいスペースにコピーする必要があります。 その場合、最初の要素を指す参照は、解放されたメモリを指すことになるでしょう。 借用規則がそのような状況に陥らないよう防いでくれるのです。
注釈:
Vec<T>の実装に関する詳細については、“The Rustonomicon”を参照してください (訳注:日本語版はこちらです)。
ベクタ内の値を順に処理する
ベクタの要素に順番にアクセスしたいなら、添え字で1要素ごとにアクセスするのではなく、全要素を走査することができます。
リスト8-8でforループを使い、i32のベクタの各要素に対する不変な参照を得て、それらを表示する方法を示します。
fn main() { let v = vec![100, 32, 57]; for i in &v { println!("{}", i); } }
リスト8-8:forループで要素を走査し、ベクタの各要素を表示する
また、全要素に変更を加えるために、可変なベクタの各要素への可変な参照を走査することもできます。
リスト8-9のforループでは各要素に50を足しています。
fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } }
リスト8-9:ベクタの要素への可変な参照を走査する
可変参照が参照している値を変更するには、+=演算子を使用する前に、参照外し演算子(*)を使用してiの値にたどり着かないといけません。
参照外し演算子については、第15章の「参照外し演算子で値までポインタを追いかける」節でより詳しく扱います。
Enumを使って複数の型を保持する
この章の冒頭で、ベクタは同じ型の値しか保持できないと述べました。 これは不便なこともあります。 異なる型の要素を保持する必要のあるユースケースは必ず存在します。 幸運なことに、enumの列挙子は同じenumの型の中に定義されるので、ベクタに異なる型の要素を保持する必要が出たら、enumを定義して使用すればよいのです!
例えば、スプレッドシートのある行から値を得ることを考えます。 ここで、その行の中の列には、整数を含むもの、浮動小数点数を含むもの、文字列を含むものがあるとします。 列挙子ごとに異なる値の型を保持するenumが定義できます。 そして、このenumの列挙子は全て同じ型、つまりenumの型、と考えられるわけです。 ですから、そのenumを保持するベクタを作成でき、結果的に異なる型を保持できるようになるわけです。 リスト8-10でこれを実演しています。
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), ]; }
リスト8-10:enumを定義して、一つのベクタに異なる型の値を保持する
個々の要素を格納するのにヒープ上で必要となるメモリの量を正確に把握するめに、Rustコンパイラはコンパイル時にベクタに入る型を知る必要があります。
また、このベクタではどんな型が許容されるのか明示できるという副次的な利点があります。
もしRustが、ベクタにどんな型でも保持できることを許していたら、ベクタの要素に対して行われる処理に対して、いくつかの型がエラーを引き起こすかもしれません。
enumに加えてmatch式を使うことで、第6章で説明したとおり、あらゆるケースが処理できることを、Rustがコンパイル時に保証することになります。
プログラムを書いている時点で、プログラムが実行時に取得し、ベクタに格納し得る全ての型を網羅できない場合には、このenumを使ったテクニックはうまくいかないでしょう。 代わりにトレイトオブジェクトを使用できます。 こちらは第17章で取り上げます。
これまでにベクタの代表的な使い方をいくつか紹介しました。
標準ライブラリでVec<T>に定義されている多くの有益なメソッドについて、APIドキュメントを必ず確認するようにしてください。
例えば、pushに加えて、popというメソッドがあり、これは最後の要素を削除して返します。
それでは次のコレクション型であるStringに移りましょう!
文字列でUTF-8でエンコードされたテキストを保持する
第4章で文字列について語りましたが、今度はより掘り下げていきましょう。新参者のRustaceanは、 3つの概念の組み合わせにより、文字列でよく行き詰まります: Rustのありうるエラーを晒す性質、 多くのプログラマが思っている以上に文字列が複雑なデータ構造であること、そしてUTF-8です。 これらの要因が、他のプログラミング言語から移ってきた場合、一見困難に見えるように絡み合うわけです。
コレクションの文脈で文字列を議論することは、有用なことです。なぜなら、文字列はテキストとして解釈された時に有用になる機能を提供するメソッドと、
バイトのコレクションで実装されているからです。この節では、生成、更新、読み込みのような全コレクションが持つStringの処理について語ります。
また、Stringが他のコレクションと異なる点についても議論します。具体的には、人間とコンピュータがStringデータを解釈する方法の差異により、
Stringに添え字アクセスする方法がどう複雑なのかということです。
文字列とは?
まずは、文字列という用語の意味を定義しましょう。Rustには、言語の核として1種類しか文字列型が存在しません。
文字列スライスのstrで、通常借用された形態&strで見かけます。第4章で、文字列スライスについて語りました。
これは、別の場所に格納されたUTF-8エンコードされた文字列データへの参照です。例えば、文字列リテラルは、
プログラムのバイナリ出力に格納されるので、文字列スライスになります。
String型は、言語の核として組み込まれるのではなく、Rustの標準ライブラリで提供されますが、伸長可能、
可変、所有権のあるUTF-8エンコードされた文字列型です。RustaceanがRustにおいて「文字列」を指したら、
どちらかではなく、Stringと文字列スライスの&strのことを通常意味します。この節は、大方、
Stringについてですが、どちらの型もRustの標準ライブラリで重宝されており、
どちらもUTF-8エンコードされています。
また、Rustの標準ライブラリには、他の文字列型も含まれています。OsString、OsStr、CString、CStrなどです。
ライブラリクレートにより、文字列データを格納する選択肢はさらに増えます。
それらの名前が全てStringかStrで終わっているのがわかりますか?所有権ありと借用されたバージョンを指しているのです。
ちょうど以前見かけたStringと&strのようですね。例えば、これらの文字列型は、異なるエンコード方法でテキストを格納していたり、
メモリ上の表現が異なったりします。この章では、これらの他の種類の文字列については議論しません;
使用方法やどれが最適かについては、APIドキュメントを参照してください。
新規文字列を生成する
Vec<T>で使用可能な処理の多くがStringでも使用できます。文字列を生成するnew関数から始めましょうか。
リスト8-11に示したようにですね。
#![allow(unused)] fn main() { let mut s = String::new(); }
リスト8-11: 新しい空のStringを生成する
この行は、新しい空のsという文字列を生成しています。それからここにデータを読み込むことができるわけです。
だいたい、文字列の初期値を決めるデータがあるでしょう。そのために、to_stringメソッドを使用します。
このメソッドは、文字列リテラルのように、Displayトレイトを実装する型ならなんでも使用できます。
リスト8-12に2例、示しています。
#![allow(unused)] 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(); }
リスト8-12: to_stringメソッドを使用して文字列リテラルからStringを生成する
このコードは、initial contents(初期値)を含む文字列を生成します。
さらに、String::from関数を使っても、文字列リテラルからStringを生成することができます。
リスト8-13のコードは、to_stringを使用するリスト8-12のコードと等価です。
#![allow(unused)] fn main() { let s = String::from("initial contents"); }
リスト8-13: String::from関数を使って文字列リテラルからStringを作る
文字列は、非常に多くのものに使用されるので、多くの異なる一般的なAPIを使用でき、たくさんの選択肢があるわけです。
冗長に思われるものもありますが、適材適所です!今回の場合、String::fromとto_stringは全く同じことをします。
従って、どちらを選ぶかは、スタイル次第です。
文字列はUTF-8エンコードされていることを覚えていますか?要するに文字列には、適切にエンコードされていればどんなものでも含めます。 リスト8-14に示したように。
#![allow(unused)] 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"); }
リスト8-14: いろんな言語の挨拶を文字列に保持する
これらは全て、有効なStringの値です。
文字列を更新する
Stringは、サイズを伸ばすことができ、Vec<T>の中身のように、追加のデータをプッシュすれば、中身も変化します。
付け加えると、String値を連結する+演算子や、format!マクロを便利に使用することができます。
push_strとpushで文字列に追加する
push_strメソッドで文字列スライスを追記することで、Stringを伸ばすことができます。
リスト8-15の通りです。
#![allow(unused)] fn main() { let mut s = String::from("foo"); s.push_str("bar"); }
リスト8-15: push_strメソッドでStringに文字列スライスを追記する
この2行の後、sはfoobarを含むことになります。push_strメソッドは、必ずしも引数の所有権を得なくていいので、
文字列スライスを取ります。例えば、リスト8-16のコードは、中身をs1に追加した後、
s2を使えなかったら残念だということを示しています。
#![allow(unused)] fn main() { let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(s2); println!("s2 is {}", s2); }
リスト8-16: 中身をStringに追加した後に、文字列スライスを使用する
もし、push_strメソッドがs2の所有権を奪っていたら、最後の行でその値を出力することは不可能でしょう。
ところが、このコードは予想通りに動きます!
pushメソッドは、1文字を引数として取り、Stringに追加します。リスト8-15は、
pushメソッドでlをStringに追加するコードを呈示しています。
#![allow(unused)] fn main() { let mut s = String::from("lo"); s.push('l'); }
リスト8-17: pushでString値に1文字を追加する
このコードの結果、sはlolを含むことになるでしょう。
編者注:
lolはlaughing out loud(大笑いする)の頭文字からできたスラングです。 日本語のwwwみたいなものですね。
+演算子、またはformat!マクロで連結
2つのすでにある文字列を組み合わせたくなることがよくあります。リスト8-18に示したように、
一つ目の方法は、+演算子を使用することです。
#![allow(unused)] fn main() { let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // s1はムーブされ、もう使用できないことに注意 }
リスト8-18: +演算子を使用して二つのString値を新しいString値にする
このコードの結果、s3という文字列は、Hello, world!を含むことになるでしょう。
追記の後、s1がもう有効でなくなった理由と、s2への参照を使用した理由は、
+演算子を使用した時に呼ばれるメソッドのシグニチャと関係があります。+演算子は、addメソッドを使用し、
そのシグニチャは以下のような感じです:
fn add(self, s: &str) -> String {
これは、標準ライブラリにあるシグニチャそのものではありません: 標準ライブラリでは、addはジェネリクスで定義されています。
ここでは、ジェネリックな型を具体的な型に置き換えたaddのシグニチャを見ており、これは、
このメソッドをString値とともに呼び出した時に起こることです。ジェネリクスについては、第10章で議論します。
このシグニチャが、+演算子の巧妙な部分を理解するのに必要な手がかりになるのです。
まず、s2には&がついてます。つまり、add関数のs引数のために最初の文字列に2番目の文字列の参照を追加するということです:
Stringには&strを追加することしかできません。要するに2つのString値を追加することはできないのです。
でも待ってください。addの第2引数で指定されているように、&s2の型は、&strではなく、
&Stringではないですか。では、なぜ、リスト8-18は、コンパイルできるのでしょうか?
add呼び出しで&s2を使える理由は、コンパイラが&String引数を&strに型強制してくれるためです。
addメソッド呼び出しの際、コンパイラは、参照外し型強制というものを使用し、ここでは、
&s2を&s2[..]に変えるものと考えることができます。参照外し型強制について詳しくは、第15章で議論します。
addがs引数の所有権を奪わないので、この処理後もs2が有効なStringになるわけです。
2番目に、シグニチャからaddはselfの所有権をもらうことがわかります。selfには&がついていないからです。
これはつまり、リスト8-18においてs1はadd呼び出しにムーブされ、その後は有効ではなくなるということです。
故に、s3 = s1 + &s2;は両文字列をコピーして新しいものを作るように見えますが、
この文は実際にはs1の所有権を奪い、s2の中身のコピーを追記し、結果の所有権を返すのです。言い換えると、
たくさんのコピーをしているように見えますが、違います; 実装は、コピーよりも効率的です。
複数の文字列を連結する必要が出ると、+演算子の振る舞いは扱いにくくなります:
#![allow(unused)] 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!マクロを使用することができます:
#![allow(unused)] 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!を使用したコードの方がはるかに読みやすく、
引数の所有権を奪いません。
文字列に添え字アクセスする
他の多くのプログラミング言語では、文字列中の文字に、添え字で参照してアクセスすることは、有効なコードであり、
一般的な処理です。しかしながら、Rustにおいて、添え字記法でStringの一部にアクセスしようとすると、
エラーが発生するでしょう。リスト8-19の非合法なコードを考えてください。
let s1 = String::from("hello");
let h = s1[0];
リスト8-19: 文字列に対して添え字記法を試みる
このコードは、以下のようなエラーに落ち着きます:
error[E0277]: the trait bound `std::string::String: std::ops::Index<{Integer}>` is not satisfied
(エラー: トレイト境界`std::string::String: std::ops::Index<{Integer}>`が満たされていません)
|>
3 |> let h = s1[0];
|> ^^^^^ the type `std::string::String` cannot be indexed by `{Integer}`
|> (型`std::string::String`は`{Integer}`で添え字アクセスできません)
= help: the trait `std::ops::Index<{Integer}>` is not implemented for `std::string::String`
(ヘルプ: `std::ops::Index<{Integer}>`というトレイトが`std::string::String`に対して実装されていません)
エラーと注釈が全てを物語っています: Rustの文字列は、添え字アクセスをサポートしていないのです。 でも、なぜでしょうか?その疑問に答えるには、Rustがメモリにどのように文字列を保持しているかについて議論する必要があります。
内部表現
StringはVec<u8>のラッパです。リスト8-14から適切にUTF-8でエンコードされた文字列の例をご覧ください。
まずは、これ:
#![allow(unused)] fn main() { let len = String::from("Hola").len(); }
この場合、lenは4になり、これは、文字列"Hola"を保持するベクタの長さが4バイトであることを意味します。
これらの各文字は、UTF-8でエンコードすると、1バイトになるのです。しかし、以下の行ではどうでしょうか?
(この文字列は大文字のキリル文字Zeで始まり、アラビア数字の3では始まっていないことに注意してください)
#![allow(unused)] fn main() { let len = String::from("Здравствуйте").len(); }
文字列の長さはと問われたら、あなたは12と答えるかもしれません。ところが、Rustの答えは、24です: “Здравствуйте”をUTF-8でエンコードすると、この長さになります。各Unicodeスカラー値は、2バイトの領域を取るからです。 それ故に、文字列のバイトの添え字は、必ずしも有効なUnicodeのスカラー値とは相互に関係しないのです。 デモ用に、こんな非合法なRustコードを考えてください:
let hello = "Здравствуйте";
let answer = &hello[0];
answerの値は何になるべきでしょうか?最初の文字のЗになるべきでしょうか?UTF-8エンコードされた時、
Зの最初のバイトは208、2番目は151になるので、answerは実際、208になるべきですが、
208は単独では有効な文字ではありません。この文字列の最初の文字を求めている場合、208を返すことは、
ユーザの望んでいるものではないでしょう; しかしながら、Rustには、バイト添え字0の位置には、そのデータしかないのです。
文字列がラテン文字のみを含む場合でも、ユーザは一般的にバイト値が返ることを望みません:
&"hello"[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でそのパフォーマンスを保証することはできません。というのも、
合法な文字がいくつあるか決定するのに、最初から添え字まで中身を走査する必要があるからです。
文字列をスライスする
文字列に添え字アクセスするのは、しばしば悪い考えです。文字列添え字処理の戻り値の型が明瞭ではないからです:
バイト値、文字、書記素クラスタ、あるいは文字列スライスにもなります。故に、文字列スライスを生成するのに、
添え字を使う必要が本当に出た場合にコンパイラは、もっと特定するよう求めてきます。添え字アクセスを特定し、
文字列スライスが欲しいと示唆するためには、[]で1つの数値により添え字アクセスするのではなく、
範囲とともに[]を使って、特定のバイトを含む文字列スライスを作ることができます:
#![allow(unused)] fn main() { let hello = "Здравствуйте"; let s = &hello[0..4]; }
ここで、sは文字列の最初の4バイトを含む&strになります。先ほど、これらの文字は各々2バイトになると指摘しましたから、
sはЗдになります。
&hello[0..1]と使用したら、何が起きるでしょうか?答え: Rustはベクタの非合法な添え字にアクセスしたかのように、
実行時にパニックするでしょう:
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/libcore/str/mod.rs:2188:4
('main'スレッドは「バイト添え字1は文字の境界ではありません; `Здравствуйте`の'З'(バイト番号0から2)の中です」でパニックしました)
範囲を使用して文字列スライスを作る際にはプログラムをクラッシュさせることがあるので、気をつけるべきです。
文字列を走査するメソッド群
幸いなことに、他の方法でも文字列の要素にアクセスすることができます。
もし、個々のUnicodeスカラー値に対して処理を行う必要があったら、最適な方法はcharsメソッドを使用するものです。
“नमस्ते”に対してcharsを呼び出したら、分解して6つのchar型の値を返すので、各要素にアクセスするには、
その結果を走査すればいいわけです:
#![allow(unused)] fn main() { for c in "नमस्ते".chars() { println!("{}", c); } }
このコードは、以下のように出力します:
न
म
स
्
त
े
bytesメソッドは、各バイトをそのまま返すので、最適になることもあるかもしれません:
#![allow(unused)] fn main() { for b in "नमस्ते".bytes() { println!("{}", b); } }
このコードは、Stringをなす18バイトを出力します:
224
164
// --snip--
165
135
ですが、合法なUnicodeスカラー値は、2バイト以上からなる場合もあることは心得ておいてください。
書記素クラスタを文字列から得る方法は複雑なので、この機能は標準ライブラリでは提供されていません。 この機能が必要なら、crates.ioでクレートを入手可能です。
文字列はそう単純じゃない
まとめると、文字列は込み入っています。プログラミング言語ごとにこの複雑性をプログラマに提示する方法は違います。
Rustでは、Stringデータを正しく扱うことが、全てのRustプログラムにとっての既定動作になっているわけであり、
これは、プログラマがUTF-8データを素直に扱う際に、よりしっかり考えないといけないことを意味します。
このトレードオフにより、他のプログラミング言語で見えるよりも文字列の複雑性がより露出していますが、
ASCII以外の文字に関するエラーを開発の後半で扱わなければならない可能性が排除されているのです。
もう少し複雑でないものに切り替えていきましょう: ハッシュマップです!
キーとそれに紐づいた値をハッシュマップに格納する
一般的なコレクションのトリを飾るのは、ハッシュマップです。型HashMap<K, V>は、
K型のキーとV型の値の対応関係を保持します。これをハッシュ関数を介して行います。
ハッシュ関数は、キーと値のメモリ配置方法を決めるものです。多くのプログラミング言語でもこの種のデータ構造はサポートされていますが、
しばしば名前が違います。hash、map、object、ハッシュテーブル、連想配列など、枚挙に暇はありません。
ハッシュマップは、ベクタのように番号ではなく、どんな型にもなりうるキーを使ってデータを参照したいときに有用です。 例えば、ゲームにおいて、各チームのスコアをハッシュマップで追いかけることができます。ここで、各キーはチーム名、 値が各チームのスコアになります。チーム名が与えられれば、スコアを扱うことができるわけです。
この節でハッシュマップの基礎的なAPIを見ていきますが、より多くのグッズが標準ライブラリにより、
HashMap<K, V>上に定義された関数に隠されています。いつものように、
もっと情報が欲しければ、標準ライブラリのドキュメントをチェックしてください。
新規ハッシュマップを生成する
空のハッシュマップをnewで作り、要素をinsertで追加することができます。リスト8-20では、
名前がブルーとイエローの2チームのスコアを追いかけています。ブルーチームは10点から、イエローチームは50点から始まります。
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); }
リスト8-20: ハッシュマップを生成してキーと値を挿入する
最初に標準ライブラリのコレクション部分からHashMapをuseする必要があることに注意してください。
今までの3つの一般的なコレクションの内、これが最も使用頻度が低いので、初期化処理で自動的にスコープに導入される機能には含まれていません。
また、標準ライブラリからのサポートもハッシュマップは少ないです; 例えば、生成するための組み込みマクロがありません。
ベクタと全く同様に、ハッシュマップはデータをヒープに保持します。このHashMapはキーがString型、
値はi32型です。ベクタのように、ハッシュマップは均質です: キーは全て同じ型でなければならず、
値も全て同じ型でなければなりません。
ハッシュマップを生成する別の方法は、タプルのベクタに対してcollectメソッドを使用するものです。
ここで、各タプルは、キーと値から構成されています。collectメソッドはいろんなコレクション型にデータをまとめ上げ、
そこにはHashMapも含まれています。例として、チーム名と初期スコアが別々のベクタに含まれていたら、
zipメソッドを使ってタプルのベクタを作り上げることができ、そこでは「ブルー」は10とペアになるなどします。
リスト8-21に示したように、それからcollectメソッドを使って、そのタプルのベクタをハッシュマップに変換することができるわけです。
#![allow(unused)] fn main() { use std::collections::HashMap; let teams = vec![String::from("Blue"), String::from("Yellow")]; let initial_scores = vec![10, 50]; let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect(); }
リスト8-21: チームのリストとスコアのリストからハッシュマップを作る
ここでは、HashMap<_, _>という型注釈が必要になります。なぜなら、いろんなデータ構造にまとめ上げることができ、
コンパイラは指定しない限り、どれを所望なのかわからないからです。ところが、キーと値の型引数については、
アンダースコアを使用しており、コンパイラはベクタのデータ型に基づいてハッシュマップが含む型を推論することができるのです。
ハッシュマップと所有権
i32のようなCopyトレイトを実装する型について、値はハッシュマップにコピーされます。
Stringのような所有権のある値なら、値はムーブされ、リスト8-22でデモされているように、
ハッシュマップはそれらの値の所有者になるでしょう。
#![allow(unused)] 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はこの時点で無効になる。試しに使ってみて // どんなコンパイルエラーが出るか確認してみて! }
リスト8-22: 一旦挿入されたら、キーと値はハッシュマップに所有されることを示す
insertを呼び出してfield_nameとfield_valueがハッシュマップにムーブされた後は、
これらの変数を使用することは叶いません。
値への参照をハッシュマップに挿入したら、値はハッシュマップにムーブされません。参照が指している値は、 最低でもハッシュマップが有効な間は、有効でなければなりません。これらの問題について詳細には、 第10章の「ライフタイムで参照を有効化する」節で語ります。
ハッシュマップの値にアクセスする
リスト8-23に示したように、キーをgetメソッドに提供することで、ハッシュマップから値を取り出すことができます。
#![allow(unused)] 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); }
リスト8-23: ハッシュマップに保持されたブルーチームのスコアにアクセスする
ここで、scoreはブルーチームに紐づけられた値になり、結果はSome(&10)となるでしょう。
結果はSomeに包まれます。というのも、getはOption<&V>を返すからです; キーに対応する値がハッシュマップになかったら、
getはNoneを返すでしょう。プログラムは、このOptionを第6章で講義した方法のどれかで扱う必要があるでしょう。
ベクタのように、forループでハッシュマップのキーと値のペアを走査することができます:
#![allow(unused)] 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
ハッシュマップを更新する
キーと値の数は伸長可能なものの、各キーには1回に1つの値しか紐づけることができません。 ハッシュマップ内のデータを変えたい時は、すでにキーに値が紐づいている場合の扱い方を決めなければなりません。 古い値を新しい値で置き換えて、古い値を完全に無視することもできます。古い値を保持して、 新しい値を無視し、キーにまだ値がない場合に新しい値を追加するだけにすることもできます。 あるいは、古い値と新しい値を組み合わせることもできます。各方法について見ていきましょう!
値を上書きする
キーと値をハッシュマップに挿入し、同じキーを異なる値で挿入したら、そのキーに紐づけられている値は置換されます。
リスト8-24のコードは、insertを二度呼んでいるものの、ハッシュマップには一つのキーと値の組しか含まれません。
なぜなら、ブルーチームキーに対する値を2回とも挿入しているからです。
#![allow(unused)] 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); }
リスト8-24: 特定のキーで保持された値を置き換える
このコードは、{"Blue": 25}と出力するでしょう。10という元の値は上書きされたのです。
キーに値がなかった時のみ値を挿入する
特定のキーに値があるか確認することは一般的であり、存在しない時に値を挿入することも一般的です。
ハッシュマップには、これを行うentryと呼ばれる特別なAPIがあり、これは、引数としてチェックしたいキーを取ります。
このentryメソッドの戻り値は、Entryと呼ばれるenumであり、これは存在したりしなかったりする可能性のある値を表します。
イエローチームに対するキーに値が紐づけられているか否か確認したくなったとしましょう。存在しなかったら、
50という値を挿入したく、ブルーチームに対しても同様です。entryAPIを使用すれば、コードはリスト8-25のようになります。
#![allow(unused)] 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-25: entryメソッドを使ってキーに値がない場合だけ挿入する
Entry上のor_insertメソッドは、対応するEntryキーが存在した時にそのキーに対する値への可変参照を返すために定義されており、
もしなかったら、引数をこのキーの新しい値として挿入し、新しい値への可変参照を返します。このテクニックの方が、
そのロジックを自分で書くよりもはるかに綺麗な上に、borrow checkerとも親和性が高くなります。
リスト8-25のコードを実行すると、{"Yellow": 50, "Blue": 10}と出力するでしょう。
最初のentry呼び出しは、まだイエローチームに対する値がないので、値50でイエローチームのキーを挿入します。
entryの2回目の呼び出しはハッシュマップを変更しません。なぜなら、ブルーチームには既に10という値があるからです。
古い値に基づいて値を更新する
ハッシュマップの別の一般的なユースケースは、キーの値を探し、古い値に基づいてそれを更新することです。 例えば、リスト8-26は、各単語があるテキストに何回出現するかを数え上げるコードを示しています。 キーに単語を入れたハッシュマップを使用し、その単語を何回見かけたか追いかけるために値を増やします。 ある単語を見かけたのが最初だったら、まず0という値を挿入します:
#![allow(unused)] 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); }
リスト8-26: 単語とカウントを保持するハッシュマップを使って単語の出現数をカウントする
このコードは、{"world": 2, "hello": 1, "wonderful": 1}と出力するでしょう。
or_insert関数は実際、このキーに対する値への可変参照(&mut V)を返すのです。
ここでその可変参照をcount変数に保持しているので、その値に代入するには、
まずアスタリスク(*)でcountを参照外ししなければならないのです。この可変参照は、
forループの終端でスコープを抜けるので、これらの変更は全て安全であり、借用規則により許可されるのです。
ハッシュ関数
標準では、HashMapはサービス拒否(DoS)アタックに対して抵抗を示す暗号学的に安全なハッシュ関数を使用します。
これは、利用可能な最速のハッシュアルゴリズムではありませんが、パフォーマンスの欠落と引き換えに安全性を得るというトレードオフは、
価値があります。自分のコードをプロファイリングして、自分の目的では標準のハッシュ関数は遅すぎると判明したら、
異なるhasherを指定することで別の関数に切り替えることができます。hasherとは、
BuildHasherトレイトを実装する型のことです。トレイトについてとその実装方法については、第10章で語ります。
必ずしも独自のhasherを1から作り上げる必要はありません; crates.ioには、
他のRustユーザによって共有された多くの一般的なハッシュアルゴリズムを実装したhasherを提供するライブラリがあります。
まとめ
ベクタ、文字列、ハッシュマップはデータを保持し、アクセスし、変更する必要のあるプログラムで必要になる、 多くの機能を提供してくれるでしょう。今なら解決可能なはずの練習問題を用意しました:
- 整数のリストが与えられ、ベクタを使ってmean(平均値)、median(ソートされた時に真ん中に来る値)、 mode(最も頻繁に出現する値; ハッシュマップがここでは有効活用できるでしょう)を返してください。
- 文字列をピッグ・ラテン(
訳注: 英語の言葉遊びの一つ)に変換してください。各単語の最初の子音は、 単語の終端に移り、"ay"が足されます。従って、"first"は"irst-fay"になります。ただし、 母音で始まる単語には、お尻に"hay"が付け足されます("apple"は"apple-hay"になります)。 UTF-8エンコードに関する詳細を心に留めておいてください! - ハッシュマップとベクタを使用して、ユーザに会社の部署に雇用者の名前を追加させられるテキストインターフェイスを作ってください。 例えば、"Add Sally to Engineering"(開発部門にサリーを追加)や"Add Amir to Sales"(販売部門にアミールを追加)などです。 それからユーザに、ある部署にいる人間の一覧や部署ごとにアルファベット順で並べ替えられた会社の全人間の一覧を扱わせてあげてください。
標準ライブラリのAPIドキュメントには、この練習問題に有用な、ベクタ、文字列、ハッシュマップのメソッドが解説されています。
処理が失敗することもあるような、より複雑なプログラムに入り込んできています; ということは、 エラーの処理法について議論するのにぴったりということです。次はそれをします!
エラー処理
Rustの信頼性への傾倒は、エラー処理にも及びます。ソフトウェアにおいて、エラーは生きている証しです。 従って、Rustには何かがおかしくなる場面を扱う機能がたくさんあります。多くの場面で、 コンパイラは、プログラマにエラーの可能性を知り、コードのコンパイルが通るまでに何かしら対応を行うことを要求してきます。 この要求により、エラーを発見し、コードを実用に供する前に適切に対処していることを確認することでプログラムを頑健なものにしてくれるのです!
Rustでは、エラーは大きく二つに分類されます: 回復可能と回復不能なエラーです。 ファイルが見つからないなどの回復可能なエラーには、問題をユーザに報告し、処理を再試行することが合理的になります。 回復不能なエラーは、常にバグの兆候です。例えば、配列の境界を超えた箇所にアクセスしようとすることなどです。
多くの言語では、この2種のエラーを区別することはなく、例外などの機構を使用して同様に扱います。
Rustには例外が存在しません。代わりに、回復可能なエラーにはResult<T, E>値があり、
プログラムが回復不能なエラーに遭遇した時には、実行を中止するpanic!マクロがあります。
この章では、まずpanic!の呼び出しを講義し、それからResult<T, E>を戻り値にする話をします。
加えて、エラーからの回復を試みるか、実行を中止するか決定する際に考慮すべき事項についても、探究しましょう。
panic!で回復不能なエラー
時として、コードで悪いことが起きるものです。そして、それに対してできることは何もありません。
このような場面で、Rustにはpanic!マクロが用意されています。panic!マクロが実行されると、
プログラムは失敗のメッセージを表示し、スタックを巻き戻し掃除して、終了します。これが最もありふれて起こるのは、
何らかのバグが検出された時であり、プログラマには、どうエラーを処理すればいいか明確ではありません。
パニックに対してスタックを巻き戻すか異常終了するか
標準では、パニックが発生すると、プログラムは巻き戻しを始めます。つまり、言語がスタックを遡り、 遭遇した各関数のデータを片付けるということです。しかし、この遡りと片付けはすべきことが多くなります。 対立案は、即座に異常終了し、片付けをせずにプログラムを終了させることです。そうなると、プログラムが使用していたメモリは、 OSが片付ける必要があります。プロジェクトにおいて、実行可能ファイルを極力小さくする必要があれば、 Cargo.tomlファイルの適切な
[profile]欄にpanic = 'abort'を追記することで、 パニック時に巻き戻しから異常終了するように切り替えることができます。例として、 リリースモード時に異常終了するようにしたければ、以下を追記してください:[profile.release] panic = 'abort'
単純なプログラムでpanic!の呼び出しを試してみましょう:
ファイル名: src/main.rs
fn main() { panic!("crash and burn"); //クラッシュして炎上 }
このプログラムを実行すると、以下のような出力を目の当たりにするでしょう:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.25 secs
Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2:4
('main'スレッドはsrc/main.rs:2:4の「クラッシュして炎上」でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.
panic!の呼び出しが、最後の2行に含まれるエラーメッセージを発生させているのです。
1行目にパニックメッセージとソースコード中でパニックが発生した箇所を示唆しています:
src/main.rs:2:4は、src/main.rsファイルの2行目4文字目であることを示しています。
この場合、示唆される行は、自分のコードの一部で、その箇所を見に行けば、panic!マクロ呼び出しがあるわけです。
それ以外では、panic!呼び出しが、自分のコードが呼び出しているコードの一部になっている可能性もあるわけです。
エラーメッセージで報告されるファイル名と行番号が、結果的にpanic!呼び出しに導いた自分のコードの行ではなく、
panic!マクロが呼び出されている他人のコードになるでしょう。panic!呼び出しの発生元である関数のバックトレースを使用して、
問題を起こしている自分のコードの箇所を割り出すことができます。バックトレースがどんなものか、次に議論しましょう。
panic!バックトレースを使用する
別の例を眺めて、自分のコードでマクロを直接呼び出す代わりに、コードに存在するバグにより、
ライブラリでpanic!呼び出しが発生するとどんな感じなのか確かめてみましょう。リスト9-1は、
添え字でベクタの要素にアクセスを試みる何らかのコードです。
ファイル名: src/main.rs
fn main() { let v = vec![1, 2, 3]; v[99]; }
リスト9-1: ベクタの境界を超えて要素へのアクセスを試み、panic!の呼び出しを発生させる
ここでは、ベクタの100番目の要素(添え字は0始まりなので添え字99)にアクセスを試みていますが、ベクタには3つしか要素がありません。
この場面では、Rustはパニックします。[]の使用は、要素を返すと想定されるものの、
無効な添え字を渡せば、ここでRustが返せて正しいと思われる要素は何もないわけです。
他の言語(Cなど)では、この場面で欲しいものではないにもかかわらず、まさしく要求したものを返そうとしてきます:
メモリがベクタに属していないにもかかわらず、ベクタ内のその要素に対応するメモリ上の箇所にあるものを何か返してくるのです。
これは、バッファー外読み出し(buffer overread; 訳注: バッファー読みすぎとも解釈できるか)と呼ばれ、
攻撃者が、配列の後に格納された読めるべきでないデータを読み出せるように添え字を操作できたら、
セキュリティ脆弱性につながる可能性があります。
この種の脆弱性からプログラムを保護するために、存在しない添え字の要素を読もうとしたら、 Rustは実行を中止し、継続を拒みます。試して確認してみましょう:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.27 secs
Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is
99', /checkout/src/liballoc/vec.rs:1555:10
('main'スレッドは、/checkout/src/liballoc/vec.rs:1555:10の
「境界外番号: 長さは3なのに、添え字は99です」でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.
このエラーは、自分のファイルではないvec.rsファイルを指しています。
標準ライブラリのVec<T>の実装です。ベクタvに対して[]を使った時に走るコードは、
vec.rsに存在し、ここで実際にpanic!が発生しているのです。
その次の注釈行は、RUST_BACKTRACE環境変数をセットして、まさしく何が起き、
エラーが発生したのかのバックトレースを得られることを教えてくれています。
バックトレースとは、ここに至るまでに呼び出された全関数の一覧です。Rustのバックトレースも、
他の言語同様に動作します: バックトレースを読むコツは、頭からスタートして自分のファイルを見つけるまで読むことです。
そこが、問題の根源になるのです。自分のファイルを言及している箇所以前は、自分のコードで呼び出したコードになります;
以後は、自分のコードを呼び出しているコードになります。これらの行には、Rustの核となるコード、標準ライブラリのコード、
使用しているクレートなどが含まれるかもしれません。RUST_BACKTRACE環境変数を0以外の値にセットして、
バックトレースを出力してみましょう。リスト9-2のような出力が得られるでしょう。
$ RUST_BACKTRACE=1 cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', /checkout/src/liballoc/vec.rs:1555:10
stack backtrace:
0: std::sys::imp::backtrace::tracing::imp::unwind_backtrace
at /checkout/src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
1: std::sys_common::backtrace::_print
at /checkout/src/libstd/sys_common/backtrace.rs:71
2: std::panicking::default_hook::{{closure}}
at /checkout/src/libstd/sys_common/backtrace.rs:60
at /checkout/src/libstd/panicking.rs:381
3: std::panicking::default_hook
at /checkout/src/libstd/panicking.rs:397
4: std::panicking::rust_panic_with_hook
at /checkout/src/libstd/panicking.rs:611
5: std::panicking::begin_panic
at /checkout/src/libstd/panicking.rs:572
6: std::panicking::begin_panic_fmt
at /checkout/src/libstd/panicking.rs:522
7: rust_begin_unwind
at /checkout/src/libstd/panicking.rs:498
8: core::panicking::panic_fmt
at /checkout/src/libcore/panicking.rs:71
9: core::panicking::panic_bounds_check
at /checkout/src/libcore/panicking.rs:58
10: <alloc::vec::Vec<T> as core::ops::index::Index<usize>>::index
at /checkout/src/liballoc/vec.rs:1555
11: panic::main
at src/main.rs:4
12: __rust_maybe_catch_panic
at /checkout/src/libpanic_unwind/lib.rs:99
13: std::rt::lang_start
at /checkout/src/libstd/panicking.rs:459
at /checkout/src/libstd/panic.rs:361
at /checkout/src/libstd/rt.rs:61
14: main
15: __libc_start_main
16: <unknown>
リスト9-2: RUST_BACKTRACE環境変数をセットした時に表示される、
panic!呼び出しが生成するバックトレース
出力が多いですね!OSやRustのバージョンによって、出力の詳細は変わる可能性があります。この情報とともに、
バックトレースを得るには、デバッグシンボルを有効にしなければなりません。デバッグシンボルは、
--releaseオプションなしでcargo buildやcargo runを使用していれば、標準で有効になり、
ここではそうなっています。
リスト9-2の出力で、バックトレースの11行目が問題発生箇所を指し示しています: src/main.rsの4行目です。 プログラムにパニックしてほしくなければ、自分のファイルについて言及している最初の行で示されている箇所が、 どのようにパニックを引き起こす値でこの箇所にたどり着いたか割り出すために調査を開始すべき箇所になります。 バックトレースの使用法を模擬するためにわざとパニックするコードを書いたリスト9-1において、 パニックを解消する方法は、3つしか要素のないベクタの添え字99の要素を要求しないことです。 将来コードがパニックしたら、パニックを引き起こすどんな値でコードがどんな動作をしているのかと、 代わりにコードは何をすべきなのかを算出する必要があるでしょう。
この章の後ほど、「panic!するかpanic!するまいか」節でpanic!とエラー状態を扱うのにpanic!を使うべき時と使わぬべき時に戻ってきます。
次は、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では、
ファイルを開こうとしています。
ファイル名: src/main.rs
use std::fs::File; fn main() { let f = File::open("hello.txt"); }
リスト9-3: ファイルを開く
File::openがResultを返すとどう知るのでしょうか?標準ライブラリのAPIドキュメントを参照することもできますし、
コンパイラに尋ねることもできます!fに関数の戻り値ではないと判明している型注釈を与えて、
コードのコンパイルを試みれば、コンパイラは型が合わないと教えてくれるでしょう。そして、エラーメッセージは、
fの実際の型を教えてくれるでしょう。試してみましょう!File::openの戻り値の型はu32ではないと判明しているので、
let f文を以下のように変更しましょう:
let f: u32 = File::open("hello.txt");
これでコンパイルしようとすると、以下のような出力が得られます:
error[E0308]: mismatched types
(エラー: 型が合いません)
--> src/main.rs:4:18
|
4 | let f: u32 = File::open("hello.txt");
| ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
`std::result::Result`
|
= note: expected type `u32`
(注釈: 予期した型は`u32`です)
found type `std::result::Result<std::fs::File, std::io::Error>`
(実際の型は`std::result::Result<std::fs::File, std::io::Error>`です)
これにより、File::open関数の戻り値の型は、Result<T, E>であることがわかります。ジェネリック引数のTは、
ここでは成功値の型std::fs::Fileで埋められていて、これはファイルハンドルです。
エラー値で使用されているEの型は、std::io::Errorです。
この戻り値型は、File::openの呼び出しが成功し、読み込みと書き込みを行えるファイルハンドルを返す可能性があることを意味します。
また、関数呼び出しは失敗もする可能性があります: 例えば、ファイルが存在しない可能性、ファイルへのアクセス権限がない可能性です。
File::openには成功したか失敗したかを知らせる方法とファイルハンドルまたは、エラー情報を与える方法が必要なのです。
この情報こそがResult enumが伝達するものなのです。
File::openが成功した場合、変数fの値はファイルハンドルを含むOkインスタンスになります。
失敗した場合には、発生したエラーの種類に関する情報をより多く含むErrインスタンスがfの値になります。
リスト9-3のコードに追記をしてFile::openが返す値に応じて異なる動作をする必要があります。
リスト9-4に基礎的な道具を使ってResultを扱う方法を一つ示しています。第6章で議論したmatch式です。
ファイル名: src/main.rs
use std::fs::File; fn main() { let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(error) => { // ファイルを開く際に問題がありました panic!("There was a problem opening the file: {:?}", error) }, }; }
リスト9-4: match式を使用して返却される可能性のあるResult列挙子を処理する
Option enumのように、Result enumとその列挙子は、初期化処理でインポートされているので、
matchアーム内でOkとErr列挙子の前にResult::を指定する必要がないことに注目してください。
ここでは、結果がOkの時に、Ok列挙子から中身のfile値を返すように指示し、
それからそのファイルハンドル値を変数fに代入しています。matchの後には、
ファイルハンドルを使用して読み込んだり書き込むことができるわけです。
matchのもう一つのアームは、File::openからErr値が得られたケースを処理しています。
この例では、panic!マクロを呼び出すことを選択しています。カレントディレクトリにhello.txtというファイルがなく、
このコードを走らせたら、panic!マクロからの以下のような出力を目の当たりにするでしょう:
thread 'main' panicked at 'There was a problem opening the file: Error { repr:
Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12
('main'スレッドは、src/main.rs:9:12の「ファイルを開く際に問題がありました: Error{ repr:
Os { code: 2, message: "そのような名前のファイルまたはディレクトリはありません"}}」でパニックしました)
通常通り、この出力は、一体何がおかしくなったのかを物語っています。
色々なエラーにマッチする
リスト9-4のコードは、File::openが失敗した理由にかかわらずpanic!します。代わりにしたいことは、
失敗理由によって動作を変えることです: ファイルが存在しないためにFile::openが失敗したら、
ファイルを作成し、その新しいファイルへのハンドルを返したいです。他の理由(例えばファイルを開く権限がなかったなど)で、
File::openが失敗したら、リスト9-4のようにコードにはpanic!してほしいのです。
リスト9-5を眺めてください。ここではmatchに別のアームを追加しています。
ファイル名: src/main.rs
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(ref error) if error.kind() == ErrorKind::NotFound => {
match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => {
panic!(
//ファイルを作成しようとしましたが、問題がありました
"Tried to create file but there was a problem: {:?}",
e
)
},
}
},
Err(error) => {
panic!(
"There was a problem opening the file: {:?}",
error
)
},
};
}
リスト9-5: 色々な種類のエラーを異なる方法で扱う
File::openがErr列挙子に含めて返す値の型は、io::Errorであり、これは標準ライブラリで提供されている構造体です。
この構造体には、呼び出すとio::ErrorKind値が得られるkindメソッドがあります。io::ErrorKindというenumは、
標準ライブラリで提供されていて、io処理の結果発生する可能性のある色々な種類のエラーを表す列挙子があります。
使用したい列挙子は、ErrorKind::NotFoundで、これは開こうとしているファイルがまだ存在しないことを示唆します。
if error.kind() == ErrorKind::Notfoundという条件式は、マッチガードと呼ばれます:
アームのパターンをさらに洗練するmatchアーム上のおまけの条件式です。この条件式は、
そのアームのコードが実行されるには真でなければいけないのです; そうでなければ、
パターンマッチングは継続し、matchの次のアームを考慮します。パターンのrefは、
errorがガード条件式にムーブされないように必要ですが、ただ単にガード式に参照されます。
refを使用して&の代わりにパターン内で参照を作っている理由は、第18章で詳しく講義します。
手短に言えば、パターンの文脈において、&は参照にマッチし、その値を返しますが、
refは値にマッチし、それへの参照を返すということなのです。
マッチガードで精査したい条件は、error.kind()により返る値が、ErrorKind enumのNotFound列挙子であるかということです。
もしそうなら、File::createでファイル作成を試みます。ところが、File::createも失敗する可能性があるので、
内部にもmatch式を追加する必要があるのです。ファイルが開けないなら、異なるエラーメッセージが出力されるでしょう。
外側のmatchの最後のアームは同じままなので、ファイルが存在しないエラー以外ならプログラムはパニックします。
エラー時にパニックするショートカット: unwrapとexpect
matchの使用は、十分に仕事をしてくれますが、いささか冗長になり得る上、必ずしも意図をよく伝えるとは限りません。
Result<T, E>型には、色々な作業をするヘルパーメソッドが多く定義されています。それらの関数の一つは、
unwrapと呼ばれますが、リスト9-4で書いたmatch式と同じように実装された短絡メソッドです。
Result値がOk列挙子なら、unwrapはOkの中身を返します。ResultがErr列挙子なら、
unwrapはpanic!マクロを呼んでくれます。こちらが実際に動作しているunwrapの例です:
ファイル名: src/main.rs
use std::fs::File; fn main() { let f = File::open("hello.txt").unwrap(); }
このコードをhello.txtファイルなしで走らせたら、unwrapメソッドが行うpanic!呼び出しからのエラーメッセージを目の当たりにするでしょう:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
repr: Os { code: 2, message: "No such file or directory" } }',
src/libcore/result.rs:906:4
('main'スレッドは、src/libcore/result.rs:906:4の
「`Err`値に対して`Result::unwrap()`が呼び出されました: Error{
repr: Os { code: 2, message: "そのようなファイルまたはディレクトリはありません" } }」でパニックしました)
別のメソッドexpectは、unwrapに似ていますが、panic!のエラーメッセージも選択させてくれます。
unwrapの代わりにexpectを使用して、いいエラーメッセージを提供すると、意図を伝え、
パニックの原因をたどりやすくしてくれます。expectの表記はこんな感じです:
ファイル名: src/main.rs
use std::fs::File; fn main() { // hello.txtを開くのに失敗しました let f = File::open("hello.txt").expect("Failed to open hello.txt"); }
expectをunwrapと同じように使用してます: ファイルハンドルを返したり、panic!マクロを呼び出しています。
expectがpanic!呼び出しで使用するエラーメッセージは、unwrapが使用するデフォルトのpanic!メッセージではなく、
expectに渡した引数になります。以下のようになります:
thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
2, message: "No such file or directory" } }', src/libcore/result.rs:906:4
このエラーメッセージは、指定したテキストのhello.txtを開くのに失敗しましたで始まっているので、
コード内のどこでエラーメッセージが出力されたのかより見つけやすくなるでしょう。複数箇所でunwrapを使用していたら、
ズバリどのunwrapがパニックを引き起こしているのか理解するのは、より時間がかかる可能性があります。
パニックするunwrap呼び出しは全て、同じメッセージを出力するからです。
エラーを委譲する
失敗する可能性のある何かを呼び出す実装をした関数を書く際、関数内でエラーを処理する代わりに、 呼び出し元がどうするかを決められるようにエラーを返すことができます。これはエラーの委譲として認知され、 自分のコードの文脈で利用可能なものよりも、 エラーの処理法を規定する情報やロジックがより多くある呼び出し元のコードに制御を明け渡します。
例えば、リスト9-6の関数は、ファイルからユーザ名を読み取ります。ファイルが存在しなかったり、読み込みできなければ、 この関数はそのようなエラーを呼び出し元のコードに返します。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::io; use std::io::Read; use std::fs::File; fn read_username_from_file() -> Result<String, io::Error> { let f = File::open("hello.txt"); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(e) => Err(e), } } }
リスト9-6: matchでエラーを呼び出し元のコードに返す関数
まずは、関数の戻り値型に注目してください: Result<String, io::Error>です。つまり、この関数は、
Result<T, E>型の値を返しているということです。ここでジェネリック引数のTは、具体型Stringで埋められ、
ジェネリック引数のEは具体型io::Errorで埋められています。この関数が何の問題もなく成功すれば、
この関数を呼び出したコードは、String(関数がファイルから読み取ったユーザ名)を保持するOk値を受け取ります。
この関数が何か問題に行き当たったら、呼び出し元のコードはio::Errorのインスタンスを保持するErr値を受け取り、
このio::Errorは問題の内容に関する情報をより多く含んでいます。関数の戻り値の型にio::Errorを選んだのは、
この関数本体で呼び出している失敗する可能性のある処理が両方とも偶然この型をエラー値として返すからです:
File::open関数とread_to_stringメソッドです。
関数の本体は、File::open関数を呼び出すところから始まります。そして、リスト9-4のmatchに似たmatchで返ってくるResult値を扱い、
Errケースにpanic!を呼び出すだけの代わりに、この関数から早期リターンしてこの関数のエラー値として、
File::openから得たエラー値を呼び出し元に渡し戻します。File::openが成功すれば、
ファイルハンドルを変数fに保管して継続します。
さらに、変数sに新規Stringを生成し、fのファイルハンドルに対してread_to_stringを呼び出して、
ファイルの中身をsに読み出します。File::openが成功しても、失敗する可能性があるので、read_to_stringメソッドも、
Resultを返却します。そのResultを処理するために別のmatchが必要になります: read_to_stringが成功したら、
関数は成功し、今はOkに包まれたsに入っているファイルのユーザ名を返却します。read_to_stringが失敗したら、
File::openの戻り値を扱ったmatchでエラー値を返したように、エラー値を返します。
しかし、明示的にreturnを述べる必要はありません。これが関数の最後の式だからです。
そうしたら、呼び出し元のコードは、ユーザ名を含むOk値か、io::Errorを含むErr値を得て扱います。
呼び出し元のコードがそれらの値をどうするかはわかりません。呼び出しコードがErr値を得たら、
例えば、panic!を呼び出してプログラムをクラッシュさせたり、デフォルトのユーザ名を使ったり、
ファイル以外の場所からユーザ名を検索したりできるでしょう。呼び出し元のコードが実際に何をしようとするかについて、
十分な情報がないので、成功や失敗情報を全て委譲して適切に扱えるようにするのです。
Rustにおいて、この種のエラー委譲は非常に一般的なので、Rustにはこれをしやすくする?演算子が用意されています。
エラー委譲のショートカット: ?演算子
リスト9-7もリスト9-6と同じ機能を有するread_username_from_fileの実装ですが、
こちらは?演算子を使用しています:
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::io; use std::io::Read; use std::fs::File; fn read_username_from_file() -> Result<String, io::Error> { let mut f = File::open("hello.txt")?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) } }
リスト9-7: ?演算子でエラーを呼び出し元に返す関数
Result値の直後に置かれた?は、リスト9-6でResult値を処理するために定義したmatch式とほぼ同じように動作します。
Resultの値がOkなら、Okの中身がこの式から返ってきて、プログラムは継続します。値がErrなら、
returnキーワードを使ったかのように関数全体からErrの中身が返ってくるので、
エラー値は呼び出し元のコードに委譲されます。
リスト9-6のmatch式と?演算子には違いがあります: ?を使ったエラー値は、
標準ライブラリのFromトレイトで定義され、エラーの型を別のものに変換するfrom関数を通ることです。
?演算子がfrom関数を呼び出すと、受け取ったエラー型が現在の関数の戻り値型で定義されているエラー型に変換されます。これは、
個々がいろんな理由で失敗する可能性があるのにも関わらず、関数が失敗する可能性を全て一つのエラー型で表現して返す時に有用です。
各エラー型がfrom関数を実装して返り値のエラー型への変換を定義している限り、
?演算子が変換の面倒を自動的に見てくれます。
リスト9-7の文脈では、File::open呼び出し末尾の?はOkの中身を変数fに返します。
エラーが発生したら、?演算子により関数全体から早期リターンし、あらゆるErr値を呼び出し元に与えます。
同じ法則がread_to_string呼び出し末尾の?にも適用されます。
?演算子により定型コードの多くが排除され、この関数の実装を単純にしてくれます。
リスト9-8で示したように、?の直後のメソッド呼び出しを連結することでさらにこのコードを短くすることさえもできます。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::io; use std::io::Read; use std::fs::File; fn read_username_from_file() -> Result<String, io::Error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s) } }
リスト9-8: ?演算子の後のメソッド呼び出しを連結する
sの新規Stringの生成を関数の冒頭に移動しました; その部分は変化していません。変数fを生成する代わりに、
read_to_stringの呼び出しを直接File::open("hello.txt")?の結果に連結させました。
それでも、read_to_string呼び出しの末尾には?があり、File::openとread_to_string両方が成功したら、
エラーを返すというよりもそれでも、sにユーザ名を含むOk値を返します。機能もまたリスト9-6及び、9-7と同じです;
ただ単に異なるバージョンのよりエルゴノミックな書き方なのです。
?演算子は、Resultを返す関数でしか使用できない
?演算子は戻り値にResultを持つ関数でしか使用できません。というのも、リスト9-6で定義したmatch式と同様に動作するよう、
定義されているからです。Resultの戻り値型を要求するmatchの部品は、return Err(e)なので、
関数の戻り値はこのreturnと互換性を保つためにResultでなければならないのです。
main関数で?演算子を使用したらどうなるか見てみましょう。main関数は、戻り値が()でしたね:
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?;
}
このコードをコンパイルすると、以下のようなエラーメッセージが得られます:
error[E0277]: the trait bound `(): std::ops::Try` is not satisfied
(エラー: `(): std::ops::Try`というトレイト境界が満たされていません)
--> src/main.rs:4:13
|
4 | let f = File::open("hello.txt")?;
| ------------------------
| |
| the `?` operator can only be used in a function that returns
`Result` (or another type that implements `std::ops::Try`)
| in this macro invocation
| (このマクロ呼び出しの`Result`(かまたは`std::ops::Try`を実装する他の型)を返す関数でしか`?`演算子は使用できません)
|
= help: the trait `std::ops::Try` is not implemented for `()`
(助言: `std::ops::Try`トレイトは`()`には実装されていません)
= note: required by `std::ops::Try::from_error`
(注釈: `std::ops::Try::from_error`で要求されています)
このエラーは、?演算子はResultを返す関数でしか使用が許可されないと指摘しています。
Resultを返さない関数では、Resultを返す別の関数を呼び出した時、
?演算子を使用してエラーを呼び出し元に委譲する可能性を生み出す代わりに、matchかResultのメソッドのどれかを使う必要があるでしょう。
さて、panic!呼び出しやResultを返す詳細について議論し終えたので、
どんな場合にどちらを使うのが適切か決める方法についての話に戻りましょう。
panic!すべきかするまいか
では、panic!すべき時とResultを返すべき時はどう決定すればいいのでしょうか?コードがパニックしたら、
回復する手段はありません。回復する可能性のある手段の有る無しに関わらず、どんなエラー場面でもpanic!を呼ぶことはできますが、
そうすると、呼び出す側のコードの立場に立ってこの場面は回復不能だという決定を下すことになります。
Result値を返す決定をすると、決断を下すのではなく、呼び出し側に選択肢を与えることになります。
呼び出し側は、場面に合わせて回復を試みることを決定したり、この場合のErr値は回復不能と断定して、
panic!を呼び出し、回復可能だったエラーを回復不能に変換することもできます。故に、Resultを返却することは、
失敗する可能性のある関数を定義する際には、いい第一選択肢になります。
稀な場面では、Resultを返すよりもパニックするコードを書く方がより適切になることもあります。
例やプロトタイプコード、テストでパニックするのが適切な理由を探ってみましょう。
それからコンパイラではありえない失敗だと気づけなくとも、人間なら気づける場面を議論しましょう。
そして、ライブラリコードでパニックするか決定する方法についての一般的なガイドラインで結論づけましょう。
例、プロトタイプコード、テスト
例を記述して何らかの概念を具体化している時、頑健なエラー処理コードも例に含むことは、例の明瞭さを欠くことになりかねません。
例において、unwrapなどのパニックする可能性のあるメソッド呼び出しは、
アプリケーションにエラーを処理してほしい方法へのプレースホルダーを意味していると理解され、
これは残りのコードがしていることによって異なる可能性があります。
同様に、unwrapやexpectメソッドは、エラーの処理法を決定する準備ができる前、プロトタイプの段階では、
非常に便利です。それらにより、コードにプログラムをより頑健にする時の明らかなマーカーが残されるわけです。
メソッド呼び出しがテスト内で失敗したら、そのメソッドがテスト下に置かれた機能ではなかったとしても、
テスト全体が失敗してほしいでしょう。panic!が、テストが失敗と印づけられる手段なので、
unwrapやexpectの呼び出しはズバリ起こるべきことです。
コンパイラよりもプログラマがより情報を持っている場合
ResultがOk値であると確認する何らかの別のロジックがある場合、unwrapを呼び出すことは適切でしょうが、
コンパイラは、そのロジックを理解はしません。それでも、処理する必要のあるResultは存在するでしょう:
呼び出している処理が何であれ、自分の特定の場面では論理的に起こり得なくても、一般的にまだ失敗する可能性はあるわけです。
手動でコードを調査してErr列挙子は存在しないと確認できたら、unwrapを呼び出すことは完全に受容できることです。
こちらが例です:
#![allow(unused)] fn main() { use std::net::IpAddr; let home: IpAddr = "127.0.0.1".parse().unwrap(); }
ハードコードされた文字列を構文解析することでIpAddrインスタンスを生成しています。
プログラマには127.0.0.1が合法なIPアドレスであることがわかるので、ここでunwrapを使用することは、
受容可能なことです。しかしながら、ハードコードされた合法な文字列が存在することは、
parseメソッドの戻り値型を変えることにはなりません: それでも得られるのは、Result値であり、
コンパイラはまだErr列挙子になる可能性があるかのようにResultを処理することを強制してきます。
コンパイラは、この文字列が常に合法なIPアドレスであると把握できるほど利口ではないからです。
プログラムにハードコードされるのではなく、IPアドレス文字列がユーザ起源でそれ故に確かに失敗する可能性がある場合、
Resultをもっと頑健な方法で処理したほうが絶対にいいでしょう。
エラー処理のガイドライン
コードが悪い状態に陥る可能性があるときにパニックさせるのは、推奨されることです。この文脈において、 悪い状態とは、何らかの前提、保証、契約、不変性が破られたことを言い、例を挙げれば、無効な値、 矛盾する値、行方不明な値がコードに渡されることと、さらに以下のいずれか一つ以上の状態であります:
- 悪い状態がときに起こるとは予想されないとき。
- この時点以降、この悪い状態にないことを頼りにコードが書かれているとき。
- 使用している型にこの情報をコード化するいい手段がないとき。
誰かが自分のコードを呼び出して筋の通らない値を渡してきたら、最善の選択肢はpanic!し、
開発段階で修正できるように自分たちのコードにバグがあることをライブラリ使用者に通知することかもしれません。
同様に自分の制御下にない外部コードを呼び出し、修正しようのない無効な状態を返すときにpanic!はしばしば適切です。
しかし、どんなにコードをうまく書いても起こると予想されますが、悪い状態に達したとき、それでもpanic!呼び出しをするよりも、
Resultを返すほうがより適切です。例には、不正なデータを渡されたパーサとか、
訪問制限に引っかかったことを示唆するステータスを返すHTTPリクエストなどが挙げられます。
このような場合には、呼び出し側が問題の処理方法を決定できるようにResultを返してこの悪い状態を委譲して、
失敗が予想される可能性であることを示唆するべきです。panic!を呼び出すことは、
これらのケースでは最善策ではないでしょう。
コードが値に対して処理を行う場合、コードはまず値が合法であることを確認し、
値が合法でなければパニックするべきです。これはほぼ安全性上の理由によるものです: 不正なデータの処理を試みると、
コードを脆弱性に晒す可能性があります。これが、境界外へのメモリアクセスを試みたときに標準ライブラリがpanic!を呼び出す主な理由です:
現在のデータ構造に属しないメモリにアクセスを試みることは、ありふれたセキュリティ問題なのです。
関数にはしばしば契約が伴います: 入力が特定の条件を満たすときのみ、振る舞いが保証されるのです。
契約が侵されたときにパニックすることは、道理が通っています。なぜなら、契約侵害は常に呼び出し側のバグを示唆し、
呼び出し側に明示的に処理してもらう必要のある種類のエラーではないからです。実際に、
呼び出し側が回復する合理的な手段はありません; 呼び出し側のプログラマがコードを修正する必要があるのです。
関数の契約は、特に侵害がパニックを引き起こす際には、関数のAPIドキュメント内で説明されているべきです。
ですが、全ての関数でたくさんのエラーチェックを行うことは冗長で煩わしいことでしょう。幸運にも、
Rustの型システム(故にコンパイラが行う型精査)を使用して多くの検査を行ってもらうことができます。
関数の引数に特定の型があるなら、合法な値があるとコンパイラがすでに確認していることを把握して、
コードのロジックに進むことができます。例えば、Option以外の型がある場合、プログラムは、
何もないではなく何かあると想定します。そうしたらコードは、
SomeとNone列挙子の2つの場合を処理する必要がなくなるわけです:
確実に値があるという可能性しかありません。関数に何もないことを渡そうとしてくるコードは、
コンパイルが通りもしませんので、その場合を実行時に検査する必要はないわけです。
別の例は、u32のような符号なし整数を使うことであり、この場合、引数は負には絶対にならないことが確認されます。
検証のために独自の型を作る
Rustの型システムを使用して合法な値があると確認するというアイディアを一歩先に進め、 検証のために独自の型を作ることに目を向けましょう。第2章の数当てゲームで、 コードがユーザに1から100までの数字を推測するよう求めたことを思い出してください。 秘密の数字と照合する前にユーザの推測がそれらの値の範囲にあることを全く確認しませんでした; 推測が正であることしか確認しませんでした。この場合、結果はそれほど悲惨なものではありませんでした: 「大きすぎ」、「小さすぎ」という出力は、それでも正しかったでしょう。ユーザを合法な推測に導き、 ユーザが範囲外の数字を推測したり、例えばユーザが文字を代わりに入力したりしたときに別の挙動をするようにしたら、 有益な改善になるでしょう。
これをする一つの方法は、ただのu32の代わりにi32として推測をパースし、負の数になる可能性を許可し、
それから数字が範囲に収まっているというチェックを追加することでしょう。そう、以下のように:
loop {
// --snip--
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--
}
このif式が、値が範囲外かどうかをチェックし、ユーザに問題を告知し、continueを呼び出してループの次の繰り返しを始め、
別の推測を求めます。if式の後、guessは1から100の範囲にあると把握して、guessと秘密の数字の比較に進むことができます。
ところが、これは理想的な解決策ではありません: プログラムが1から100の範囲の値しか処理しないことが間違いなく、 肝要であり、この要求がある関数の数が多ければ、このようなチェックを全関数で行うことは、 面倒でパフォーマンスにも影響を及ぼす可能性があるでしょう。
代わりに、新しい型を作って検証を関数内に閉じ込め、検証を全箇所で繰り返すのではなく、
その型のインスタンスを生成することができます。そうすれば、関数がその新しい型をシグニチャに用い、
受け取った値を自信を持って使用することは安全になります。リスト9-9に、new関数が1から100までの値を受け取った時のみ、
Guessのインスタンスを生成するGuess型を定義する一つの方法を示しました。
#![allow(unused)] fn main() { pub struct Guess { value: u32, } impl Guess { pub fn new(value: u32) -> Guess { if value < 1 || value > 100 { // 予想の値は1から100の範囲でなければなりませんが、{}でした panic!("Guess value must be between 1 and 100, got {}.", value); } Guess { value } } pub fn value(&self) -> u32 { self.value } } }
リスト9-9: 値が1から100の場合のみ処理を継続するGuess型
まず、u32型のvalueをフィールドに持つGuessという名前の構造体を定義しています。
ここに数値が保管されます。
それからGuessにGuess値のインスタンスを生成するnewという名前の関連関数を実装しています。
new関数は、u32型のvalueという引数を取り、Guessを返すように定義されています。
new関数の本体のコードは、valueをふるいにかけ、1から100の範囲であることを確かめます。
valueがふるいに引っかかったら、panic!呼び出しを行います。これにより、呼び出しコードを書いているプログラマに、
修正すべきバグがあると警告します。というのも、この範囲外のvalueでGuessを生成することは、
Guess::newが頼りにしている契約を侵害するからです。Guess::newがパニックするかもしれない条件は、
公開されているAPIドキュメントで議論されるべきでしょう; あなたが作成するAPIドキュメントでpanic!の可能性を示唆する、
ドキュメントの規約は、第14章で講義します。valueが確かにふるいを通ったら、
valueフィールドがvalue引数にセットされた新しいGuessを作成して返します。
次に、selfを借用し、他に引数はなく、u32を返すvalueというメソッドを実装します。
この類のメソッドは時にゲッターと呼ばれます。目的がフィールドから何らかのデータを得て返すことだからです。
この公開メソッドは、Guess構造体のvalueフィールドが非公開なので、必要になります。
valueフィールドが非公開なことは重要であり、そのためにGuess構造体を使用するコードは、
直接valueをセットすることが叶わないのです: モジュール外のコードは、
Guess::new関数を使用してGuessのインスタンスを生成しなければならず、
それにより、Guess::new関数の条件式でチェックされていないvalueがGuessに存在する手段はないことが保証されるわけです。
そうしたら、引数を一つ持つか、1から100の範囲の数値のみを返す関数は、シグニチャでu32ではなく、
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つの関数からジェネリックな関数を生成します。また、 ジェネリックな型を構造体やenum定義で使用する方法も説明します。
それから、トレイトを使用して、ジェネリックな方法で振る舞いを定義する方法を学びます。 ジェネリックな型にトレイトを組み合わせることで、ジェネリックな型を、単にあらゆる型に対してではなく、特定の振る舞いのある型のみに制限できます。
最後に、ライフタイムを議論します。ライフタイムとは、コンパイラに参照がお互いにどう関係しているかの情報を与える一種のジェネリクスです。 ライフタイムのおかげでコンパイラに参照が有効であることを確認してもらうことを可能にしつつ、多くの場面で値を借用できます。
関数を抽出することで重複を取り除く
ジェネリクスの記法に飛び込む前にまずは、関数を抽出することでジェネリックな型が関わらない重複を取り除く方法を見ましょう。 そして、このテクニックを適用してジェネリックな関数を抽出するのです!重複したコードを認識して関数に抽出できるのと同じように、 ジェネリクスを使用できる重複コードも認識し始めるでしょう。
リスト10-1に示したように、リスト内の最大値を求める短いプログラムを考えてください。
ファイル名: src/main.rs
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); }
リスト10-1: 数字のリストから最大値を求めるコード
このコードは、整数のリストを変数number_listに格納し、リストの最初の数字をlargestという変数に配置しています。
それからリストの数字全部を走査し、現在の数字がlargestに格納された数値よりも大きければ、
その変数の値を置き換えます。ですが、現在の数値が今まで見た最大値よりも小さければ、
変数は変わらず、コードはリストの次の数値に移っていきます。リストの数値全てを吟味した後、
largestは最大値を保持しているはずで、今回は100になります。
2つの異なる数値のリストから最大値を発見するには、リスト10-1のコードを複製し、 プログラムの異なる2箇所で同じロジックを使用できます。リスト10-2のようにですね。
ファイル名: src/main.rs
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-2: 2つの数値のリストから最大値を探すコード
このコードは動くものの、コードを複製することは退屈ですし、間違いも起きやすいです。また、 コードを変更したい時に複数箇所、更新しなければなりません。
この重複を排除するには、引数で与えられた整数のどんなリストに対しても処理が行える関数を定義して抽象化できます。 この解決策によりコードがより明確になり、リストの最大値を探すという概念を抽象的に表現させてくれます。
リスト10-3では、最大値を探すコードをlargestという関数に抽出しました。リスト10-1のコードは、
たった1つの特定のリストからだけ最大値を探せますが、それとは異なり、このプログラムは2つの異なるリストから最大値を探せます。
ファイル名: src/main.rs
fn largest(list: &[i32]) -> i32 { let mut largest = list[0]; for &item in list.iter() { 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); }
リスト10-3: 2つのリストから最大値を探す抽象化されたコード
largest関数にはlistと呼ばれる引数があり、これは、関数に渡す可能性のある、あらゆるi32値の具体的なスライスを示します。
結果的に、関数呼び出しの際、コードは渡した特定の値に対して走るのです。
まとめとして、こちらがリスト10-2のコードからリスト10-3に変更するのに要したステップです:
- 重複したコードを見分ける。
- 重複コードを関数本体に抽出し、コードの入力と戻り値を関数シグニチャで指定する。
- 重複したコードの2つの実体を代わりに関数を呼び出すように更新する。
次は、この同じ手順をジェネリクスでも踏んで異なる方法でコードの重複を減らします。
関数本体が特定の値ではなく抽象的なlistに対して処理できたのと同様に、
ジェネリクスは抽象的な型に対して処理するコードを可能にしてくれます。
例えば、関数が2つあるとしましょう: 1つはi32値のスライスから最大の要素を探し、1つはchar値のスライスから最大要素を探します。
この重複はどう排除するのでしょうか?答えを見つけましょう!
ジェネリックなデータ型
関数シグニチャや構造体などの要素の定義を生成するのにジェネリクスを使用することができ、 それはさらに他の多くの具体的なデータ型と使用することもできます。まずは、 ジェネリクスで関数、構造体、enum、メソッドを定義する方法を見ましょう。それから、 ジェネリクスがコードのパフォーマンスに与える影響を議論します。
関数定義では
ジェネリクスを使用する関数を定義する時、通常、引数や戻り値のデータ型を指定する関数のシグニチャにジェネリクスを配置します。 そうすることでコードがより柔軟になり、コードの重複を阻止しつつ、関数の呼び出し元により多くの機能を提供します。
largest関数を続けます。リスト10-4はどちらもスライスから最大値を探す2つの関数を示しています。
ファイル名: src/main.rs
fn largest_i32(list: &[i32]) -> i32 { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest } fn largest_char(list: &[char]) -> char { let mut largest = list[0]; for &item in list.iter() { 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'); }
リスト10-4: 名前とシグニチャの型のみが異なる2つの関数
largest_i32関数は、リスト10-3で抽出したスライスから最大のi32を探す関数です。
largest_char関数は、スライスから最大のcharを探します。関数本体には同じコードがあるので、
単独の関数にジェネリックな型引数を導入してこの重複を排除しましょう。
これから定義する新しい関数の型を引数にするには、ちょうど関数の値引数のように型引数に名前をつける必要があります。
型引数の名前にはどんな識別子も使用できますが、Tを使用します。というのも、慣習では、
Rustの引数名は短く(しばしばたった1文字になります)、Rustの型の命名規則がキャメルケースだからです。
"type"の省略形なので、Tが多くのRustプログラマの既定の選択なのです。
関数の本体で引数を使用するとき、コンパイラがその名前の意味を把握できるようにシグニチャでその引数名を宣言しなければなりません。
同様に、型引数名を関数シグニチャで使用する際には、使用する前に型引数名を宣言しなければなりません。
ジェネリックなlargest関数を定義するために、型名宣言を山カッコ(<>)内、関数名と引数リストの間に配置してください。
こんな感じに:
fn largest<T>(list: &[T]) -> T {
この定義は以下のように解読します: 関数largestは、なんらかの型Tに関してジェネリックであると。
この関数にはlistという引数が1つあり、これは型Tの値のスライスです。
largest関数は同じT型の値を返します。
リスト10-5は、シグニチャにジェネリックなデータ型を使用してlargest関数定義を組み合わせたものを示しています。
このリストはさらに、この関数をi32値かchar値のどちらかで呼べる方法も表示しています。
このコードはまだコンパイルできないことに注意してください。ですが、この章の後ほど修正します。
ファイル名: src/main.rs
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
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);
}
リスト10-5: ジェネリックな型引数を使用するものの、まだコンパイルできないlargest関数の定義
直ちにこのコードをコンパイルしたら、以下のようなエラーが出ます:
error[E0369]: binary operation `>` cannot be applied to type `T`
(エラー: 2項演算`>`は、型`T`に適用できません)
--> src/main.rs:5:12
|
5 | if item > largest {
| ^^^^^^^^^^^^^^
|
= note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
(注釈: `std::cmp::PartialOrd`の実装が`T`に対して存在しない可能性があります)
注釈がstd::cmp::PartialOrdに触れています。これは、トレイトです。トレイトについては、次の節で語ります。
とりあえず、このエラーは、largestの本体は、Tがなりうる全ての可能性のある型に対して動作しないと述べています。
本体で型Tの値を比較したいので、値が順序付け可能な型のみしか使用できないのです。比較を可能にするために、
標準ライブラリには型に実装できるstd::cmp::PartialOrdトレイトがあります(このトレイトについて詳しくは付録Cを参照されたし)。
ジェネリックな型が特定のトレイトを持つと指定する方法は「トレイト境界」節で習うでしょうが、
先にジェネリックな型引数を使用する他の方法を探究しましょう。
構造体定義では
構造体を定義して<>記法で1つ以上のフィールドにジェネリックな型引数を使用することもできます。
リスト10-6は、Point<T>構造体を定義してあらゆる型のxとy座標を保持する方法を示しています。
ファイル名: src/main.rs
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 }; }
リスト10-6: 型Tのxとy値を保持するPoint<T>構造体
構造体定義でジェネリクスを使用する記法は、関数定義のものと似ています。まず、山カッコ内に型引数の名前を構造体名の直後に宣言します。 そうすると、本来具体的なデータ型を記述する構造体定義の箇所に、ジェネリックな型を使用できます。
ジェネリックな型を1つだけ使用してPoint<T>を定義したので、この定義は、Point<T>構造体がなんらかの型Tに関して、
ジェネリックであると述べていて、その型がなんであれ、xとyのフィールドは両方その同じ型になっていることに注意してください。
リスト10-7のように、異なる型の値のあるPoint<T>のインスタンスを生成すれば、コードはコンパイルできません。
ファイル名: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
リスト10-7: どちらも同じジェネリックなデータ型Tなので、xとyというフィールドは同じ型でなければならない
この例で、xに整数値5を代入すると、このPoint<T>のインスタンスに対するジェネリックな型Tは整数になるとコンパイラに知らせます。
それからyに4.0を指定する時に、このフィールドはxと同じ型と定義したはずなので、このように型不一致エラーが出ます:
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integral variable, found
floating-point variable
|
= note: expected type `{integer}`
found type `{float}`
xとyが両方ジェネリックだけれども、異なる型になり得るPoint構造体を定義するには、
複数のジェネリックな型引数を使用できます。例えば、リスト10-8では、Pointの定義を変更して、
型TとUに関してジェネリックにし、xが型Tで、yが型Uになります。
ファイル名: src/main.rs
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 }; }
リスト10-8: Point<T, U>は2つの型に関してジェネリックなので、xとyは異なる型の値になり得る
これで、示されたPointインスタンスは全部使用可能です!所望の数だけ定義でジェネリックな型引数を使用できますが、
数個以上使用すると、コードが読みづらくなります。コードで多くのジェネリックな型が必要な時は、
コードの小分けが必要なサインかもしれません。
enum定義では
構造体のように、列挙子にジェネリックなデータ型を保持するenumを定義することができます。
標準ライブラリが提供しているOption<T> enumをもう一度見ましょう。このenumは第6章で使用しました:
#![allow(unused)] fn main() { enum Option<T> { Some(T), None, } }
この定義はもう、あなたにとってより道理が通っているはずです。ご覧の通り、Option<T>は、
型Tに関してジェネリックで2つの列挙子のあるenumです: その列挙子は、型Tの値を保持するSomeと、
値を何も保持しないNoneです。Option<T> enumを使用することで、オプショナルな値があるという抽象的な概念を表現でき、
Option<T>はジェネリックなので、オプショナルな値の型に関わらず、この抽象を使用できます。
enumも複数のジェネリックな型を使用できます。第9章で使用したResult enumの定義が一例です:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
Result enumは2つの型T、Eに関してジェネリックで、2つの列挙子があります: 型Tの値を保持するOkと、
型Eの値を保持するErrです。この定義により、Result enumを、成功する(なんらかの型Tの値を返す)か、
失敗する(なんらかの型Eのエラーを返す)可能性のある処理がある、あらゆる箇所に使用するのが便利になります。
事実、ファイルを開くのに成功した時にTに型std::fs::Fileが入り、ファイルを開く際に問題があった時にEに型std::io::Errorが入ったものが、
リスト9-3でファイルを開くのに使用したものです。
自分のコード内で、保持している値の型のみが異なる構造体やenum定義の場面を認識したら、 代わりにジェネリックな型を使用することで重複を避けることができます。
メソッド定義では
(第5章のように、)定義にジェネリックな型を使うメソッドを構造体やenumに実装することもできます。リスト10-9は、
リスト10-6で定義したPoint<T>構造体にxというメソッドを実装したものを示しています。
ファイル名: src/main.rs
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()); }
リスト10-9: 型Tのxフィールドへの参照を返すxというメソッドをPoint<T>構造体に実装する
ここで、フィールドxのデータへの参照を返すxというメソッドをPoint<T>に定義しました。
implの直後にTを宣言しなければならないことに注意してください。こうすることで、型Point<T>にメソッドを実装していることを指定するために、Tを使用することができます。
implの後にTをジェネリックな型として宣言することで、コンパイラは、Pointの山カッコ内の型が、
具体的な型ではなくジェネリックな型であることを認識できるのです。
例えば、ジェネリックな型を持つPoint<T>インスタンスではなく、Point<f32>だけにメソッドを実装することもできるでしょう。
リスト10-10では、具体的な型f32を使用しています。つまり、implの後に型を宣言しません。
#![allow(unused)] fn main() { struct Point<T> { x: T, y: T, } impl Point<f32> { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() } } }
リスト10-10: ジェネリックな型引数Tに対して特定の具体的な型がある構造体にのみ適用されるimplブロック
このコードは、Point<f32>にはdistance_from_originというメソッドが存在するが、
Tがf32ではないPoint<T>の他のインスタンスにはこのメソッドが定義されないことを意味します。
このメソッドは、この点が座標(0.0, 0.0)の点からどれだけ離れているかを測定し、
浮動小数点数にのみ利用可能な数学的処理を使用します。
構造体定義のジェネリックな型引数は、必ずしもその構造体のメソッドシグニチャで使用するものと同じにはなりません。
例を挙げれば、リスト10-11は、リスト10-8のPoint<T, U>にメソッドmixupを定義しています。
このメソッドは、他のPointを引数として取り、この引数はmixupを呼び出しているselfのPointとは異なる型の可能性があります。
このメソッドは、(型Tの)selfのPointのx値と渡した(型Wの)Pointのy値から新しいPointインスタンスを生成します。
ファイル名: src/main.rs
struct Point<T, U> { x: T, y: U, } impl<T, U> Point<T, U> { fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { 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); }
リスト10-11: 構造体定義とは異なるジェネリックな型を使用するメソッド
mainで、x(値は5)にi32、y(値は10.4)にf64を持つPointを定義しました。p2変数は、
x(値は"Hello")に文字列スライス、y(値はc)にcharを持つPoint構造体です。
引数p2でp1にmixupを呼び出すと、p3が得られ、xはi32になります。xはp1由来だからです。
p3変数のyは、charになります。yはp2由来だからです。println!マクロの呼び出しは、
p3.x = 5, p3.y = cと出力するでしょう。
この例の目的は、一部のジェネリックな引数はimplで宣言され、他の一部はメソッド定義で宣言される場面をデモすることです。
ここで、ジェネリックな引数TとUはimplの後に宣言されています。構造体定義にはまるからです。
ジェネリックな引数VとWはfn mixupの後に宣言されています。何故なら、このメソッドにしか関係ないからです。
ジェネリクスを使用したコードのパフォーマンス
ジェネリックな型引数を使用すると、実行時にコストが発生するのかな、と思うかもしれません。 嬉しいことにRustでは、ジェネリクスを、具体的な型があるコードよりもジェネリックな型を使用したコードを実行するのが遅くならないように実装しています。
コンパイラはこれを、ジェネリクスを使用しているコードの単相化をコンパイル時に行うことで達成しています。 単相化(monomorphization)は、コンパイル時に使用されている具体的な型を入れることで、 ジェネリックなコードを特定のコードに変換する過程のことです。
この過程において、コンパイラは、リスト10-5でジェネリックな関数を生成するために使用した手順と真逆のことをしています: コンパイラは、ジェネリックなコードが呼び出されている箇所全部を見て、 ジェネリックなコードが呼び出されている具体的な型のコードを生成するのです。
標準ライブラリのOption<T> enumを使用する例でこれが動作する方法を見ましょう:
#![allow(unused)] fn main() { let integer = Some(5); let float = Some(5.0); }
コンパイラがこのコードをコンパイルすると、単相化を行います。その過程で、コンパイラはOption<T>のインスタンスに使用された値を読み取り、
2種類のOption<T>を識別します: 一方はi32で、もう片方はf64です。そのように、
コンパイラは、Option<T>のジェネリックな定義をOption_i32とOption_f64に展開し、
それにより、ジェネリックな定義を特定の定義と置き換えます。
単相化されたバージョンのコードは、以下のようになります。ジェネリックなOption<T>が、
コンパイラが生成した特定の定義に置き換えられています:
ファイル名: src/main.rs
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); }
Rustでは、ジェネリックなコードを各インスタンスで型を指定したコードにコンパイルするので、 ジェネリクスを使用することに対して実行時コストを払うことはありません。コードを実行すると、 それぞれの定義を手作業で複製した時のように振る舞います。単相化の過程により、 Rustのジェネリクスは実行時に究極的に効率的になるのです。
トレイト: 共通の振る舞いを定義する
トレイトは、Rustコンパイラに、特定の型に存在し、他の型と共有できる機能について知らせます。 トレイトを使用すると、共通の振る舞いを抽象的に定義できます。トレイト境界を使用すると、 あるジェネリックが、特定の振る舞いをもつあらゆる型になり得ることを指定できます。
注釈: 違いはあるものの、トレイトは他の言語でよくインターフェイスと呼ばれる機能に類似しています。
トレイトを定義する
型の振る舞いは、その型に対して呼び出せるメソッドから構成されます。異なる型は、それらの型全てに対して同じメソッドを呼び出せるなら、 同じ振る舞いを共有することになります。トレイト定義は、メソッドシグニチャをあるグループにまとめ、なんらかの目的を達成するのに必要な一連の振る舞いを定義する手段です。
例えば、いろんな種類や量のテキストを保持する複数の構造体があるとしましょう: 特定の場所から送られる新しいニュースを保持するNewsArticleと、
新規ツイートか、リツイートか、はたまた他のツイートへのリプライなのかを示すメタデータを伴う最大で280文字までのTweetです。
NewsArticle または Tweet インスタンスに保存されているデータのサマリーを表示できるメディア アグリゲータ ライブラリを作成します。
これをするには、各型のサマリーが必要で、インスタンスで summarize メソッドを呼び出してサマリーを要求する必要があります。
リスト10-12は、この振る舞いを表現するSummaryトレイトの定義を表示しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub trait Summary { fn summarize(&self) -> String; } }
リスト10-12: summarizeメソッドで提供される振る舞いからなるSummaryトレイト
ここでは、traitキーワード、それからトレイト名を使用してトレイトを定義していて、その名前は今回の場合、
Summaryです。波括弧の中にこのトレイトを実装する型の振る舞いを記述するメソッドシグニチャを定義し、
今回の場合は、fn summarize(&self) -> Stringです。
メソッドシグニチャの後に、波括弧内に実装を提供する代わりに、セミコロンを使用しています。
このトレイトを実装する型はそれぞれ、メソッドの本体に独自の振る舞いを提供しなければなりません。
コンパイラにより、Summaryトレイトを保持するあらゆる型に、このシグニチャと全く同じメソッドsummarizeが定義されていることが
強制されます。
トレイトには、本体に複数のメソッドを含むことができます: メソッドシグニチャは行ごとに並べられ、 各行はセミコロンで終わります。
トレイトを型に実装する
今や Summary トレイトを使用して目的の動作を定義できたので、メディア アグリゲータでこれを型に実装できます。
リスト10-13は、 Summary トレイトを NewsArticle 構造体上に実装したもので、ヘッドライン、著者、そして地域情報を使ってsummarize の戻り値を作っています。
Tweet 構造体に関しては、ツイートの内容が既に280文字に制限されていると仮定して、ユーザー名の後にツイートのテキスト全体が続くものとして summarize を定義します。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { 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 Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } } }
リスト10-13: SummaryトレイトをNewsArticleとTweet型に実装する
型にトレイトを実装することは、普通のメソッドを実装することに似ています。違いは、implの後に、
実装したいトレイトの名前を置き、それからforキーワード、さらにトレイトの実装対象の型の名前を指定することです。
implブロック内に、トレイト定義で定義したメソッドシグニチャを置きます。各シグニチャの後にセミコロンを追記するのではなく、
波括弧を使用し、メソッド本体に特定の型のトレイトのメソッドに欲しい特定の振る舞いを入れます。
トレイトを実装後、普通のメソッド同様にNewsArticleやTweetのインスタンスに対してこのメソッドを呼び出せます。
こんな感じで:
use chapter10::{self, Summary, Tweet};
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
// もちろん、ご存知かもしれませんがね、みなさん
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
}
このコードは、1 new tweet: horse_ebooks: of course, as you probably already know, peopleと出力します。
リスト10-13でSummaryトレイトとNewArticle、Tweet型を同じlib.rsに定義したので、
全部同じスコープにあることに注目してください。このlib.rsをaggregatorと呼ばれるクレート専用にして、
誰か他の人が私たちのクレートの機能を活用して自分のライブラリのスコープに定義された構造体にSummaryトレイトを実装したいとしましょう。
まず、トレイトをスコープに取り込む必要があるでしょう。use aggregator::Summary;と指定してそれを行えば、
これにより、自分の型にSummaryを実装することが可能になるでしょう。Summaryトレイトは、
他のクレートが実装するためには、公開トレイトである必要があり、ここでは、リスト10-12のtraitの前に、
pubキーワードを置いたのでそうなっています。
トレイト実装で注意すべき制限の1つは、トレイトか対象の型が自分のクレートに固有(local)である時のみ、
型に対してトレイトを実装できるということです。例えば、Displayのような標準ライブラリのトレイトをaggregatorクレートの機能の一部として、
Tweetのような独自の型に実装できます。型Tweetがaggregatorクレートに固有だからです。
また、SummaryをaggregatorクレートでVec<T>に対して実装することもできます。
トレイトSummaryは、aggregatorクレートに固有だからです。
しかし、外部のトレイトを外部の型に対して実装することはできません。例として、
aggregatorクレート内でVec<T>に対してDisplayトレイトを実装することはできません。
DisplayとVec<T>は標準ライブラリで定義され、aggregatorクレートに固有ではないからです。
この制限は、コヒーレンス(coherence)、特に孤児のルール(orphan rule)と呼ばれるプログラムの特性の一部で、
親の型が存在しないためにそう命名されました。この規則により、他の人のコードが自分のコードを壊したり、
その逆が起きないことを保証してくれます。この規則がなければ、2つのクレートが同じ型に対して同じトレイトを実装できてしまい、
コンパイラはどちらの実装を使うべきかわからなくなってしまうでしょう。
デフォルト実装
時として、全ての型の全メソッドに対して実装を要求するのではなく、トレイトの全てあるいは一部のメソッドに対してデフォルトの振る舞いがあると有用です。 そうすれば、特定の型にトレイトを実装する際、各メソッドのデフォルト実装を保持するかオーバーライドするか選べるわけです。
リスト10-14は、リスト10-12のように、メソッドシグニチャだけを定義するのではなく、
Summaryトレイトのsummarizeメソッドにデフォルトの文字列を指定する方法を示しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { 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 Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } } }
リスト10-14: summarizeメソッドのデフォルト実装があるSummaryトレイトの定義
独自の実装を定義するのではなく、デフォルト実装を利用してNewsArticleのインスタンスをまとめるには、
impl Summary for NewsArticle {}と空のimplブロックを指定します。
もはやNewsArticleに直接summarizeメソッドを定義してはいませんが、私達はデフォルト実装を提供しており、
NewsArticleはSummaryトレイトを実装すると指定しました。そのため、
NewsArticleのインスタンスに対してsummarizeメソッドを同じように呼び出すことができます。
このように:
use chapter10::{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"),
// ピッツバーグ・ペンギンが再度NHL(National Hockey League)で最強のホッケーチームになった
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...)(新しい記事があります!(もっと読む))と出力します。
summarizeにデフォルト実装を用意しても、リスト10-13のTweetのSummary実装を変える必要はありません。
理由は、デフォルト実装をオーバーライドする記法はデフォルト実装のないトレイトメソッドを実装する記法と同じだからです。
デフォルト実装は、自らのトレイトのデフォルト実装を持たない他のメソッドを呼び出すことができます。
このようにすれば、トレイトは多くの有用な機能を提供しつつ、実装者は僅かな部分しか指定しなくて済むようになります。
例えば、Summaryトレイトを、(実装者が)内容を実装しなければならないsummarize_authorメソッドを持つように定義し、
それからsummarize_authorメソッドを呼び出すデフォルト実装を持つsummarizeメソッドを定義することもできます:
#![allow(unused)] fn main() { pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { // "({}さんの文章をもっと読む)" format!("(Read more from {}...)", self.summarize_author()) } } pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } impl Summary for Tweet { 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 Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
summarize_author定義後、Tweet構造体のインスタンスに対してsummarizeを呼び出せ、
summarizeのデフォルト実装は、私達が提供したsummarize_authorの定義を呼び出すでしょう。
summarize_authorを実装したので、追加のコードを書く必要なく、Summaryトレイトは、
summarizeメソッドの振る舞いを与えてくれました。
use chapter10::{self, Summary, Tweet};
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
}
このコードは、1 new tweet: (Read more from @horse_ebooks...)(1つの新しいツイート:(@horse_ebooksさんの文章をもっと読む))と出力します。
デフォルト実装を、そのメソッドをオーバーライドしている実装から呼び出すことはできないことに注意してください。
引数としてのトレイト
トレイトを定義し実装する方法はわかったので、トレイトを使っていろんな種類の型を受け付ける関数を定義する方法を学んでいきましょう。
たとえば、Listing 10-13では、NewsArticleとTweet型に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 Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
引数のitemには、具体的な型の代わりに、implキーワードとトレイト名を指定します。
この引数は、指定されたトレイトを実装しているあらゆる型を受け付けます。
notifyの中身では、summarizeのような、Summaryトレイトに由来するitemのあらゆるメソッドを呼び出すことができます。
私達は、notifyを呼びだし、NewsArticleかTweetのどんなインスタンスでも渡すことができます。
この関数を呼び出すときに、Stringやi32のような他の型を渡すようなコードはコンパイルできません。
なぜなら、これらの型はSummaryを実装していないからです。
トレイト境界構文
impl Trait構文は単純なケースを解決しますが、実はより長いトレイト境界 (trait bound) と呼ばれる姿の糖衣構文 (syntax sugar) なのです。
それは以下のようなものです:
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) {
この関数が受け取るitem1とitem2の型が(どちらもSummaryを実装する限り)異なっても良いとするならば、impl Traitは適切でしょう。
両方の引数が同じ型であることを強制することは、以下のようにトレイト境界を使ってのみ表現可能です:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
引数であるitem1とitem2の型としてジェネリックな型Tを指定しました。
これにより、item1とitem2として関数に渡される値の具体的な型が同一でなければならない、という制約を与えています。
複数のトレイト境界を+構文で指定する
複数のトレイト境界も指定できます。
たとえば、notifyにsummarizeメソッドに加えてitemの画面出力形式(ディスプレイフォーマット)を使わせたいとします。
その場合は、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
{
この関数シグニチャは、よりさっぱりとしています。トレイト境界を多く持たない関数と同じように、関数名、引数リスト、戻り値の型が一緒になって近くにあるからですね。
トレイトを実装している型を返す
以下のように、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 Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
戻り値の型としてimpl Summaryを使うことにより、具体的な型が何かを言うことなく、returns_summarizable関数はSummaryトレイトを実装している何らかの型を返すのだ、と指定することができます。
今回returns_summarizableはTweetを返しますが、この関数を呼び出すコードはそのことを知りません。
実装しているトレイトだけで戻り値型を指定できることは、13章で学ぶ、クロージャとイテレータを扱うときに特に便利です。
クロージャとイテレータの作り出す型は、コンパイラだけが知っているものであったり、指定するには長すぎるものであったりします。
impl Trait構文を使えば、非常に長い型を書くことなく、ある関数はIteratorトレイトを実装するある型を返すのだ、と簡潔に指定することができます。
ただし、impl Traitは一種類の型を返す場合にのみ使えます。
たとえば、以下のように、戻り値の型はimpl Summaryで指定しつつ、NewsArticleかTweetを返すようなコードは失敗します:
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 Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
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 {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
}
NewsArticleかTweetを返すというのは、コンパイラのimpl Trait構文の実装まわりの制約により許されていません。
このような振る舞いをする関数を書く方法は、17章のトレイトオブジェクトで異なる型の値を許容する節で学びます。
トレイト境界でlargest関数を修正する
ジェネリックな型引数の境界で使用したい振る舞いを指定する方法がわかったので、リスト10-5に戻って、
ジェネリックな型引数を使用するlargest関数の定義を修正しましょう!最後にそのコードを実行しようとした時、
こんなエラーが出ていました:
$ 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
|
= note: `T` might need a bound for `std::cmp::PartialOrd`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
largestの本体で、大なり演算子(>)を使用して型Tの2つの値を比較しようとしていました。この演算子は、
標準ライブラリトレイトのstd::cmp::PartialOrdでデフォルトメソッドとして定義されているので、
largest関数が、比較できるあらゆる型のスライスに対して動くようにするためには、Tのトレイト境界にPartialOrdを指定する必要があります。
PartialOrdはpreludeに含まれているので、これをスコープに導入する必要はありません。
largestのシグニチャを以下のように変えてください:
fn largest<T: PartialOrd>(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[E0508]: cannot move out of type `[T]`, a non-copy slice
(エラー[E0508]: 型`[T]`をもつ、非コピーのスライスからのムーブはできません)
--> src/main.rs:2:23
|
2 | let mut largest = list[0];
| ^^^^^^^
| |
| cannot move out of here
| (ここからムーブすることはできません)
| move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait
| (ムーブが発生するのは、`list[_]`は`T`という、`Copy`トレイトを実装しない型であるためです)
| help: consider borrowing here: `&list[0]`
| (助言:借用するようにしてみてはいかがですか: `&list[0]`)
error[E0507]: cannot move out of a shared reference
(エラー[E0507]:共有の参照からムーブはできません)
--> src/main.rs:4:18
|
4 | for &item in list {
| ----- ^^^^
| ||
| |data moved here
| |(データがここでムーブされています)
| |move occurs because `item` has type `T`, which does not implement the `Copy` trait
| |(ムーブが発生するのは、`item`は`T`という、`Copy`トレイトを実装しない型であるためです)
| help: consider removing the `&`: `item`
| (助言:`&`を取り除いてみてはいかがですか: `item`)
error: aborting due to 2 previous errors
Some errors have detailed explanations: E0507, E0508.
For more information about an error, try `rustc --explain E0507`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
このエラーの鍵となる行は、cannot move out of type [T], a non-copy sliceです。
ジェネリックでないバージョンのlargest関数では、最大のi32かcharを探そうとするだけでした。
第4章のスタックのみのデータ: コピー節で議論したように、i32やcharのようなサイズが既知の型は
スタックに格納できるので、Copyトレイトを実装しています。しかし、largest関数をジェネリックにすると、
list引数がCopyトレイトを実装しない型を含む可能性も出てきたのです。結果として、
list[0]から値をlargestにムーブできず、このエラーに陥ったのです。
このコードをCopyトレイトを実装する型だけを使って呼び出すようにしたいなら、Tのトレイト境界にCopyを追加すればよいです!
リスト10-15は、関数に渡したスライスの値の型が、i32やcharなどのようにPartialOrdとCopyを実装する限りコンパイルできる、ジェネリックなlargest関数の完全なコードを示しています。
ファイル名: src/main.rs
fn largest<T: PartialOrd + Copy>(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); }
リスト10-15: PartialOrdとCopyトレイトを実装するあらゆるジェネリックな型に対して動く、
largest関数の実際の定義
もしlargest関数をCopyを実装する型だけに制限したくなかったら、TがCopyではなくCloneというトレイト境界を持つと指定することもできます。そうしたら、
largest関数に所有権が欲しい時にスライスの各値をクローンできます。clone関数を使用するということは、
Stringのようなヒープデータを持つ型の場合により多くのヒープ確保が発生する可能性があることを意味します。
そして、大量のデータを取り扱っていたら、ヒープ確保には時間がかかることもあります。
largestの別の実装方法は、関数がスライスのT値への参照を返すようにすることです。
戻り値の型をTではなく&Tに変え、それにより関数の本体を参照を返すように変更したら、
CloneやCopyトレイト境界は必要なくなり、ヒープ確保も避けられるでしょう。
これらの代替策をご自身で実装してみましょう!
トレイト境界を使用して、メソッド実装を条件分けする
ジェネリックな型引数を持つimplブロックにトレイト境界を与えることで、
特定のトレイトを実装する型に対するメソッド実装を条件分けできます。例えば、
リスト10-16の型Pair<T>は、常にnew関数を実装します。しかし、Pair<T>は、
内部の型Tが比較を可能にするPartialOrdトレイトと出力を可能にするDisplayトレイトを実装している時のみ、
cmp_displayメソッドを実装します。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { 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); } } } }
リスト10-16: トレイト境界によってジェネリックな型に対するメソッド実装を条件分けする
また、別のトレイトを実装するあらゆる型に対するトレイト実装を条件分けすることもできます。
トレイト境界を満たすあらゆる型にトレイトを実装することは、ブランケット実装(blanket implementation)と呼ばれ、
Rustの標準ライブラリで広く使用されています。例を挙げれば、標準ライブラリは、
Displayトレイトを実装するあらゆる型にToStringトレイトを実装しています。
標準ライブラリのimplブロックは以下のような見た目です:
impl<T: Display> ToString for T {
// --snip--
}
標準ライブラリにはこのブランケット実装があるので、Displayトレイトを実装する任意の型に対して、
ToStringトレイトで定義されたto_stringメソッドを呼び出せるのです。
例えば、整数はDisplayを実装するので、このように整数値を対応するString値に変換できます:
#![allow(unused)] fn main() { let s = 3.to_string(); }
ブランケット実装は、トレイトのドキュメンテーションの「実装したもの」節に出現します。
トレイトとトレイト境界により、ジェネリックな型引数を使用して重複を減らしつつ、コンパイラに対して、 そのジェネリックな型に特定の振る舞いが欲しいことを指定するコードを書くことができます。 それからコンパイラは、トレイト境界の情報を活用してコードに使用された具体的な型が正しい振る舞いを提供しているか確認できます。 動的型付き言語では、その型に定義されていないメソッドを呼び出せば、実行時 (runtime) にエラーが出るでしょう。 しかし、Rustはこの種のエラーをコンパイル時に移したので、コードが動かせるようになる以前に問題を修正することを強制されるのです。 加えて、コンパイル時に既に確認したので、実行時の振る舞いを確認するコードを書かなくても済みます。 そうすることで、ジェネリクスの柔軟性を諦めることなくパフォーマンスを向上させます。
すでに使っている他のジェネリクスに、ライフタイムと呼ばれるものがあります。 ライフタイムは、型が欲しい振る舞いを保持していることではなく、必要な間だけ参照が有効であることを保証します。 ライフタイムがどうやってそれを行うかを見てみましょう。
ライフタイムで参照を検証する
第4章の「参照と借用」節で議論しなかった詳細の一つに、Rustにおいて参照は全てライフタイムを保持するということがあります。 ライフタイムとは、その参照が有効になるスコープのことです。多くの場合、型が推論されるように、 大体の場合、ライフタイムも暗黙的に推論されます。複数の型の可能性があるときには、型を注釈しなければなりません。 同様に、参照のライフタイムがいくつか異なる方法で関係することがある場合には注釈しなければなりません。 コンパイラは、ジェネリックライフタイム引数を使用して関係を注釈し、実行時に実際の参照が確かに有効であることを保証することを要求するのです。
ライフタイムの概念は、他のプログラミング言語の道具とはどこか異なり、間違いなくRustで一番際立った機能になっています。 この章では、ライフタイムの全体を解説することはしませんが、 ライフタイム記法が必要となる最も一般的な場合について議論しますので、ライフタイムの概念について馴染むことができるでしょう。
ライフタイムでダングリング参照を回避する
ライフタイムの主な目的は、ダングリング参照を回避することです。ダングリング参照によりプログラムは、 参照するつもりだったデータ以外のデータを参照してしまいます。リスト10-17のプログラムを考えてください。 これには、外側のスコープと内側のスコープが含まれています。
fn main() {
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
}
リスト10-17: 値がスコープを抜けてしまった参照を使用しようとする
注釈: リスト10-17や10-18、10-24では、変数に初期値を与えずに宣言しているので、変数名は外側のスコープに存在します。 初見では、これは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
(エラー[E0597]: `x`の生存期間が短すぎます)
--> src/main.rs:7:17
|
7 | r = &x;
| ^^ borrowed value does not live long enough
| (借用された値の生存期間が短すぎます)
8 | }
| - `x` dropped here while still borrowed
| (`x`は借用されている間にここでドロップされました)
9 |
10 | println!("r: {}", r);
| - borrow later used here
| (その後、借用はここで使われています)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
変数xの「生存期間が短すぎます」。原因は、内側のスコープが7行目で終わった時点でxがスコープを抜けるからです。
ですが、rはまだ、外側のスコープに対して有効です; スコープが大きいので、「長生きする」と言います。
Rustで、このコードが動くことを許可していたら、rはxがスコープを抜けた時に解放されるメモリを参照していることになり、
rで行おうとするいかなることもちゃんと動作しないでしょう。では、どうやってコンパイラはこのコードが無効であると決定しているのでしょうか?
それは、借用チェッカーを使用しているのです。
借用精査機
Rustコンパイラには、スコープを比較して全ての借用が有効であるかを決定する借用チェッカーがあります。 リスト10-18は、リスト10-17と同じコードを示していますが、変数のライフタイムを表示する注釈が付いています。
fn main() {
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
}
リスト10-18: それぞれ'aと'bと名付けられたrとxのライフタイムの注釈
ここで、rのライフタイムは'a、xのライフタイムは'bで注釈しました。ご覧の通り、
内側の'bブロックの方が、外側の'aライフタイムブロックよりはるかに小さいです。
コンパイル時に、コンパイラは2つのライフタイムのサイズを比較し、rは'aのライフタイムだけれども、
'bのライフタイムのメモリを参照していると確認します。'bは'aよりも短いので、プログラムは拒否されます:
参照の対象が参照ほど長生きしないのです。
リスト10-19でコードを修正したので、ダングリング参照はなくなり、エラーなくコンパイルできます。
fn main() { { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {}", r); // | | // --+ | } // ----------+ }
リスト10-19: データのライフタイムが参照より長いので、有効な参照
ここでxのライフタイムは'bになり、今回の場合'aよりも大きいです。つまり、
コンパイラはxが有効な間、rの参照も常に有効になることを把握しているので、rはxを参照できます。
今や、参照のライフタイムがどれだけであるかと、コンパイラがライフタイムを解析して参照が常に有効であることを保証する仕組みがわかったので、 関数における引数と戻り値のジェネリックなライフタイムを探究しましょう。
関数のジェネリックなライフタイム
2つの文字列スライスのうち、長い方を返す関数を書きましょう。この関数は、
2つの文字列スライスを取り、1つの文字列スライスを返します。longest関数の実装完了後、
リスト10-20のコードは、The longest string is abcdと出力するはずです。
ファイル名: src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
// 最長の文字列は、{}です
println!("The longest string is {}", result);
}
リスト10-20: longest関数を呼び出して2つの文字列スライスのうち長い方を探すmain関数
関数に取ってほしい引数が文字列スライス、つまり参照であることに注意してください。
何故なら、longest関数に引数の所有権を奪ってほしくないからです。
リスト10-20で使用している引数が、我々が必要としているものである理由についてもっと詳しい議論は、
第4章の「引数としての文字列スライス」節をご参照ください。
リスト10-21に示すようにlongest関数を実装しようとしたら、コンパイルできないでしょう。
ファイル名: src/main.rs
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
}
}
リスト10-21: 2つの文字列スライスのうち長い方を返すけれども、コンパイルできないlongest関数の実装
代わりに、以下のようなライフタイムに言及するエラーが出ます:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
(エラー[E0106]: ライフタイム指定子が不足しています)
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ^ expected 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`
(助言: この関数の戻り値型は借用された値を含んでいますが、
シグニチャは、それが`x`と`y`どちらから借用されたものなのか宣言していません)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
助言テキストが、戻り値の型はジェネリックなライフタイム引数である必要があると明かしています。
というのも、返している参照がxかyのどちらを参照しているか、コンパイラにはわからないからです。
実際のところ、この関数の本体のifブロックはxへの参照を返し、elseブロックはyへの参照を返すので、
どちらなのか私たちにもわかりません!
この関数を定義する際、この関数に渡される具体的な値がわからないので、ifケースとelseケースのどちらが実行されるかわからないのです。
また、リスト10-18と10-19で、返す参照が常に有効であるかを決定したときのようにスコープを見ることも、渡される参照の具体的なライフタイムがわからないのでできないのです。
借用チェッカーもこれを決定することはできません。xとyのライフタイムがどう戻り値のライフタイムと関係するかわからないからです。
このエラーを修正するために、借用チェッカーが解析を実行できるように、参照間の関係を定義するジェネリックなライフタイム引数を追加しましょう。
ライフタイム注釈記法
ライフタイム注釈は、いかなる参照の生存期間も変えることはありません。シグニチャにジェネリックな型引数を指定された 関数が、あらゆる型を受け取ることができるのと同様に、ジェネリックなライフタイム引数を指定された関数は、 あらゆるライフタイムの参照を受け取ることができます。ライフタイム注釈は、ライフタイムに影響することなく、 複数の参照のライフタイムのお互いの関係を記述します。
ライフタイム注釈は、少し不自然な記法です: ライフタイム引数の名前はアポストロフィー(')で始まらなければならず、
通常全部小文字で、ジェネリック型のようにとても短いです。多くの人は、'aという名前を使います。
ライフタイム引数注釈は、参照の&の後に配置し、注釈と参照の型を区別するために空白を1つ使用します。
例を挙げましょう: ライフタイム引数なしのi32への参照、'aというライフタイム引数付きのi32への参照、
そして同じくライフタイム'aを持つi32への可変参照です。
&i32 // a reference
// (ただの)参照
&'a i32 // a reference with an explicit lifetime
// 明示的なライフタイム付きの参照
&'a mut i32 // a mutable reference with an explicit lifetime
// 明示的なライフタイム付きの可変参照
1つのライフタイム注釈それだけでは、大して意味はありません。注釈は、複数の参照のジェネリックなライフタイム引数が、
お互いにどう関係するかをコンパイラに指示することを意図しているからです。例えば、
ライフタイム'a付きのi32への参照となる引数firstのある関数があるとしましょう。
この関数にはさらに、'aのライフタイム付きのi32への別の参照となるsecondという別の引数もあります。
ライフタイム注釈は、firstとsecondの参照がどちらもこのジェネリックなライフタイムと同じだけ生きることを示唆します。
関数シグニチャにおけるライフタイム注釈
さて、longest関数を例にライフタイム注釈を詳しく見ていきましょう。ジェネリックな型引数同様、
関数名と引数リストの間の山カッコの中にジェネリックなライフタイム引数を宣言します。
このシグニチャで表現したい制約は、引数の全ての参照と戻り値が同じライフタイムを持つことです。
リスト10-22に示すように、ライフタイムを'aと名付け、それを各参照に付与します。
ファイル名: src/main.rs
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-22: シグニチャの全参照が同じライフタイム'aを持つと指定したlongest関数の定義
このコードはコンパイルでき、リスト10-20のmain関数とともに使用したら、欲しい結果になるはずです。
これで関数シグニチャは、何らかのライフタイム'aに対して、関数は2つの引数を取り、
どちらも少なくともライフタイム'aと同じだけ生きる文字列スライスであるとコンパイラに教えるようになりました。
また、この関数シグニチャは、関数から返る文字列スライスも少なくともライフタイム'aと同じだけ生きると、
コンパイラに教えています。
実際には、longest関数が返す参照のライフタイムは、渡された参照のうち、小さい方のライフタイムと同じであるという事です。
これらの制約は、まさに私たちがコンパイラに保証してほしかったものです。
この関数シグニチャでライフタイム引数を指定する時、渡されたり、返したりした、いかなる値のライフタイムも変更していないことを思い出してください。
むしろ、借用チェッカーは、これらの制約を守らない値全てを拒否するべきと指定しています。
longest関数は、xとyの正確な生存期間を知っている必要はなく、
このシグニチャを満たすようなスコープを'aに代入できることを知っているだけであることに注意してください。
関数にライフタイムを注釈するときは、注釈は関数の本体ではなくシグニチャに付与します。 コンパイラは注釈がなくとも関数内のコードを解析できます。しかしながら、 関数に関数外からの参照や関数外への参照がある場合、コンパイラが引数や戻り値のライフタイムを自力で解決することはほとんど不可能になります。 そのライフタイムは、関数が呼び出される度に異なる可能性があります。このために、手動でライフタイムを注釈する必要があるのです。
具体的な参照をlongestに渡すと、'aに代入される具体的なライフタイムは、xのスコープの一部であってyのスコープと重なる部分となります。
言い換えると、ジェネリックなライフタイム'aは、xとyのライフタイムのうち、小さい方に等しい具体的なライフタイムになるのです。
返却される参照を同じライフタイム引数'aで注釈したので、返却される参照もxかyのライフタイムの小さい方と同じだけ有効になるでしょう。
ライフタイム注釈が異なる具体的なライフタイムを持つ参照を渡すことでlongest関数を制限する方法を見ましょう。
リスト10-23はそのシンプルな例です。
ファイル名: src/main.rs
fn main() { // 長い文字列は長い let string1 = String::from("long string is long"); // (訳注:この言葉自体に深い意味はない。下の"xyz"より長いということだけが重要) { 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 } }
リスト10-23: 異なる具体的なライフタイムを持つString値への参照でlongest関数を使用する
この例において、string1は外側のスコープの終わりまで有効で、string2は内側のスコープの終わりまで有効、
そしてresultは内側のスコープの終わりまで有効な何かを参照しています。このコードを実行すると、
借用チェッカーがこのコードを良しとするのがわかるでしょう。要するに、コンパイルでき、
The longest string is long string is longと出力するのです。
次に、resultの参照のライフタイムが2つの引数の小さい方のライフタイムになることを示す例を試しましょう。
result変数の宣言を内側のスコープの外に移すものの、result変数への代入はstring2のスコープ内に残したままにします。
それからresultを使用するprintln!を内側のスコープの外、内側のスコープが終わった後に移動します。
リスト10-24のコードはコンパイルできません。
ファイル名: src/main.rs
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
}
}
リスト10-24: string2がスコープを抜けてからresultを使用しようとする
このコードのコンパイルを試みると、こんなエラーになります:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
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
error: aborting due to previous error
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
このエラーは、resultがprintln!文に対して有効であるためには、string2が外側のスコープの終わりまで有効である必要があることを示しています。
関数引数と戻り値のライフタイムを同じライフタイム引数'aで注釈したので、コンパイラはこのことを知っています。
人間からしたら、string1はstring2よりも長く、それ故にresultがstring1への参照を含んでいることは
コードから明らかです。まだstring1はスコープを抜けていないので、
string1への参照はprintln!にとって有効でしょう。ですが、コンパイラはこの場合、
参照が有効であると見なせません。longest関数から返ってくる参照のライフタイムは、
渡した参照のうちの小さい方と同じだとコンパイラに指示しました。したがって、
借用チェッカーは、リスト10-24のコードを無効な参照がある可能性があるとして許可しないのです。
試しに、値や、longest関数に渡される参照のライフタイムや、返される参照の使われかたが異なる実験をもっとしてみてください。
コンパイル前に、その実験が借用チェッカーを通るかどうか仮説を立ててください; そして、正しいか確かめてください!
ライフタイムの観点で思考する
何にライフタイム引数を指定する必要があるかは、関数が行っていることに依存します。例えば、
longest関数の実装を最長の文字列スライスではなく、常に最初の引数を返すように変更したら、
y引数に対してライフタイムを指定する必要はなくなるでしょう。以下のコードはコンパイルできます:
ファイル名: src/main.rs
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関数の未完成の実装を考えてください:
ファイル名: src/main.rs
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`
(エラー[E0515]: ローカル変数`result`を参照している値は返せません)
--> src/main.rs:11:5
|
11 | result.as_str()
| ------^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `result` is borrowed here
| (現在の関数に所有されているデータを参照する値を返しています
| `result`はここで借用されています)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
問題は、resultがlongest関数の末端でスコープを抜け、片付けられてしまうことです。
かつ、関数からresultへの参照を返そうともしています。ダングリング参照を変えてくれるようなライフタイム引数を指定する手段はなく、
コンパイラは、ダングリング参照を生成させてくれません。今回の場合、最善の修正案は、
(呼び出し先ではなく)呼び出し元の関数に値の片付けをさせるために、参照ではなく所有されたデータ型を返すことでしょう。
究極的にライフタイム記法は、関数のいろんな引数と戻り値のライフタイムを接続することに関するものです。 一旦それらが繋がれば、メモリ安全な処理を許可し、ダングリングポインタを生成したりメモリ安全性を侵害したりする処理を禁止するのに十分な情報をコンパイラは得たことになります。
構造体定義のライフタイム注釈
ここまで、所有された型を保持する構造体だけを定義してきました。構造体に参照を保持させることもできますが、
その場合、構造体定義の全参照にライフタイム注釈を付け加える必要があるでしょう。
リスト10-25には、文字列スライスを保持するImportantExcerpt(重要な一節)という構造体があります。
ファイル名: src/main.rs
struct ImportantExcerpt<'a> { part: &'a str, } fn main() { // 僕をイシュマエルとお呼び。何年か前・・・ let novel = String::from("Call me Ishmael. Some years ago..."); // "'.'が見つかりませんでした" let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence, }; }
リスト10-25: 参照を含む構造体なので、定義にライフタイム注釈が必要
この構造体には文字列スライスを保持する1つのフィールド、partがあり、これは参照です。
ジェネリックなデータ型同様、構造体名の後、山カッコの中にジェネリックなライフタイム引数の名前を宣言するので、
構造体定義の本体でライフタイム引数を使用できます。この注釈は、ImportantExcerptのインスタンスが、
partフィールドに保持している参照よりも長生きしないことを意味します。
ここのmain関数は、変数novelに所有されるStringの、最初の文への参照を保持するImportantExcerptインスタンスを生成しています。
novelのデータは、ImportantExcerptインスタンスが作られる前に存在しています。
加えて、ImportantExcerptがスコープを抜けるまでnovelはスコープを抜けないので、
ImportantExcerptインスタンスの参照は有効なのです。
ライフタイム省略
全参照にはライフタイムがあり、参照を使用する関数や構造体にはライフタイム引数を指定する必要があることを学びました。 しかし、リスト4-9にあった関数(リスト10-26に再度示しました)はライフタイム注釈なしでコンパイルできました。
ファイル名: src/lib.rs
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); }
リスト10-26: リスト4-9で定義した、引数と戻り値型が参照であるにも関わらず、ライフタイム注釈なしでコンパイルできた関数
この関数がライフタイム注釈なしでコンパイルできる理由には、Rustの歴史が関わっています: 昔のバージョンのRust(1.0以前)では、 全参照に明示的なライフタイムが必要だったので、このコードはコンパイルできませんでした。 その頃、関数シグニチャはこのように記述されていたのです:
fn first_word<'a>(s: &'a str) -> &'a str {
多くの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番目の規則は、複数の入力ライフタイム引数があるけれども、メソッドなのでそのうちの一つが&selfや&mut selfだったら、
selfのライフタイムが全出力ライフタイム引数に代入されるというものです。
この3番目の規則により、必要なシンボルの数が減るので、メソッドが遥かに読み書きしやすくなります。
コンパイラの立場になってみましょう。これらの規則を適用して、リスト10-26の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-21で取り掛かったときにはライフタイム引数がなかった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-21でこのコードをコンパイルしようとしてエラーになったのです:
コンパイラは、ライフタイム省略規則全てを適用したけれども、シグニチャの参照全部のライフタイムを計算できなかったのです。
実際のところ、3番目の規則はメソッドのシグニチャにしか適用されません。ですので、次はその文脈においてライフタイムを観察し、 3番目の規則のおかげで、メソッドシグニチャであまり頻繁にライフタイムを注釈しなくても済む理由を確認します。
メソッド定義におけるライフタイム注釈
構造体にライフタイムのあるメソッドを実装する際、リスト10-11で示したジェネリックな型引数と同じ記法を使用します。 ライフタイム引数を宣言し使用する場所は、構造体フィールドかメソッド引数と戻り値に関係するかによります。
構造体のフィールド用のライフタイム名は、implキーワードの後に宣言する必要があり、
それから構造体名の後に使用されます。そのようなライフタイムは構造体の型の一部になるからです。
implブロック内のメソッドシグニチャでは、参照は構造体のフィールドの参照のライフタイムに紐づいている可能性と、
独立している可能性があります。加えて、ライフタイム省略規則により、メソッドシグニチャでライフタイム注釈が必要なくなることがよくあります。
リスト10-25で定義したImportantExcerptという構造体を使用した例をいくつか見てみましょう。
まず、唯一の引数がselfへの参照で戻り値がi32という何かへの参照ではないlevelというメソッドを使用します:
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().expect("Could not find a '.'"); 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().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence, }; }
2つ入力ライフタイムがあるので、コンパイラは最初のライフタイム省略規則を適用し、
&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ライフタイムを指定することではありません。
ジェネリックな型引数、トレイト境界、ライフタイムを一度に
ジェネリックな型引数、トレイト境界、ライフタイム指定の構文のすべてを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 } }
これがリスト10-22からの2つの文字列のうち長い方を返すlongest関数ですが、
ジェネリックな型Tのannという追加の引数があり、これはwhere節で指定されているように、
Displayトレイトを実装するあらゆる型で埋めることができます。
この追加の引数は、関数が文字列スライスの長さを比較する前に出力されるので、
Displayトレイト境界が必要なのです。ライフタイムは一種のジェネリックなので、
ライフタイム引数'aとジェネリックな型引数Tが関数名の後、山カッコ内の同じリストに収まっています。
まとめ
たくさんのことをこの章では講義しましたね!今やジェネリックな型引数、トレイトとトレイト境界、そしてジェネリックなライフタイム引数を知ったので、 多くの異なる場面で動くコードを繰り返すことなく書く準備ができました。ジェネリックな型引数により、 コードを異なる型に適用させてくれます。トレイトとトレイト境界は、型がジェネリックであっても、 コードが必要とする振る舞いを持つことを保証します。ライフタイム注釈を活用して、 この柔軟なコードにダングリング参照が存在しないことを保証する方法を学びました。 さらにこの解析は全てコンパイル時に起こり、実行時のパフォーマンスには影響しません!
信じられないかもしれませんが、この章で議論した話題にはもっともっと学ぶべきことがあります: 第17章ではトレイトオブジェクトを議論します。これはトレイトを使用する別の手段です。 非常に高度な筋書きの場合でのみ必要になる、ライフタイム注釈が関わる、もっと複雑な筋書きもあります。 それらについては、Rust Referenceをお読みください。 ですが次は、コードがあるべき通りに動いていることを確かめられるように、Rustでテストを書く方法を学びます。
自動テストを書く
1972年のエッセイ「謙虚なプログラマ」でエドガー・W・ダイクストラは以下のように述べています。 「プログラムのテストは、バグの存在を示すには非常に効率的な手法であるが、 バグの不在を示すには望み薄く不適切である」と。これは、できるだけテストを試みるべきではないということではありません。
プログラムの正当性は、どこまで自分のコードが意図していることをしているかなのです。 Rustは、プログラムの正当性に重きを置いて設計されていますが、 正当性は複雑で、単純に証明することはありません。Rustの型システムは、 この重荷の多くの部分を肩代わりしてくれますが、型システムはあらゆる種類の不当性を捕捉してはくれません。 ゆえに、Rustでは、言語内で自動化されたソフトウェアテストを書くことをサポートしているのです。
例として、渡された何かの数値に2を足すadd_twoという関数を書くとしましょう。
この関数のシグニチャは、引数に整数を取り、結果として整数を返します。
この関数を実装してコンパイルすると、コンパイラはこれまでに学んできた型チェックと借用チェックを全て行い、
例えば、Stringの値や無効な参照をこの関数に渡していないかなどを確かめるのです。
ところが、コンパイラはプログラマがまさしく意図したことを関数が実行しているかどうかは確かめられません。
つまり、そうですね、引数に10を足したり、50を引いたりするのではなく、引数に2を足していることです。
そんな時に、テストは必要になるのです。
例えば、add_two関数に3を渡した時に、戻り値は5であることをアサーションするようなテストを書くことができます。
コードに変更を加えた際にこれらのテストを走らせ、既存の正当な振る舞いが変わっていないことを確認できます。
テストは、複雑なスキルです: いいテストの書き方をあらゆる方面から講義することは1章だけではできないのですが、 Rustのテスト機構のメカニズムについて議論します。テストを書く際に利用可能になるアノテーションとマクロについて、 テストを実行するのに提供されているオプションと標準の動作、さらにテストをユニットテストや統合テストに体系化する方法について語ります。
テストの記述法
テストは、テスト以外のコードが想定された方法で機能していることを実証するRustの関数です。 テスト関数の本体は、典型的には以下の3つの動作を行います:
- 必要なデータや状態をセットアップする。
- テスト対象のコードを走らせる。
- 結果が想定通りであることを断定(以下、アサーションという)する。
Rustが、特にこれらの動作を行うテストを書くために用意している機能を見ていきましょう。
これには、test属性、いくつかのマクロ、should_panic属性が含まれます。
テスト関数の構成
最も単純には、Rustにおけるテストはtest属性で注釈された関数のことです。属性とは、
Rustコードの部品に関するメタデータです; 一例を挙げれば、構造体とともに第5章で使用したderive属性です。
関数をテスト関数に変えるには、fnの前に#[test]を付け加えてください。
cargo testコマンドでテストを実行したら、コンパイラはtest属性で注釈された関数を走らせるテスト用バイナリをビルドし、
各テスト関数が通過したか失敗したかを報告します。
新しいライブラリプロジェクトをCargoで作ると、テスト関数付きのテストモジュールが自動的に生成されます。 このモジュールのおかげで、新しいプロジェクトを始めるたびにテスト関数の正しい構造とか文法をいちいち検索しなくてすみます。 ここに好きな数だけテスト関数やテストモジュールを追加すればいいというわけです!
まず、実際にはコードをテストしない、自動生成されたテンプレートのテストで実験して、テストの動作の性質をいくらか学びましょう。 その後で、以前書いたコードを呼び出し、振る舞いが正しいことをアサーションする、ホンモノのテストを書きましょう。
adderという新しいライブラリプロジェクトを生成しましょう:
$ cargo new adder --lib
Created library `adder` project
$ cd adder
adderライブラリのsrc/lib.rsファイルの中身は、リスト11-1のような見た目のはずです。
ファイル名: src/lib.rs
#[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } } fn main() {}
リスト11-1: cargo newで自動生成されたテストモジュールと関数
とりあえず、最初の2行は無視し、関数に集中してその動作法を見ましょう。
fn行の#[test]注釈に注目してください: この属性は、これがテスト関数であることを示すので、
テスト実行機はこの関数をテストとして扱うとわかるのです。さらに、testsモジュール内にはテスト関数以外の関数を入れ、
一般的なシナリオをセットアップしたり、共通の処理を行う手助けをしたりもできるので、
#[test]属性でどの関数がテストかを示す必要があるのです。
関数本体は、assert_eq!マクロを使用して、2 + 2が4に等しいことをアサーションしています。
このアサーションは、典型的なテストのフォーマット例をなしているわけです。走らせてこのテストが通る(訳注:テストが成功する、の意味。英語でpassということから、このように表現される)ことを確かめましょう。
cargo testコマンドでプロジェクトにあるテストが全て実行されます。リスト11-2に示したようにですね。
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.57s
Running target/debug/deps/adder-92948b65e88960b4
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
リスト11-2: 自動生成されたテストを走らせた出力
Cargoがテストをコンパイルし、走らせました。Compiling, Finished, Runningの行の後にrunning 1 testの行があります。
次行が、生成されたテスト関数のit_worksという名前とこのテストの実行結果、okを示しています。
テスト実行の総合的なまとめが次に出現します。test result:ok.というテキストは、
全テストが通ったことを意味し、1 passed; 0 failedと読める部分は、通過または失敗したテストの数を合計しているのです。
無視すると指定したテストは何もなかったため、まとめは0 ignoredと示しています。
また、実行するテストにフィルタをかけもしなかったので、まとめの最後に0 filtered outと表示されています。
テストを無視することとフィルタすることに関しては次の節、テストの実行され方を制御するで語ります。
0 measuredという統計は、パフォーマンスを測定するベンチマークテスト用です。
ベンチマークテストは、本書記述の時点では、nightly版のRustでのみ利用可能です。
詳しくは、ベンチマークテストのドキュメンテーションを参照されたし。
テスト出力の次の部分、つまりDoc-tests adderで始まる部分は、ドキュメンテーションテストの結果用のものです。
まだドキュメンテーションテストは何もないものの、コンパイラは、APIドキュメントに現れるどんなコード例もコンパイルできます。
この機能により、ドキュメントとコードを同期することができるわけです。ドキュメンテーションテストの書き方については、
第14章のテストとしてのドキュメンテーションコメント節で議論しましょう。今は、Doc-tests出力は無視します。
テストの名前を変更してどうテスト出力が変わるか確かめましょう。it_works関数を違う名前、explorationなどに変えてください。
そう、以下のように:
ファイル名: src/lib.rs
#[cfg(test)] mod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); } } fn main() {}
そして、cargo testを再度走らせます。これで出力がit_worksの代わりにexplorationと表示しています:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.59s
Running 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
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
別のテストを追加しますが、今回は失敗するテストにしましょう!テスト関数内の何かがパニックすると、
テストは失敗します。各テストは、新規スレッドで実行され、メインスレッドが、テストスレッドが死んだと確認した時、
テストは失敗と印づけられます。第9章でパニックを引き起こす最も単純な方法について語りました。
そう、panic!マクロを呼び出すことですね。src/lib.rsファイルがリスト11-3のような見た目になるよう、
新しいテストanotherを入力してください。
ファイル名: src/lib.rs
#[cfg(test)] mod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); } #[test] fn another() { //このテストを失敗させる panic!("Make this test fail"); } } fn main() {}
リスト11-3: panic!マクロを呼び出したために失敗する2番目のテストを追加する
cargo testで再度テストを走らせてください。出力はリスト11-4のようになるはずであり、
explorationテストは通り、anotherは失敗したと表示されます。
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.72s
Running target/debug/deps/adder-92948b65e88960b4
running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok
failures:
---- tests::another stdout ----
thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9
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
error: test failed, to rerun pass '--lib'
リスト11-4: 一つのテストが通り、一つが失敗するときのテスト結果
okの代わりにtest test::anotherの行は、FAILEDを表示しています。個々の結果とまとめの間に、
2つ新たな区域ができました: 最初の区域は、失敗したテスト各々の具体的な理由を表示しています。
今回の場合、anotherは'Make this test fail'でパニックしたために失敗し、
これは、src/lib.rsファイルの10行で起きました。次の区域は失敗したテストの名前だけを列挙しています。
これは、テストがたくさんあり、失敗したテストの詳細がたくさん表示されるときに有用になります。
失敗したテストの名前を使用してそのテストだけを実行し、より簡単にデバッグすることができます。
テストの実行方法については、テストの実行され方を制御する節でもっと語りましょう。
サマリー行が最後に出力されています: 総合的に言うと、テスト結果はFAILEDでした。
一つのテストが通り、一つが失敗したわけです。
様々な状況でのテスト結果がどんな風になるか見てきたので、テストを行う際に有用になるpanic!以外のマクロに目を向けましょう。
assert!マクロで結果を確認する
assert!マクロは、標準ライブラリで提供されていますが、テスト内の何らかの条件がtrueと評価されることを確かめたいときに有効です。
assert!マクロには、論理値に評価される引数を与えます。その値がtrueなら、
assert!は何もせず、テストは通ります。その値がfalseなら、assert!マクロはpanic!マクロを呼び出し、
テストは失敗します。assert!マクロを使用することで、コードが意図した通りに機能していることを確認する助けになるわけです。
第5章のリスト5-15で、Rectangle構造体とcan_holdメソッドを使用しました。リスト11-5でもそれを繰り返しています。
このコードをsrc/lib.rsファイルに放り込み、assert!マクロでそれ用のテストを何か書いてみましょう。
ファイル名: 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 } } fn main() {}
リスト11-5: 第5章からRectangle構造体とそのcan_holdメソッドを使用する
can_holdメソッドは論理値を返すので、assert!マクロの完璧なユースケースになるわけです。
リスト11-6で、幅が8、高さが7のRectangleインスタンスを生成し、これが幅5、
高さ1の別のRectangleインスタンスを保持できるとアサーションすることでcan_holdを用いるテストを書きます。
ファイル名: 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() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(larger.can_hold(&smaller)); } } fn main() {}
リスト11-6: より大きな長方形がより小さな長方形を確かに保持できるかを確認するcan_hold用のテスト
testsモジュール内に新しい行を加えたことに注目してください: use super::*です。
testsモジュールは、第7章のモジュールツリーの要素を示すためのパス節で講義した通常の公開ルールに従う普通のモジュールです。
testsモジュールは、内部モジュールなので、外部モジュール内のテスト配下にあるコードを内部モジュールのスコープに持っていく必要があります。
ここではglobを使用して、外部モジュールで定義したもの全てがこのtestsモジュールでも使用可能になるようにしています。
テストはlarger_can_hold_smallerと名付け、必要なRectangleインスタンスを2つ生成しています。
そして、assert!マクロを呼び出し、larger.can_hold(&smaller)の呼び出し結果を渡しました。
この式は、trueを返すと考えられるので、テストは通るはずです。確かめましょう!
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running 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
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
通ります!別のテストを追加しましょう。今回は、小さい長方形は、より大きな長方形を保持できないことをアサーションします。
ファイル名: 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)); } } fn main() {}
今回の場合、can_hold関数の正しい結果はfalseなので、その結果をassert!マクロに渡す前に反転させる必要があります。
結果として、can_holdがfalseを返せば、テストは通ります。
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running 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
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
通るテストが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)); } } fn main() {}
テストを実行すると、以下のような出力をします:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running 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 'main' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:28:9
(スレッド'main'は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
error: test failed, to rerun pass '--lib'
テストによりバグが捕捉されました!larger.widthが8、smaller.widthが5なので、
can_hold内の幅の比較が今はfalseを返すようになったのです: 8は5より小さくないですからね。
assert_eq!とassert_ne!マクロで等値性をテストする
機能をテストする一般的な方法は、テスト下にあるコードの結果をコードが返すと期待される値と比較して、
等しいと確かめることです。これをassertマクロを使用して==演算子を使用した式を渡すことで行うこともできます。
しかしながら、これはありふれたテストなので、標準ライブラリには1組のマクロ(assert_eq!とassert_ne!)が提供され、
このテストをより便利に行うことができます。これらのマクロはそれぞれ、二つの引数を比べ、等しいかと等しくないかを確かめます。
また、アサーションが失敗したら二つの値の出力もし、テストが失敗した原因を確認しやすくなります。
一方でassert!マクロは、==式の値がfalseになったことしか示さず、falseになった原因の値は出力しません。
リスト11-7において、引数に2を加えて結果を返すadd_twoという名前の関数を書いています。
そして、assert_eq!マクロでこの関数をテストしています。
ファイル名: src/lib.rs
pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn it_adds_two() { assert_eq!(4, add_two(2)); } } fn main() {}
リスト11-7: assert_eq!マクロでadd_two関数をテストする
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running 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
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
assert_eq!マクロに与えた第1引数の4は、add_two(2)の呼び出し結果と等しいです。
このテストの行はtest tests::it_adds_two ... okであり、okというテキストはテストが通ったことを示しています!
コードにバグを仕込んで、assert_eq!を使ったテストが失敗した時にどんな見た目になるのか確認してみましょう。
add_two関数の実装を代わりに3を足すように変えてください:
pub fn add_two(a: i32) -> i32 { a + 3 } #[cfg(test)] mod tests { use super::*; #[test] fn it_adds_two() { assert_eq!(4, add_two(2)); } } fn main() {}
テストを再度実行します:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.61s
Running target/debug/deps/adder-92948b65e88960b4
running 1 test
test tests::it_adds_two ... FAILED
failures:
---- tests::it_adds_two stdout ----
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `4`,
right: `5`', src/lib.rs:11:9
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
error: test failed, to rerun pass '--lib'
テストがバグを捕捉しました!it_adds_twoのテストは失敗し、assertion failed: `(left == right)`というメッセージを表示し、
leftは4で、rightは5だったと示しています。このメッセージは有用で、デバッグを開始する助けになります:
assert_eq!のleft引数は4だったが、add_two(2)があるright引数は5だったことを意味しています。
二つの値が等しいとアサーションを行う関数の引数を
expectedとactualと呼び、引数を指定する順序が問題になる言語やテストフレームワークもあることに注意してください。
ですがRustでは、leftとrightと呼ばれ、期待する値とテスト下のコードが生成する値を指定する順序は
問題になりません。今回のテストのアサーションをassert_eq!(add_two(2), 4)と書くこともでき、
そうすると失敗メッセージは、assertion failed: `(left == right)`となり、
leftが5でrightが4と表示されるでしょう。
assert_ne!マクロは、与えた2つの値が等しくなければ通り、等しければ失敗します。
このマクロは、値が何になるだろうか確信が持てないけれども、コードが意図した通りに動いていれば、
確実にこの値にはならないだろうとわかっているような場合に最も有用になります。例えば、
入力を何らかの手段で変え(て出力す)ることが保証されているけれども、入力の変え方がテストを実行する曜日に依存する関数をテストしているなら、
アサーションすべき最善の事柄は、関数の出力が入力と等しくないことかもしれません。
内部的には、assert_eq!とassert_ne!マクロは、それぞれ==と!=演算子を使用しています。
アサーションが失敗すると、これらのマクロは引数をデバッグフォーマットを使用してプリントするので、
比較対象の値はPartialEqとDebugトレイトを実装していなければなりません。
すべての組み込み型と、ほぼすべての標準ライブラリの型はこれらのトレイトを実装しています。
自分で定義した構造体やenumについては、
その型の値が等しいか等しくないかをアサーションするために、PartialEqを実装する必要があるでしょう。
それが失敗した時にその値をプリントできるように、Debugを実装する必要もあるでしょう。
第5章のリスト5-12で触れたように、どちらのトレイトも導出可能なトレイトなので、
これは通常、単純に構造体やenum定義に#[derive(PartialEq, Debug)]という注釈を追加するだけですみます。
これらやその他の導出可能なトレイトに関する詳細については、付録C、導出可能なトレイトをご覧ください。
カスタムの失敗メッセージを追加する
さらに、assert!、assert_eq!、assert_ne!の追加引数として、失敗メッセージと共にカスタムのメッセージが表示されるよう、
追加することもできます。assert!の1つの必須引数の後に、
あるいはassert_eq!とassert_ne!の2つの必須引数の後に指定された引数はすべてformat!マクロに渡されるので、
(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")); } } fn main() {}
このプログラムの要件はまだ取り決められておらず、挨拶の先頭の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")); } } fn main() {}
このテストを実行すると、以下のように出力されます:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished test [unoptimized + debuginfo] target(s) in 0.91s
Running target/debug/deps/greeter-170b942eb5bf5e3a
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'assertion failed: result.contains("Carol")', src/lib.rs:12:9
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
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)は名前を含んでいません。その値は`{}`でした
"Greeting did not contain name, value was `{}`",
result
);
}
}
これでテストを実行したら、より有益なエラーメッセージが得られるでしょう:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished test [unoptimized + debuginfo] target(s) in 0.93s
Running target/debug/deps/greeter-170b942eb5bf5e3a
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9
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
error: test failed, to rerun pass '--lib'
実際に得られた値がテスト出力に表示されているので、起こると想定していたものではなく、 起こったものをデバッグするのに役に立ちます。
should_panicでパニックを確認する
期待する正しい値をコードが返すことを確認することに加えて、想定通りにコードがエラー状態を扱っていることを確認するのも重要です。
例えば、第9章のリスト9-10で生成したGuess型を考えてください。Guessを使用する他のコードは、
Guessのインスタンスは1から100の範囲の値しか含まないという保証に依存しています。
その範囲外の値でGuessインスタンスを生成しようとするとパニックすることを確認するテストを書くことができます。
これは、テスト関数にshould_panicという別の属性を追加することで達成できます。
この属性は、関数内のコードがパニックしたら、テストを通過させます。つまり、
関数内のコードがパニックしなかったら、テストは失敗するわけです。
リスト11-8は、予想どおりにGuess::newのエラー条件が発生していることを確認するテストを示しています。
ファイル名: src/lib.rs
pub struct Guess { value: i32, } impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { //予想値は1から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); } } fn main() {}
リスト11-8: 状況がpanic!を引き起こすとテストする
#[test]属性の後、適用するテスト関数の前に#[should_panic]属性を配置しています。
このテストが通るときの結果を見ましょう:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running target/debug/deps/guessing_game-57d70c3acb738f4d
running 1 test
test tests::greater_than_100 ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests guessing_game
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
よさそうですね!では、値が100より大きいときにnew関数がパニックするという条件を除去することでコードにバグを導入しましょう:
pub struct Guess { value: i32, } // --snip-- impl Guess { pub fn new(value: i32) -> Guess { if value < 1 { //予想値は1から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); } } fn main() {}
リスト11-8のテストを実行すると、失敗するでしょう:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.62s
Running target/debug/deps/guessing_game-57d70c3acb738f4d
running 1 test
test tests::greater_than_100 ... FAILED
failures:
---- tests::greater_than_100 stdout ----
note: test did not panic as expected
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--lib'
この場合、それほど役に立つメッセージは得られませんが、テスト関数に目を向ければ、
#[should_panic]で注釈されていることがわかります。得られた失敗は、
テスト関数のコードがパニックを引き起こさなかったことを意味するのです。
should_panicを使用するテストは不正確なこともあります。なぜなら、コードが何らかのパニックを起こしたことしか示さないからです。
should_panicのテストは、起きると想定していたもの以外の理由でテストがパニックしても通ってしまうのです。
should_panicのテストの正確を期すために、should_panic属性にexpected引数を追加することもできます。
このテストハーネスは、失敗メッセージに与えられたテキストが含まれていることを確かめてくれます。
例えば、リスト11-9の修正されたGuessのコードを考えてください。ここでは、
new関数は、値が大きすぎるか小さすぎるかによって異なるメッセージでパニックします。
ファイル名: src/lib.rs
pub struct Guess { value: i32, } // --snip-- impl Guess { pub fn new(value: i32) -> Guess { if value < 1 { panic!( //予想値は1以上でなければなりませんが、{}でした。 "Guess value must be greater than or equal to 1, got {}.", value ); } else if value > 100 { panic!( //予想値は100以下でなければなりませんが、{}でした。 "Guess value must be less than or equal to 100, got {}.", value ); } Guess { value } } } #[cfg(test)] mod tests { use super::*; #[test] //予想値は100以下でなければなりません #[should_panic(expected = "Guess value must be less than or equal to 100")] fn greater_than_100() { Guess::new(200); } } fn main() {}
リスト11-9: 状況が特定のパニックメッセージでpanic!を引き起こすことをテストする
should_panic属性のexpected引数に置いた値がGuess::new関数がパニックしたメッセージの一部になっているので、
このテストは通ります。予想されるパニックメッセージ全体を指定することもでき、今回の場合、
Guess value must be less than or equal to 100, got 200.となります。
should_panicの予想される引数に何を指定するかは、パニックメッセージのどこが固有でどこが動的か、
またテストをどの程度正確に行いたいかによります。今回の場合、パニックメッセージの一部でも、テスト関数内のコードが、
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!(
//予想値は100以下でなければなりませんが、{}でした。
"Guess value must be less than or equal to 100, got {}.",
value
);
} else if value > 100 {
panic!(
//予想値は1以上でなければなりませんが、{}でした。
"Guess value must be greater than or equal to 1, got {}.",
value
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Guess value must be 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 [unoptimized + debuginfo] target(s) in 0.66s
Running target/debug/deps/guessing_game-57d70c3acb738f4d
running 1 test
test tests::greater_than_100 ... FAILED
failures:
---- tests::greater_than_100 stdout ----
thread 'main' panicked at 'Guess value must be greater than or equal to 1, got 200.', src/lib.rs:13:13
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: `"Guess value must be 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
error: test failed, to rerun pass '--lib'
この失敗メッセージは、このテストが確かに予想通りパニックしたことを示していますが、
パニックメッセージは、予想される文字列の'Guess value must be less than or equal to 100'を含んでいませんでした。
実際に得られたパニックメッセージは今回の場合、Guess value must be greater than or equal to 1, got 200でした。
そうしてバグの所在地を割り出し始めることができるわけです!
Result<T, E>をテストで使う
これまでは、失敗するとパニックするようなテストを書いてきましたが、
Result<T, E>を使うようなテストを書くこともできます!
以下は、Listing 11-1のテストを、Result<T, E>を使い、パニックする代わりにErrを返すように書き直したものです:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { #[test] fn it_works() -> Result<(), String> { if 2 + 2 == 4 { Ok(()) } else { Err(String::from("two plus two does not equal four")) } } } }
it_works関数の戻り値の型はResult<(), String>になりました。
関数内でassert_eq!マクロを呼び出す代わりに、テストが成功すればOk(())を、失敗すればErrにStringを入れて返すようにします。
Result<T, E> を返すようなテストを書くと、?演算子をテストの中で使えるようになります。
これは、テスト内で何らかの工程がErrヴァリアントを返したときに失敗するべきテストを書くのに便利です。
Result<T, E>を使うテストに#[should_panic]注釈を使うことはできません。
テストが失敗しなければならないときは、直接Err値を返してください。
今やテスト記法を複数知ったので、テストを走らせる際に起きていることに目を向け、
cargo testで使用できるいろんなオプションを探究しましょう。
テストの実行のされ方を制御する
cargo runがコードをコンパイルし、出来上がったバイナリを走らせるのと全く同様に、
cargo testはコードをテストモードでコンパイルし、出来上がったテストバイナリを実行します。
コマンドラインオプションを指定してcargo testの既定動作を変更することができます。
例えば、cargo testで生成されるバイナリの既定動作は、テストを全て並行に実行し、
テスト実行中に生成された出力をキャプチャして出力が表示されるのを防ぎ、
テスト結果に関係する出力を読みやすくすることです。
コマンドラインオプションの中にはcargo testにかかるものや、出来上がったテストバイナリにかかるものがあります。
この2種の引数を区別するために、cargo testにかかる引数を--という区分記号の後に列挙し、
それからテストバイナリにかかる引数を列挙します。cargo test --helpを走らせると、cargo testで使用できるオプションが表示され、
cargo test -- --helpを走らせると、--という区分記号の後に使えるオプションが表示されます。
テストを並行または連続して実行する
複数のテストを実行するとき、標準では、スレッドを使用して並行に走ります。これはつまり、 テストが早く実行し終わり、コードが機能しているいかんにかかわらず、反応をより早く得られることを意味します。 テストは同時に実行されているので、テストが相互や共有された環境を含む他の共通の状態に依存してないことを確かめてください。 現在の作業対象ディレクトリや環境変数などですね。
例えば、各テストがディスクにtest_output.txtというファイルを作成し、何らかのデータを書き込むコードを走らせるとしてください。 そして、各テストはそのファイルのデータを読み取り、ファイルが特定の値を含んでいるとアサーションし、 その値は各テストで異なります。テストが同時に走るので、あるテストが、 他のテストが書き込んだり読み込んだりする間隙にファイルを上書きするかもしれません。 それから2番目のテストが失敗します。コードが不正だからではなく、 並行に実行されている間にテストがお互いに邪魔をしてしまったせいです。 各テストが異なるファイルに書き込むことを確かめるのが一つの解決策です; 別の解決策では、 一度に一つのテストを実行します。
並行にテストを実行したくなかったり、使用されるスレッド数をよりきめ細かく制御したい場合、
--test-threadsフラグと使用したいスレッド数をテストバイナリに送ることができます。
以下の例に目を向けてください:
$ cargo test -- --test-threads=1
テストスレッドの数を1にセットし、並行性を使用しないようにプログラムに指示しています。
1スレッドのみを使用してテストを実行すると、並行に実行するより時間がかかりますが、
状態を共有していても、お互いに邪魔をすることはありません。
関数の出力を表示する
標準では、テストが通ると、Rustのテストライブラリは標準出力に出力されたものを全てキャプチャします。例えば、
テストでprintln!を呼び出してテストが通ると、println!の出力は、端末に表示されません;
テストが通ったことを示す行しか見られないでしょう。テストが失敗すれば、
残りの失敗メッセージと共に、標準出力に出力されたものが全て見えるでしょう。
例として、リスト11-10は引数の値を出力し、10を返す馬鹿げた関数と通過するテスト1つ、失敗するテスト1つです。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { 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!(10, value); } #[test] fn this_test_will_fail() { let value = prints_and_returns_10(8); assert_eq!(5, value); } } }
リスト11-10: println!を呼び出す関数用のテスト
これらのテストをcargo testで実行すると、以下のような出力を目の当たりにするでしょう:
running 2 tests
test tests::this_test_will_pass ... ok
test tests::this_test_will_fail ... FAILED
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `10`', src/lib.rs:19:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
この出力のどこにも I got the value 4 と表示されていないことに注意してください。
これは、テストに合格した場合に出力されるものです。
その出力はキャプチャされてしまいました。
失敗したテストのからの出力 I got the value 8 がテストサマリー出力のセクションに表示され、テストが失敗した原因も示されます。
通過するテストについても出力される値が見たかったら、出力キャプチャ機能を--nocaptureフラグで無効化することができます:
$ cargo test -- --nocapture
リスト11-10のテストを--nocaptureフラグと共に再度実行したら、以下のような出力を目の当たりにします:
running 2 tests
I got the value 4
I got the value 8
test tests::this_test_will_pass ... ok
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `10`', src/lib.rs:19:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
test tests::this_test_will_fail ... FAILED
failures:
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
テスト用の出力とテスト結果の出力がまぜこぜになっていることに注意してください;
その理由は、前節で語ったようにテストが並行に実行されているからです。
-test-threads=1オプションと--nocaptureフラグを使ってみて、
その時、出力がどうなるか確かめてください!
名前でテストの一部を実行する
時々、全テストを実行すると時間がかかってしまうことがあります。特定の部分のコードしか対象にしていない場合、
そのコードに関わるテストのみを走らせたいかもしれません。cargo testに走らせたいテストの名前を引数として渡すことで、
実行するテストを選ぶことができます。
テストの一部を走らせる方法を模擬するために、リスト11-11に示したように、
add_two関数用に3つテストを作成し、走らせるテストを選択します。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn add_two_and_two() { assert_eq!(4, add_two(2)); } #[test] fn add_three_and_two() { assert_eq!(5, add_two(3)); } #[test] fn one_hundred() { assert_eq!(102, add_two(100)); } } }
リスト11-11: 異なる名前の3つのテスト
以前見かけたように、引数なしでテストを走らせたら、全テストが並行に走ります:
running 3 tests
test tests::add_two_and_two ... ok
test tests::add_three_and_two ... ok
test tests::one_hundred ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
単独のテストを走らせる
あらゆるテスト関数の名前をcargo testに渡して、そのテストのみを実行することができます:
$ cargo test one_hundred
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/adder-06a75b4a1f2515e9
running 1 test
test tests::one_hundred ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out
one_hundredという名前のテストだけが走りました; 他の2つのテストはその名前に合致しなかったのです。
まとめ行の最後に2 filtered outと表示することでテスト出力は、このコマンドが走らせた以上のテストがあることを知らせてくれています。
この方法では、複数のテストの名前を指定することはできません; cargo testに与えられた最初の値のみが使われるのです。
ですが、複数のテストを走らせる方法もあります。
複数のテストを実行するようフィルターをかける
テスト名の一部を指定でき、その値に合致するあらゆるテストが走ります。例えば、
我々のテストの2つがaddという名前を含むので、cargo test addを実行することで、その二つを走らせることができます:
$ cargo test add
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/adder-06a75b4a1f2515e9
running 2 tests
test tests::add_two_and_two ... ok
test tests::add_three_and_two ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
このコマンドは名前にaddを含むテストを全て実行し、one_hundredという名前のテストを除外しました。
また、テストが出現するモジュールがテスト名の一部になっていて、
モジュール名でフィルターをかけることで、あるモジュール内のテスト全てを実行できることに注目してください。
特に要望のない限りテストを無視する
時として、いくつかの特定のテストが実行するのに非常に時間がかかることがあり、
cargo testの実行のほとんどで除外したくなるかもしれません。引数として確かに実行したいテストを全て列挙するのではなく、
ここに示したように代わりに時間のかかるテストをignore属性で除外すると注釈することができます。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[test] fn it_works() { assert_eq!(2 + 2, 4); } #[test] #[ignore] fn expensive_test() { // 実行に1時間かかるコード // code that takes an hour to run } }
#[test]の後の除外したいテストに#[ignore]行を追加しています。これで、
テストを実行したら、it_worksは実行されるものの、expensive_testは実行されません:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.24 secs
Running target/debug/deps/adder-ce99bcc2479f4607
running 2 tests
test expensive_test ... ignored
test it_works ... ok
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
expensive_test関数は、ignoredと列挙されています。無視されるテストのみを実行したかったら、
cargo test -- --ignoredを使うことができます:
$ cargo test -- --ignored
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/adder-ce99bcc2479f4607
running 1 test
test expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
どのテストを走らせるか制御することで、結果が早く出ることを確かめることができるのです。
ignoredテストの結果を確認することが道理に合い、結果を待つだけの時間ができたときに、
代わりにcargo test -- --ignoredを走らせることができます。
テストの体系化
章の初めで触れたように、テストは複雑な鍛錬であり、人によって専門用語や体系化が異なります。 Rustのコミュニティでは、テストを2つの大きなカテゴリで捉えています: 単体テストと結合テストです。 単体テストは小規模でより集中していて、個別に1回に1モジュールをテストし、非公開のインターフェイスもテストすることがあります。 結合テストは、完全にライブラリ外になり、他の外部コード同様に自分のコードを使用し、公開インターフェイスのみ使用し、 1テストにつき複数のモジュールを用いることもあります。
どちらのテストを書くのも、ライブラリの一部が個別かつ共同でしてほしいことをしていることを確認するのに重要なのです。
単体テスト
単体テストの目的は、残りのコードから切り離して各単位のコードをテストし、
コードが想定通り、動いたり動いていなかったりする箇所を迅速に特定することです。
単体テストは、テスト対象となるコードと共に、srcディレクトリの各ファイルに置きます。
慣習は、各ファイルにtestsという名前のモジュールを作り、テスト関数を含ませ、
そのモジュールをcfg(test)で注釈することです。
テストモジュールと#[cfg(test)]
testsモジュールの#[cfg(test)]という注釈は、コンパイラにcargo buildを走らせた時ではなく、cargo testを走らせた時にだけ、
テストコードをコンパイルし走らせるよう指示します。これにより、ライブラリをビルドしたいだけの時にはコンパイルタイムを節約し、
テストが含まれないので、コンパイル後の成果物のサイズも節約します。結合テストは別のディレクトリに存在することになるので、
#[cfg(test)]注釈は必要ないとわかるでしょう。しかしながら、単体テストはコードと同じファイルに存在するので、
#[cfg(test)]を使用してコンパイル結果に含まれないよう指定するのです。
この章の最初の節で新しいadderプロジェクトを生成した時に、Cargoがこのコードも生成してくれたことを思い出してください:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } } }
このコードが自動生成されたテストモジュールです。cfgという属性は、configurationを表していて、
コンパイラに続く要素が、ある特定の設定オプションを与えられたら、含まれるように指示します。
今回の場合、設定オプションは、testであり、言語によって提供されているテストをコンパイルし、
走らせるためのものです。cfg属性を使用することで、cargo testで積極的にテストを実行した場合のみ、
Cargoがテストコードをコンパイルします。これには、このモジュールに含まれるかもしれないヘルパー関数全ても含まれ、
#[test]で注釈された関数だけにはなりません。
非公開関数をテストする
テストコミュニティ内で非公開関数を直接テストするべきかについては議論があり、
他の言語では非公開関数をテストするのは困難だったり、不可能だったりします。
あなたがどちらのテストイデオロギーを支持しているかに関わらず、Rustの公開性規則により、
非公開関数をテストすることが確かに可能です。リスト11-12の非公開関数internal_adderを含むコードを考えてください。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub fn add_two(a: i32) -> i32 { internal_adder(a, 2) } fn internal_adder(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn internal() { assert_eq!(4, internal_adder(2, 2)); } } }
リスト11-12: 非公開関数をテストする
internal_adder関数はpubとマークされていないものの、テストも単なるRustのコードであり、
testsモジュールもただのモジュールでしかないので、テスト内でinternal_adderを普通にインポートし呼び出すことができます。
非公開関数はテストするべきではないとお考えなら、Rustにはそれを強制するものは何もありません。
結合テスト
Rustにおいて、結合テストは完全にライブラリ外のものです。他のコードと全く同様にあなたのライブラリを使用するので、 ライブラリの公開APIの一部である関数しか呼び出すことはできません。その目的は、 ライブラリのいろんな部分が共同で正常に動作しているかをテストすることです。 単体では正常に動くコードも、結合した状態だと問題を孕む可能性もあるので、 結合したコードのテストの範囲も同様に重要になるのです。結合テストを作成するには、 まずtestsディレクトリが必要になります。
testsディレクトリ
プロジェクトディレクトリのトップ階層、srcの隣にtestsディレクトリを作成します。 Cargoは、このディレクトリに結合テストのファイルを探すことを把握しています。 そして、このディレクトリ内にいくらでもテストファイルを作成することができ、 Cargoはそれぞれのファイルを個別のクレートとしてコンパイルします。
結合テストを作成しましょう。リスト11-12のコードがsrc/lib.rsファイルにあるまま、 testsディレクトリを作成し、tests/integration_test.rsという名前の新しいファイルを生成し、 リスト11-13のコードを入力してください。
ファイル名: tests/integration_test.rs
extern crate adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
リスト11-13: adderクレートの関数の結合テスト
コードの頂点にextern crate adderを追記しましたが、これは単体テストでは必要なかったものです。
理由は、testsディレクトリのテストはそれぞれ個別のクレートであるため、
各々ライブラリをインポートする必要があるためです。
tests/integration_test.rsのどんなコードも#[cfg(test)]で注釈する必要はありません。
Cargoはtestsディレクトリを特別に扱い、cargo testを走らせた時にのみこのディレクトリのファイルをコンパイルするのです。
さあ、cargo testを実行してください:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
Running target/debug/deps/adder-abcabcabc
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/integration_test-ce99bcc2479f4607
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
3つの区域の出力が単体テスト、結合テスト、ドックテストを含んでいます。単体テスト用の最初の区域は、
今まで見てきたものと同じです: 各単体テストに1行(リスト11-12で追加したinternalという名前のもの)と、
単体テストのサマリー行です。
結合テストの区域は、
Running target/debug/deps/integration-test-ce99bcc2479f4607という行で始まっています(最後のハッシュはあなたの出力とは違うでしょう)。
次に、この結合テストの各テスト関数用の行があり、Doc-tests adder区域が始まる直前に、
結合テストの結果用のサマリー行があります。
単体テスト関数を追加することで単体テスト区域のテスト結果の行が増えたように、 作成した結合テストファイルにテスト関数を追加することでそのファイルの区域に結果の行が増えることになります。 結合テストファイルはそれぞれ独自の区域があるため、testsディレクトリにさらにファイルを追加すれば、 結合テストの区域が増えることになるでしょう。
それでも、テスト関数の名前を引数としてcargo testに指定することで、特定の結合テスト関数を走らせることができます。
特定の結合テストファイルにあるテストを全て走らせるには、cargo testに--test引数、
その後にファイル名を続けて使用してください:
$ cargo test --test integration_test
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/integration_test-952a27e0126bb565
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
このコマンドは、tests/integration_test.rsファイルにあるテストのみを実行します。
結合テスト内のサブモジュール
結合テストを追加するにつれて、testsディレクトリに2つ以上のファイルを作成して体系化したくなるかもしれません; 例えば、テスト対象となる機能でテスト関数をグループ化することができます。前述したように、 testsディレクトリの各ファイルは、個別のクレートとしてコンパイルされます。
各結合テストファイルをそれ自身のクレートとして扱うと、 エンドユーザがあなたのクレートを使用するかのように個別のスコープを生成するのに役立ちます。 ですが、これはtestsディレクトリのファイルが、コードをモジュールとファイルに分ける方法に関して第7章で学んだように、 srcのファイルとは同じ振る舞いを共有しないことを意味します。
testsディレクトリのファイルの異なる振る舞いは、複数の結合テストファイルで役に立ちそうなヘルパー関数ができ、
第7章の「モジュールを別のファイルに移動する」節の手順に従って共通モジュールに抽出しようとした時に最も気付きやすくなります。
例えば、tests/common.rsを作成し、そこにsetupという名前の関数を配置したら、
複数のテストファイルの複数のテスト関数から呼び出したいsetupに何らかのコードを追加することができます:
ファイル名: tests/common.rs
#![allow(unused)] fn main() { pub fn setup() { // ここにライブラリテスト固有のコードが来る // setup code specific to your library's tests would go here } }
再度テストを実行すると、common.rsファイルは何もテスト関数を含んだり、setup関数をどこかから呼んだりしてないのに、
テスト出力にcommon.rs用の区域が見えるでしょう。
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/common-b8b07b6f1be2db70
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/integration_test-d993c68b431d39df
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
commonがrunning 0 testsとテスト結果に表示されるのは、望んだ結果ではありません。
ただ単に他の結合テストファイルと何らかのコードを共有したかっただけです。
commonがテスト出力に出現するのを防ぐには、tests/common.rsを作成する代わりに、
tests/common/mod.rsを作成します。第7章の「モジュールファイルシステムの規則」節において、
module_name/mod.rsという命名規則をサブモジュールのあるモジュールのファイルに使用しました。
ここではcommonにサブモジュールはありませんが、
このように命名することでコンパイラにcommonモジュールを結合テストファイルとして扱わないように指示します。
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
extern crate adder;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}
mod common;という宣言は、リスト7-21で模擬したモジュール宣言と同じであることに注意してください。それから、テスト関数内でcommon::setup()関数を呼び出すことができます。
バイナリクレート用の結合テスト
もしもプロジェクトがsrc/main.rsファイルのみを含み、src/lib.rsファイルを持たないバイナリクレートだったら、
testsディレクトリに結合テストを作成し、
extern crateを使用してsrc/main.rsファイルに定義された関数をインポートすることはできません。
ライブラリクレートのみが、他のクレートが呼び出して使用できる関数を晒せるのです;
バイナリクレートはそれ単体で実行することを意味しています。
これは、バイナリを提供するRustのプロジェクトに、
src/lib.rsファイルに存在するロジックを呼び出す単純なsrc/main.rsファイルがある一因になっています。
この構造を使用して結合テストは、extern crateを使用して重要な機能を用いることでライブラリクレートをテストすることができます。
この重要な機能が動作すれば、src/main.rsファイルの少量のコードも動作し、その少量のコードはテストする必要がないわけです。
まとめ
Rustのテスト機能は、変更を加えた後でさえ想定通りにコードが機能し続けることを保証して、 コードが機能すべき方法を指定する手段を提供します。単体テストはライブラリの異なる部分を個別に用い、 非公開の実装詳細をテストすることができます。結合テストは、ライブラリのいろんな部分が共同で正常に動作することを確認し、 ライブラリの公開APIを使用して外部コードが使用するのと同じ方法でコードをテストします。 Rustの型システムと所有権ルールにより防がれるバグの種類もあるものの、それでもテストは、 コードが振る舞うと予想される方法に関するロジックのバグを減らすのに重要なのです。
この章と以前の章で学んだ知識を結集して、とあるプロジェクトに取り掛かりましょう!
入出力プロジェクト: コマンドラインプログラムを構築する
この章は、ここまでに学んできた多くのスキルを思い出すきっかけであり、もういくつか標準ライブラリの機能も探究します。 ファイルやコマンドラインの入出力と相互作用するコマンドラインツールを構築し、 今やあなたの支配下にあるRustの概念の一部を練習していきます。
Rustの速度、安全性、単バイナリ出力、クロスプラットフォームサポートにより、コマンドラインツールを作るのにふさわしい言語なので、
このプロジェクトでは、独自の伝統的なコマンドラインツールのgrep(globally search a regular expression
and print: 正規表現をグローバルで検索し表示する)を作成していきます。最も単純な使用法では、
grepは指定したファイルから指定した文字列を検索します。そうするには、
grepは引数としてファイル名と文字列を受け取ります。それからファイルを読み込んでそのファイル内で文字列引数を含む行を探し、
検索した行を出力するのです。
その過程で、多くのコマンドラインツールが使用している端末の機能を使用させる方法を示します。
環境変数の値を読み取ってユーザがこのツールの振る舞いを設定できるようにします。また、
標準出力(stdout)の代わりに、標準エラーに出力(stderr)するので、例えば、
ユーザはエラーメッセージは画面上で確認しつつ、成功した出力はファイルにリダイレクトできます。
Rustコミュニティのあるメンバであるアンドリュー・ガラント(Andrew Gallant)が既に全機能装備の非常に高速なgrep、
ripgrepと呼ばれるものを作成しました。比較対象として、我々のgrepはとても単純ですが、
この章により、ripgrepのような現実世界のプロジェクトを理解するのに必要な背景知識の一部を身に付けられるでしょう。
このgrepプロジェクトは、ここまでに学んできた多くの概念を集結させます:
- コードを体系化する(モジュール、第7章で学んだことを使用)
- ベクタと文字列を使用する(コレクション、第8章)
- エラーを処理する(第9章)
- 適切な箇所でトレイトとライフタイムを使用する(第10章)
- テストを記述する(第11章)
さらに、クロージャ、イテレータ、トレイトオブジェクトなど、第13章、17章で詳しく講義するものもちょっとだけ紹介します。
コマンドライン引数を受け付ける
いつものように、cargo newで新しいプロジェクトを作りましょう。プロジェクトをminigrepと名付けて、
既に自分のシステムに存在するかもしれないgrepツールと区別しましょう。
最初の仕事は、minigrepを二つの引数を受け付けるようにすることです: ファイル名と検索する文字列ですね。
つまり、cargo runで検索文字列と検索を行うファイルへのパスと共にプログラムを実行できるようになりたいということです。
こんな感じにね:
$ cargo run searchstring example-filename.txt
今現在は、cargo newで生成されたプログラムは、与えた引数を処理できません。
Crates.ioに存在する既存のライブラリには、
コマンドライン引数を受け付けるプログラムを書く手助けをしてくれるものもありますが、ちょうどこの概念を学んでいる最中なので、
この能力を自分で実装しましょう。
引数の値を読み取る
minigrepが渡したコマンドライン引数の値を読み取れるようにするために、Rustの標準ライブラリで提供されている関数が必要になり、
それは、std::env::argsです。この関数は、minigrepに与えられたコマンドライン引数のイテレータを返します。
イテレータについてはまだ議論していません(完全には第13章で講義します)が、とりあえずイテレータに関しては、
2つの詳細のみ知っていればいいです: イテレータは一連の値を生成することと、イテレータに対してcollect関数を呼び出し、
イテレータが生成する要素全部を含むベクタなどのコレクションに変えられることです。
リスト12-1のコードを使用してminigrepプログラムに渡されたあらゆるコマンドライン引数を読み取れるようにし、
それからその値をベクタとして集結させてください。
ファイル名: src/main.rs
use std::env; fn main() { let args: Vec<String> = env::args().collect(); println!("{:?}", args); }
リスト12-1: コマンドライン引数をベクタに集結させ、出力する
まず、std::envモジュールをuse文でスコープに導入したので、args関数が使用できます。
std::env::args関数は、2レベルモジュールがネストされていることに注目してください。
第7章で議論したように、希望の関数が2モジュール以上ネストされている場合、
関数ではなく親モジュールをスコープに導入するのが因習的です。そうすることで、
std::envから別の関数も容易に使用することができます。また、
use std::env::argsを追記し、関数をargsとするだけで呼び出すのに比べて曖昧でもありません。
というのも、argsは現在のモジュールに定義されている関数と容易に見間違えられるかもしれないからです。
args関数と不正なユニコード引数のどれかが不正なユニコードを含んでいたら、
std::env::argsはパニックすることに注意してください。 プログラムが不正なユニコードを含む引数を受け付ける必要があるなら、代わりにstd::env::args_osを使用してください。 この関数は、String値ではなく、OsString値を生成するイテレータを返します。ここでは、 簡潔性のためにstd::env::argsを使うことを選択しました。 なぜなら、OsString値はプラットフォームごとに異なり、String値に比べて取り扱いが煩雑だからです。
mainの最初の行でenv::argsを呼び出し、そして即座にcollectを使用して、
イテレータをイテレータが生成する値全てを含むベクタに変換しています。
collect関数を使用して多くの種類のコレクションを生成することができるので、
argsの型を明示的に注釈して文字列のベクタが欲しいのだと指定しています。Rustにおいて、
型を注釈しなければならない頻度は非常に少ないのですが、collectはよく確かに注釈が必要になる一つの関数なのです。
コンパイラには、あなたが欲しているコレクションの種類が推論できないからです。
最後に、デバッグ整形機の:?を使用してベクタを出力しています。引数なしでコードを走らせてみて、
それから引数二つで試してみましょう:
$ cargo run
--snip--
["target/debug/minigrep"]
$ cargo run needle haystack
--snip--
["target/debug/minigrep", "needle", "haystack"]
ベクタの最初の値は"target/debug/minigrep"であることに注目してください。これはバイナリの名前です。
これはCの引数リストの振る舞いと合致し、実行時に呼び出された名前をプログラムに使わせてくれるわけです。
メッセージで出力したり、プログラムを起動するのに使用されたコマンドラインエイリアスによってプログラムの振る舞いを変えたい場合に、
プログラム名にアクセスするのにしばしば便利です。ですが、この章の目的には、これを無視し、必要な二つの引数のみを保存します。
引数の値を変数に保存する
引数のベクタの値を出力すると、プログラムはコマンドライン引数として指定された値にアクセスできることが説明されました。 さて、プログラムの残りを通して使用できるように、二つの引数の値を変数に保存する必要があります。 それをしているのがリスト12-2です。
ファイル名: src/main.rs
use std::env; fn main() { let args: Vec<String> = env::args().collect(); let query = &args[1]; let filename = &args[2]; // {}を探しています println!("Searching for {}", query); // {}というファイルの中 println!("In file {}", filename); }
リスト12-2: クエリ引数とファイル名引数を保持する変数を生成
ベクタを出力した時に確認したように、プログラム名がベクタの最初の値、args[0]を占めているので、
添え字1から始めます。minigrepが取る最初の引数は、検索する文字列なので、
最初の引数への参照を変数queryに置きました。2番目の引数はファイル名でしょうから、
2番目の引数への参照は変数filenameに置きました。
一時的にこれらの変数の値を出力して、コードが意図通りに動いていることを証明しています。
再度このプログラムをtestとsample.txtという引数で実行しましょう:
$ cargo run test sample.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt
素晴らしい、プログラムは動作しています!必要な引数の値が、正しい変数に保存されています。後ほど、 何らかのエラー処理を加えて、ユーザが引数を提供しなかった場合など、可能性のある特定のエラー状況に対処します; 今は、そのような状況はないものとし、代わりにファイル読み取り能力を追加することに取り組みます。
ファイルを読み込む
では、filenameコマンドライン引数で指定されたファイルを読み込む機能を追加しましょう。
まず、テスト実行するためのサンプルファイルが必要ですね: minigrepが動作していることを確かめるために使用するのに最適なファイルは、
複数行にわたって同じ単語の繰り返しのある少量のテキストです。リスト12-3は、
うまくいくであろうエミリー・ディキンソン(Emily Dickinson)の詩です!
プロジェクトのルート階層にpoem.txtというファイルを作成し、この詩「私は誰でもない!あなたは誰?」を入力してください。
ファイル名: 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!
私は誰でもない!あなたは誰?
あなたも誰でもないの?
なら、私たちは組だね、何も言わないで!
あの人たちは、私たちを追放するでしょう。わかりますよね?
誰かでいるなんて侘しいじゃない!
カエルみたいで公すぎるじゃない。
自分の名を長い1日に告げるのなんて。
感服するような沼地にね!
リスト12-3: エミリー・ディキンソンの詩は、いいテストケースになる
テキストを適当な場所に置いて、src/main.rsを編集し、ファイルを開くコードを追加してください。 リスト12-4に示したようにですね。
ファイル名: src/main.rs
use std::env; use std::fs::File; use std::io::prelude::*; fn main() { let args: Vec<String> = env::args().collect(); let query = &args[1]; let filename = &args[2]; println!("Searching for {}", query); // --snip-- println!("In file {}", filename); // ファイルが見つかりませんでした let mut f = File::open(filename).expect("file not found"); let mut contents = String::new(); f.read_to_string(&mut contents) // ファイルの読み込み中に問題がありました .expect("something went wrong reading the file"); // テキストは\n{}です println!("With text:\n{}", contents); }
リスト12-4: 第2引数で指定されたファイルの中身を読み込む
最初に、もう何個かuse文を追記して、標準ライブラリの関係のある箇所を持ってきています:
ファイルを扱うのにstd::fs::Fileが必要ですし、
std::io::prelude::*はファイル入出力を含む入出力処理をするのに有用なトレイトを色々含んでいます。
言語が一般的な初期化処理で特定の型や関数を自動的にスコープに導入するように、
std::ioモジュールにはそれ独自の共通の型や関数の初期化処理があり、入出力を行う際に必要になるわけです。
標準の初期化処理とは異なり、std::ioの初期化処理には明示的にuse文を加えなければなりません。
mainに3文を追記しました: 一つ目が、File::open関数を呼んでfilename変数の値に渡して、
ファイルへの可変なハンドルを得る処理です。二つ目が、contentsという名の変数を生成して、
可変で空のStringを割り当てる処理です。この変数が、ファイル読み込み後に中身を保持します。
三つ目が、ファイルハンドルに対してread_to_stringを呼び出し、引数としてcontentsへの可変参照を渡す処理です。
それらの行の後に、今回もファイル読み込み後にcontentsの値を出力する一時的なprintln!文を追記したので、
ここまでプログラムがきちんと動作していることを確認できます。
第1コマンドライン引数には適当な文字列(まだ検索する箇所は実装してませんからね)を、第2引数にpoem.txtファイルを入れて、 このコードを実行しましょう:
$ cargo run the poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
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関数が複数の責任を受け持っています: 一般に、各関数がただ一つの責任だけを持つようになれば、
関数は明確かつ、管理しやすくなります。もう一つの問題点は、できうる限りのエラー処理を怠っていることです。
まだプログラムが小規模なので、これらの欠陥は大きな問題にはなりませんが、プログラムが大規模になるにつれ、
それを綺麗に解消するのは困難になっていきます。プログラムを開発する際に早い段階でリファクタリングを行うのは、
良い戦術です。リファクタリングするコードの量が少なければ、はるかに簡単になりますからね。次は、それを行いましょう。
リファクタリングしてモジュール性とエラー処理を向上させる
プログラムを改善するために、プログラムの構造と起こりうるエラーに対処する方法に関連する4つの問題を修正していきましょう。
1番目は、main関数が2つの仕事を受け持っていることです: 引数を解析し、ファイルを開いています。
このような小さな関数なら、これは、大した問題ではありませんが、main内でプログラムを巨大化させ続けたら、
main関数が扱う個別の仕事の数も増えていきます。関数が責任を受け持つごとに、
正しいことを確認しにくくなり、テストも行いづらくなり、機能を壊さずに変更するのも困難になっていきます。
機能を小分けして、各関数が1つの仕事のみに責任を持つようにするのが最善です。
この問題は、2番目の問題にも結びついています: queryとfilenameはプログラムの設定用変数ですが、
fやcontentsといった変数は、プログラムのロジックを担っています。mainが長くなるほど、
スコープに入れるべき変数も増えます。そして、スコープにある変数が増えれば、各々の目的を追うのも大変になるわけです。
設定用変数を一つの構造に押し込め、目的を明瞭化するのが最善です。
3番目の問題は、ファイルを開き損ねた時にexpectを使ってエラーメッセージを出力しているのに、
エラーメッセージがファイルが見つかりませんでしたとしか表示しないことです。
ファイルを開く行為は、ファイルが存在しない以外にもいろんな方法で失敗することがあります:
例えば、ファイルは存在するかもしれないけれど、開く権限がないかもしれないなどです。
現時点では、そのような状況になった時、「ファイルが見つかりませんでした」というエラーメッセージを出力し、
これはユーザに間違った情報を与えるのです。
4番目は、異なるエラーを処理するのにexpectを繰り返し使用しているので、ユーザが十分な数の引数を渡さずにプログラムを起動した時に、
問題を明確に説明しない「範囲外アクセス(index out of bounds)」というエラーがRustから得られることです。
エラー処理のコードが全て1箇所に存在し、将来エラー処理ロジックが変更になった時に、
メンテナンス者が1箇所のコードのみを考慮すればいいようにするのが最善でしょう。
エラー処理コードが1箇所にあれば、エンドユーザにとって意味のあるメッセージを出力していることを確認することにもつながります。
プロジェクトをリファクタリングして、これら4つの問題を扱いましょう。
バイナリプロジェクトの責任の分離
main関数に複数の仕事の責任を割り当てるという構造上の問題は、多くのバイナリプロジェクトでありふれています。
結果として、mainが肥大化し始めた際にバイナリプログラムの個別の責任を分割するためにガイドラインとして活用できる工程をRustコミュニティは、
開発しました。この工程は、以下のような手順になっています:
- プログラムをmain.rsとlib.rsに分け、ロジックをlib.rsに移動する。
- コマンドライン引数の解析ロジックが小規模な限り、main.rsに置いても良い。
- コマンドライン引数の解析ロジックが複雑化の様相を呈し始めたら、main.rsから抽出してlib.rsに移動する。
この工程の後にmain関数に残る責任は以下に限定される:
- 引数の値でコマンドライン引数の解析ロジックを呼び出す
- 他のあらゆる設定を行う
- lib.rsの
run関数を呼び出す runがエラーを返した時に処理する
このパターンは、責任の分離についてです: main.rsはプログラムの実行を行い、
そして、lib.rsが手にある仕事のロジック全てを扱います。main関数を直接テストすることはできないので、
この構造により、プログラムのロジック全てをlib.rsの関数に移すことでテストできるようになります。
main.rsに残る唯一のコードは、読めばその正当性が評価できるだけ小規模になるでしょう。
この工程に従って、プログラムのやり直しをしましょう。
引数解析器を抽出する
引数解析の機能をmainが呼び出す関数に抽出して、コマンドライン引数解析ロジックをsrc/lib.rsに移動する準備をします。
リスト12-5に新しい関数parse_configを呼び出すmainの冒頭部を示し、
この新しい関数は今だけsrc/main.rsに定義します。
ファイル名: src/main.rs
fn main() {
let args: Vec<String> = env::args().collect();
let (query, filename) = parse_config(&args);
// --snip--
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let filename = &args[2];
(query, filename)
}
リスト12-5: mainからparse_config関数を抽出する
それでもまだ、コマンドライン引数をベクタに集結させていますが、main関数内で引数の値の添え字1を変数queryに、
添え字2を変数filenameに代入する代わりに、ベクタ全体をparse_config関数に渡しています。
そして、parse_config関数にはどの引数がどの変数に入り、それらの値をmainに返すというロジックが存在します。
まだmain内にqueryとfilenameという変数を生成していますが、もうmainは、
コマンドライン引数と変数がどう対応するかを決定する責任は持ちません。
このやり直しは、私たちの小規模なプログラムにはやりすぎに思えるかもしれませんが、 少しずつ段階的にリファクタリングしているのです。この変更後、プログラムを再度実行して、 引数解析がまだ動作していることを実証してください。問題が発生した時に原因を特定する助けにするために頻繁に進捗を確認するのはいいことです。
設定値をまとめる
もう少しparse_config関数を改善することができます。現時点では、タプルを返していますが、
即座にタプルを分解して再度個別の値にしています。これは、正しい抽象化をまだできていないかもしれない兆候です。
まだ改善の余地があると示してくれる他の徴候は、parse_configのconfigの部分であり、
返却している二つの値は関係があり、一つの設定値の一部にどちらもなることを暗示しています。
現状では、一つのタプルにまとめていること以外、この意味をデータの構造に載せていません;
この二つの値を1構造体に置き換え、構造体のフィールドそれぞれに意味のある名前をつけることもできるでしょう。
そうすることで将来このコードのメンテナンス者が、異なる値が相互に関係する仕方や、目的を理解しやすくできるでしょう。
注釈: この複雑型(complex type)がより適切な時に組み込みの値を使うアンチパターンを、 primitive obsession(
訳注: 初めて聞いた表現。組み込み型強迫観念といったところだろうか)と呼ぶ人もいます。
リスト12-6は、parse_config関数の改善を示しています。
ファイル名: src/main.rs
use std::env; use std::fs::File; fn main() { let args: Vec<String> = env::args().collect(); let config = parse_config(&args); println!("Searching for {}", config.query); println!("In file {}", config.filename); let mut f = File::open(config.filename).expect("file not found"); // --snip-- } struct Config { query: String, filename: String, } fn parse_config(args: &[String]) -> Config { let query = args[1].clone(); let filename = args[2].clone(); Config { query, filename } }
リスト12-6: parse_configをリファクタリングしてConfig構造体のインスタンスを返す
queryとfilenameというフィールドを持つよう定義されたConfigという構造体を追加しました。
parse_configのシグニチャは、これでConfig値を返すと示すようになりました。parse_configの本体では、
以前はargsのString値を参照する文字列スライスを返していましたが、
今では所有するString値を含むようにConfigを定義しています。mainのargs変数は引数値の所有者であり、
parse_config関数だけに借用させていますが、これはConfigがargsの値の所有権を奪おうとしたら、
Rustの借用規則に違反してしまうことを意味します。
Stringのデータは、多くの異なる手法で管理できますが、最も単純だけれどもどこか非効率的な手段は、
値に対してcloneメソッドを呼び出すことです。これにより、Configインスタンスが所有するデータの総コピーが生成されるので、
文字列データへの参照を保持するよりも時間とメモリを消費します。ですが、データをクローンすることで、
コードがとても素直にもなります。というのも、参照のライフタイムを管理する必要がないからです。
つまり、この場面において、少々のパフォーマンスを犠牲にして単純性を得るのは、価値のある代償です。
cloneを使用する代償実行時コストのために
cloneを使用して所有権問題を解消するのを避ける傾向が多くのRustaceanにあります。 第13章で、この種の状況においてより効率的なメソッドの使用法を学ぶでしょう。ですがとりあえずは、 これらのコピーをするのは1回だけですし、ファイル名とクエリ文字列は非常に小さなものなので、 いくつかの文字列をコピーして進捗するのは良しとしましょう。最初の通り道でコードを究極的に効率化しようとするよりも、 ちょっと非効率的でも動くプログラムを用意する方がいいでしょう。もっとRustの経験を積めば、 最も効率的な解決法から開始することも簡単になるでしょうが、今は、cloneを呼び出すことは完璧に受け入れられることです。
mainを更新したので、parse_configから返されたConfigのインスタンスをconfigという変数に置くようになり、
以前は個別のqueryとfilename変数を使用していたコードを更新したので、代わりにConfig構造体のフィールドを使用するようになりました。
これでコードはqueryとfilenameが関連していることと、その目的がプログラムの振る舞い方を設定するということをより明確に伝えます。
これらの値を使用するあらゆるコードは、configインスタンスの目的の名前を冠したフィールドにそれらを発見することを把握しています。
Configのコンストラクタを作成する
ここまでで、コマンドライン引数を解析する責任を負ったロジックをmainから抽出し、parse_config関数に配置しました。
そうすることでqueryとfilenameの値が関連し、その関係性がコードに載っていることを確認する助けになりました。
それからConfig構造体を追加してqueryとfilenameの関係する目的を名前付けし、
構造体のフィールド名としてparse_config関数からその値の名前を返すことができています。
したがって、今やparse_config関数の目的はConfigインスタンスを生成することになったので、
parse_configをただの関数からConfig構造体に紐づくnewという関数に変えることができます。
この変更を行うことで、コードがより慣用的になります。Stringなどの標準ライブラリの型のインスタンスを、
String::newを呼び出すことで生成できます。同様に、parse_configをConfigに紐づくnew関数に変えれば、
Config::newを呼び出すことでConfigのインスタンスを生成できるようになります。リスト12-7が、
行う必要のある変更を示しています。
ファイル名: src/main.rs
use std::env; fn main() { let args: Vec<String> = env::args().collect(); let config = Config::new(&args); // --snip-- } struct Config { query: String, filename: String, } // --snip-- impl Config { fn new(args: &[String]) -> Config { let query = args[1].clone(); let filename = args[2].clone(); Config { query, filename } } }
リスト12-7: parse_configをConfig::newに変える
parse_configを呼び出していたmainを代わりにConfig::newを呼び出すように更新しました。
parse_configの名前をnewに変え、implブロックに入れ込んだので、new関数とConfigが紐づくようになりました。
再度このコードをコンパイルしてみて、動作することを確かめてください。
エラー処理を修正する
さて、エラー処理の修正に取り掛かりましょう。ベクタが2個以下の要素しか含んでいないときにargsベクタの添え字1か2にアクセスしようとすると、
プログラムがパニックすることを思い出してください。試しに引数なしでプログラムを実行してください。すると、こんな感じになります:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep`
thread 'main' panicked at 'index out of bounds: the len is 1
but the index is 1', src/main.rs:29:21
(スレッド'main'は、「境界外アクセス: 長さは1なのに添え字も1です」でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.
境界外アクセス: 長さは1なのに添え字も1ですという行は、プログラマ向けのエラーメッセージです。
エンドユーザが起きたことと代わりにすべきことを理解する手助けにはならないでしょう。これを今修正しましょう。
エラーメッセージを改善する
リスト12-8で、new関数に、添え字1と2にアクセスする前にスライスが十分長いことを実証するチェックを追加しています。
スライスの長さが十分でなければ、プログラムはパニックし、境界外インデックスよりもいいエラーメッセージを表示します。
ファイル名: src/main.rs
// --snip--
fn new(args: &[String]) -> Config {
if args.len() < 3 {
// 引数の数が足りません
panic!("not enough arguments");
}
// --snip--
リスト12-8: 引数の数のチェックを追加する
このコードは、リスト9-9で記述したvalue引数が正常な値の範囲外だった時にpanic!を呼び出したGuess::new関数と似ています。
ここでは、値の範囲を確かめる代わりに、argsの長さが少なくとも3であることを確かめていて、
関数の残りの部分は、この条件が満たされているという前提のもとで処理を行うことができます。
argsに2要素以下しかなければ、この条件は真になり、panic!マクロを呼び出して、即座にプログラムを終了させます。
では、newのこの追加の数行がある状態で、再度引数なしでプログラムを走らせ、エラーがどんな見た目か確かめましょう:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep`
thread 'main' panicked at 'not enough arguments', src/main.rs:30:12
(スレッド'main'は「引数が足りません」でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.
この出力の方がマシです: これでエラーメッセージが合理的になりました。ですが、
ユーザに与えたくない追加の情報も含まれてしまっています。おそらく、
ここではリスト9-9で使用したテクニックを使用するのは最善ではありません:
panic!の呼び出しは、第9章で議論したように、使用の問題よりもプログラミング上の問題により適しています。
代わりに、第9章で学んだもう一つのテクニックを使用することができます。成功か失敗かを示唆するResultを返すことです。
panic!を呼び出す代わりにnewからResultを返す
代わりに、成功時にはConfigインスタンスを含み、エラー時には問題に言及するResult値を返すことができます。
Config::newがmainと対話する時、Result型を使用して問題があったと信号を送ることができます。
それからmainを変更して、panic!呼び出しが引き起こしていたthread 'main'とRUST_BACKTRACEに関する周囲のテキストがない、
ユーザ向けのより実用的なエラーにErr列挙子を変換することができます。
リスト12-9は、Config::newの戻り値に必要な変更とResultを返すのに必要な関数の本体を示しています。
mainも更新するまで、これはコンパイルできないことに注意してください。その更新は次のリストで行います。
ファイル名: src/main.rs
impl Config {
fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config { query, filename })
}
}
リスト12-9: Config::newからResultを返却する
new関数は、これで、成功時にはConfigインスタンスを、エラー時には&'static strを伴うResultを返すようになりました。
第10章の「静的ライフタイム」節から&'static strは文字列リテラルの型であることを思い出してください。
これは、今はエラーメッセージの型になっています。
new関数の本体で2つ変更を行いました: 十分な数の引数をユーザが渡さなかった場合にpanic!を呼び出す代わりに、
今はErr値を返し、Config戻り値をOkに包んでいます。これらの変更により、関数が新しい型シグニチャに適合するわけです。
Config::newからErr値を返すことにより、main関数は、new関数から返ってくるResult値を処理し、
エラー時により綺麗にプロセスから抜け出すことができます。
Config::newを呼び出し、エラーを処理する
エラーケースを処理し、ユーザフレンドリーなメッセージを出力するために、mainを更新して、
リスト12-10に示したようにConfig::newから返されているResultを処理する必要があります。
また、panic!からコマンドラインツールを0以外のエラーコードで抜け出す責任も奪い取り、
手作業でそれも実装します。0以外の終了コードは、
我々のプログラムを呼び出したプロセスにプログラムがエラー状態で終了したことを通知する慣習です。
ファイル名: src/main.rs
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
// 引数解析時に問題
println!("Problem parsing arguments: {}", err);
process::exit(1);
});
// --snip--
リスト12-10: 新しいConfig作成に失敗したら、エラーコードで終了する
このリストにおいて、以前には講義していないメソッドを使用しました: unwrap_or_elseです。
これは標準ライブラリでResult<T, E>に定義されています。unwrap_or_elseを使うことで、
panic!ではない何らか独自のエラー処理を定義できるのです。このResultがOk値だったら、
このメソッドの振る舞いはunwrapに似ています: Okが包んでいる中身の値を返すのです。
しかし、値がErr値なら、このメソッドは、クロージャ内でコードを呼び出し、
クロージャは私たちが定義し、引数としてunwrap_or_elseに渡す匿名関数です。クロージャについては第13章で詳しく講義します。
とりあえず、unwrap_or_elseは、今回リスト12-9で追加したnot enough argumentsという静的文字列のErrの中身を、
縦棒の間に出現するerr引数のクロージャに渡していることだけ知っておく必要があります。
クロージャのコードはそれから、実行された時にerr値を使用できます。
新規use行を追加して標準ライブラリからprocessをインポートしました。クロージャ内のエラー時に走るコードは、
たった2行です: errの値を出力し、それからprocess::exitを呼び出します。process::exit関数は、
即座にプログラムを停止させ、渡された数字を終了コードとして返します。これは、リスト12-8で使用したpanic!ベースの処理と似ていますが、
もう余計な出力はされません。試しましょう:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.48 secs
Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments
素晴らしい!この出力の方が遥かにユーザに優しいです。
mainからロジックを抽出する
これで設定解析のリファクタリングが終了したので、プログラムのロジックに目を向けましょう。
「バイナリプロジェクトの責任の分離」で述べたように、
現在main関数に存在する設定のセットアップやエラー処理に関わらない全てのロジックを保持することになるrunという関数を抽出します。
やり終わったら、mainは簡潔かつ視察で確かめやすくなり、他のロジック全部に対してテストを書くことができるでしょう。
リスト12-11は、抜き出したrun関数を示しています。今は少しずつ段階的に関数を抽出する改善を行っています。
それでも、src/main.rsに関数を定義していきます。
ファイル名: src/main.rs
fn main() {
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
run(config);
}
fn run(config: Config) {
let mut f = File::open(config.filename).expect("file not found");
let mut contents = String::new();
f.read_to_string(&mut contents)
.expect("something went wrong reading the file");
println!("With text:\n{}", contents);
}
// --snip--
リスト12-11: 残りのプログラムロジックを含むrun関数を抽出する
これでrun関数は、ファイル読み込みから始まるmain関数の残りのロジック全てを含むようになりました。
このrun関数は、引数にConfigインスタンスを取ります。
run関数からエラーを返す
残りのプログラムロジックがrun関数に隔離されたので、リスト12-9のConfig::newのように、
エラー処理を改善することができます。expectを呼び出してプログラムにパニックさせる代わりに、
run関数は、何か問題が起きた時にResult<T, E>を返します。これにより、
さらにエラー処理周りのロジックをユーザに優しい形でmainに統合することができます。
リスト12-12にシグニチャとrun本体に必要な変更を示しています。
ファイル名: src/main.rs
use std::error::Error;
// --snip--
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
println!("With text:\n{}", contents);
Ok(())
}
リスト12-12: run関数を変更してResultを返す
ここでは、3つの大きな変更を行いました。まず、run関数の戻り値をResult<(), Box<dyn Error>>に変えました。
この関数は、以前はユニット型、()を返していて、それをOkの場合に返される値として残しました。
エラー型については、トレイトオブジェクトのBox<dyn Error>を使用しました(同時に冒頭でuse文により、
std::error::Errorをスコープに導入しています)。トレイトオブジェクトについては、第17章で講義します。
とりあえず、Box<dyn Error>は、関数がErrorトレイトを実装する型を返すことを意味しますが、
戻り値の型を具体的に指定しなくても良いことを知っておいてください。これにより、
エラーケースによって異なる型のエラー値を返す柔軟性を得ます。dyn キーワードは、"dynamic"の略です。
2番目に、expectの呼び出しよりも?演算子を選択して取り除きました。第9章で語りましたね。
エラーでパニックするのではなく、?演算子は呼び出し元が処理できるように、現在の関数からエラー値を返します。
3番目に、run関数は今、成功時にOk値を返すようになりました。run関数の成功型は、
シグニチャで()と定義したので、ユニット型の値をOk値に包む必要があります。
最初は、このOk(())という記法は奇妙に見えるかもしれませんが、このように()を使うことは、
runを副作用のためだけに呼び出していると示唆する慣習的な方法です; 必要な値は返しません。
このコードを実行すると、コンパイルは通るものの、警告が表示されるでしょう:
warning: unused `std::result::Result` which must be used
(警告: 使用されなければならない`std::result::Result`が未使用です)
--> src/main.rs:18:5
|
18 | run(config);
| ^^^^^^^^^^^^
= note: #[warn(unused_must_use)] on by default
コンパイラは、コードがResult値を無視していると教えてくれて、このResult値は、
エラーが発生したと示唆しているかもしれません。しかし、エラーがあったか確認するつもりはありませんが、
コンパイラは、ここにエラー処理コードを書くつもりだったんじゃないかと思い出させてくれています!
今、その問題を改修しましょう。
mainでrunから返ってきたエラーを処理する
リスト12-10のConfig::newに対して行った方法に似たテクニックを使用してエラーを確認し、扱いますが、
少し違いがあります:
ファイル名: src/main.rs
fn main() {
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
if let Err(e) = run(config) {
println!("Application error: {}", e);
process::exit(1);
}
}
unwrap_or_elseではなく、if letでrunがErr値を返したかどうかを確認し、そうならprocess::exit(1)を呼び出しています。
run関数は、Config::newがConfigインスタンスを返すのと同じようにunwrapしたい値を返すことはありません。
runは成功時に()を返すので、エラーを検知することにのみ興味があり、()でしかないので、
unwrap_or_elseに包まれた値を返してもらう必要はないのです。
if letとunwrap_or_else関数の中身はどちらも同じです: エラーを出力して終了します。
コードをライブラリクレートに分割する
ここまでminigrepは良さそうですね!では、テストを行え、src/main.rsファイルの責任が減らせるように、
src/main.rsファイルを分割し、一部のコードをsrc/lib.rsファイルに置きましょう。
main関数以外のコード全部をsrc/main.rsからsrc/lib.rsに移動しましょう:
run関数定義- 関係する
use文 Configの定義Config::new関数定義
src/lib.rsの中身にはリスト12-13に示したようなシグニチャがあるはずです(関数の本体は簡潔性のために省略しました)。 リスト12-14でsrc/main.rsに変更を加えるまで、このコードはコンパイルできないことに注意してください。
ファイル名: src/lib.rs
use std::error::Error;
use std::fs::File;
use std::io::prelude::*;
pub struct Config {
pub query: String,
pub filename: String,
}
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
// --snip--
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
// --snip--
}
リスト12-13: Configとrunをsrc/lib.rsに移動する
ここでは、寛大にpubを使用しています: Configのフィールドとnewメソッドとrun関数です。
これでテスト可能な公開APIのあるライブラリクレートができました!
さて、src/lib.rsに移動したコードをsrc/main.rsのバイナリクレートのスコープに持っていく必要があります。 リスト12-14に示したようにですね。
ファイル名: src/main.rs
extern crate minigrep;
use std::env;
use std::process;
use minigrep::Config;
fn main() {
// --snip--
if let Err(e) = minigrep::run(config) {
// --snip--
}
}
リスト12-14: minigrepクレートをsrc/main.rsのスコープに持っていく
ライブラリクレートをバイナリクレートに持っていくのに、extern crate minigrepを使用しています。
それからuse minigrep::Config行を追加してConfig型をスコープに持ってきて、
run関数にクレート名を接頭辞として付けます。これで全機能が連結され、動くはずです。
cargo runでプログラムを走らせて、すべてがうまくいっていることを確かめてください。
ふう!作業量が多かったですね。ですが、将来成功する準備はできています。 もう、エラー処理は遥かに楽になり、コードのモジュール化もできました。 ここから先の作業は、ほぼsrc/lib.rsで完結するでしょう。
古いコードでは大変だけれども、新しいコードでは楽なことをして新発見のモジュール性を活用しましょう: テストを書くのです!
テスト駆動開発でライブラリの機能を開発する
今や、ロジックをsrc/lib.rsに抜き出し、引数集めとエラー処理をsrc/main.rsに残したので、
コードの核となる機能のテストを書くのが非常に容易になりました。いろんな引数で関数を直接呼び出し、
コマンドラインからバイナリを呼び出す必要なく戻り値を確認できます。ご自由にConfig::newやrun関数の機能のテストは、
ご自身でお書きください。
この節では、テスト駆動開発(TDD)過程を活用してminigrepプログラムに検索ロジックを追加します。
このソフトウェア開発テクニックは、以下の手順に従います:
- 失敗するテストを書き、走らせて想定通りの理由で失敗することを確かめる。
- 十分な量のコードを書くか変更して新しいテストを通過するようにする。
- 追加または変更したばかりのコードをリファクタリングし、テストが通り続けることを確認する。
- 手順1から繰り返す!
この過程は、ソフトウェアを書く多くの方法のうちの一つに過ぎませんが、TDDによりコードデザインも駆動することができます。 テストを通過させるコードを書く前にテストを書くことで、過程を通して高いテストカバー率を保つ助けになります。
実際にクエリ文字列の検索を行う機能の実装をテスト駆動し、クエリに合致する行のリストを生成します。
この機能をsearchという関数に追加しましょう。
失敗するテストを記述する
もう必要ないので、プログラムの振る舞いを確認していたprintln!文をsrc/lib.rsとsrc/main.rsから削除しましょう。
それからsrc/lib.rsで、テスト関数のあるtestモジュールを追加します。第11章のようにですね。
このテスト関数がsearch関数に欲しい振る舞いを指定します: クエリとそれを検索するテキストを受け取り、
クエリを含む行だけをテキストから返します。リスト12-15にこのテストを示していますが、まだコンパイルは通りません。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } #[cfg(test)] mod test { use super::*; #[test] fn one_result() { let query = "duct"; // Rustは // 安全で速く生産性も高い。 // 3つ選んで。 let contents = "\ Rust: safe, fast, productive. Pick three."; assert_eq!( vec!["safe, fast, productive."], search(query, contents) ); } } }
リスト12-15: こうだったらいいなというsearch関数の失敗するテストを作成する
このテストは、"duct"という文字列を検索します。検索対象の文字列は3行で、うち1行だけが"duct"を含みます。
search関数から返る値が想定している行だけを含むことをアサーションします。
このテストを走らせ、失敗するところを観察することはできません。このテストはコンパイルもできないからです:
まだsearch関数が存在していません!ゆえに今度は、空のベクタを常に返すsearch関数の定義を追加することで、
テストをコンパイルし走らせるだけのコードを追記します。リスト12-16に示したようにですね。そうすれば、
テストはコンパイルでき、失敗するはずです。なぜなら、空のベクタは、
"safe, fast, productive."という行を含むベクタとは合致しないからです。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } }
リスト12-16: テストがコンパイルできるのに十分なだけsearch関数を定義する
明示的なライフタイムの'aがsearchのシグニチャで定義され、contents引数と戻り値で使用されていることに注目してください。
第10章からライフタイム仮引数は、どの実引数のライフタイムが戻り値のライフタイムに関連づけられているかを指定することを思い出してください。
この場合、返却されるベクタは、
(query引数ではなく)contents引数のスライスを参照する文字列スライスを含むべきと示唆しています。
言い換えると、コンパイラにsearch関数に返されるデータは、
search関数にcontents引数で渡されているデータと同期間生きることを教えています。
これは重要なことです!スライスに参照されるデータは、参照が有効になるために有効である必要があるのです;
コンパイラがcontentsではなくqueryの文字列スライスを生成すると想定してしまったら、
安全性チェックを間違って行うことになってしまいます。
ライフタイム注釈を忘れてこの関数をコンパイルしようとすると、こんなエラーが出ます:
error[E0106]: missing lifetime specifier
(エラー: ライフタイム指定子が欠けています)
--> src/lib.rs:5:51
|
5 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
| ^ expected 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`
(助言: この関数の戻り値は、借用された値を含んでいますが、シグニチャにはそれが、
`query`か`contents`から借用されたものであるかが示されていません)
コンパイラには、二つの引数のどちらが必要なのか知る由がないので、教えてあげる必要があるのです。
contentsがテキストを全て含む引数で、合致するそのテキストの一部を返したいので、
contentsがライフタイム記法で戻り値に関連づくはずの引数であることをプログラマは知っています。
他のプログラミング言語では、シグニチャで引数と戻り値を関連づける必要はありません。これは奇妙に思えるかもしれませんが、 時間とともに楽になっていきます。この例を第10章、「ライフタイムで参照を有効化する」節と比較したくなるかもしれません。
さあ、テストを実行しましょう:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
--warnings--
Finished dev [unoptimized + debuginfo] target(s) in 0.43 secs
Running target/debug/deps/minigrep-abcabcabc
running 1 test
test test::one_result ... FAILED
failures:
---- test::one_result stdout ----
thread 'test::one_result' panicked at 'assertion failed: `(left ==
right)`
left: `["safe, fast, productive."]`,
right: `[]`)', src/lib.rs:48:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
test::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--lib'
素晴らしい。テストは全く想定通りに失敗しています。テストが通るようにしましょう!
テストを通過させるコードを書く
空のベクタを常に返しているために、現状テストは失敗しています。それを修正し、searchを実装するには、
プログラムは以下の手順に従う必要があります:
- 中身を各行ごとに繰り返す。
- 行にクエリ文字列が含まれるか確認する。
- するなら、それを返却する値のリストに追加する。
- しないなら、何もしない。
- 一致する結果のリストを返す。
各行を繰り返す作業から、この手順に順に取り掛かりましょう。
linesメソッドで各行を繰り返す
Rustには、文字列を行ごとに繰り返す役立つメソッドがあり、利便性のためにlinesと名付けられ、
リスト12-17のように動作します。まだ、これはコンパイルできないことに注意してください。
ファイル名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// 行に対して何かする
// do something with line
}
}
リスト12-17: contentsの各行を繰り返す
linesメソッドはイテレータを返します。イテレータについて詳しくは、第13章で話しますが、
リスト3-5でこのようなイテレータの使用法は見かけたことを思い出してください。
そこでは、イテレータにforループを使用してコレクションの各要素に対して何らかのコードを走らせていました。
クエリを求めて各行を検索する
次に現在の行がクエリ文字列を含むか確認します。幸運なことに、
文字列にはこれを行ってくれるcontainsという役に立つメソッドがあります!search関数に、
containsメソッドの呼び出しを追加してください。リスト12-18のようにですね。
それでもまだコンパイルできないことに注意してください。
ファイル名: src/lib.rs
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
}
}
}
リスト12-18: 行がqueryの文字列を含むか確認する機能を追加する
合致した行を保存する
また、クエリ文字列を含む行を保存する方法が必要です。そのために、forループの前に可変なベクタを生成し、
pushメソッドを呼び出してlineをベクタに保存することができます。forループの後でベクタを返却します。
リスト12-19のようにですね。
ファイル名: src/lib.rs
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
}
リスト12-19: 合致する行を保存したので、返すことができる
これでsearch関数は、queryを含む行だけを返すはずであり、テストも通るはずです。
テストを実行しましょう:
$ cargo test
--snip--
running 1 test
test test::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
テストが通り、動いていることがわかりました!
ここで、テストが通過するよう保ったまま、同じ機能を保持しながら、検索関数の実装をリファクタリングする機会を考えることもできます。 検索関数のコードは悪すぎるわけではありませんが、イテレータの有用な機能の一部を活用していません。 この例には第13章で再度触れ、そこでは、イテレータをより深く探究し、さらに改善する方法に目を向けます。
run関数内でsearch関数を使用する
search関数が動きテストできたので、run関数からsearchを呼び出す必要があります。config.queryの値と、
ファイルからrunが読み込むcontentsの値をsearch関数に渡す必要があります。
それからrunは、searchから返ってきた各行を出力するでしょう:
ファイル名: src/lib.rs
pub fn run(config: Config) -> Result<(), Box<Error>> {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
for line in search(&config.query, &contents) {
println!("{}", line);
}
Ok(())
}
それでもforループでsearchから各行を返し、出力しています。
さて、プログラム全体が動くはずです!試してみましょう。まずはエミリー・ディキンソンの詩から、 ちょうど1行だけを返すはずの言葉から。"frog"です:
$ cargo run frog poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.38 secs
Running `target/debug/minigrep frog poem.txt`
How public, like a frog
かっこいい!今度は、複数行にマッチするであろう言葉を試しましょう。"body"とかね:
$ cargo run body poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
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
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep monomorphization poem.txt`
最高です!古典的なツールの独自のミニバージョンを構築し、アプリケーションを構造化する方法を多く学びました。 また、ファイル入出力、ライフタイム、テスト、コマンドライン引数の解析についても、少し学びました。
このプロジェクトをまとめ上げるために、環境変数を扱う方法と標準エラー出力に出力する方法を少しだけデモします。 これらはどちらも、コマンドラインプログラムを書く際に有用です。
環境変数を取り扱う
おまけの機能を追加してminigrepを改善します: 環境変数でユーザがオンにできる大文字小文字無視の検索用のオプションです。
この機能をコマンドラインオプションにして、適用したい度にユーザが入力しなければならないようにすることもできますが、
代わりに環境変数を使用します。そうすることでユーザは1回環境変数をセットすれば、そのターミナルセッションの間は、
大文字小文字無視の検索を行うことができるようになるわけです。
大文字小文字を区別しないsearch関数用に失敗するテストを書く
環境変数がオンの場合に呼び出すsearch_case_insensitive関数を新しく追加したいです。テスト駆動開発の過程に従い続けるので、
最初の手順は、今回も失敗するテストを書くことです。新しいsearch_case_insensitive関数用の新規テストを追加し、
古いテストをone_resultからcase_sensitiveに名前変更して、二つのテストの差異を明確化します。
リスト12-20に示したようにですね。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[cfg(test)] mod test { use super::*; #[test] fn case_sensitive() { let query = "duct"; // Rust // 安全かつ高速で生産的 // 三つを選んで // ガムテープ 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) ); } } }
リスト12-20: 追加しようとしている大文字小文字を区別しない関数用の失敗するテストを新しく追加する
古いテストのcontentsも変更していることに注意してください。大文字小文字を区別する検索を行う際に、
"duct"というクエリに合致しないはずの大文字Dを使用した"Duct tape"(ガムテープ)という新しい行を追加しました。
このように古いテストを変更することで、既に実装済みの大文字小文字を区別する検索機能を誤って壊してしまわないことを保証する助けになります。
このテストはもう通り、大文字小文字を区別しない検索に取り掛かっても通り続けるはずです。
大文字小文字を区別しない検索の新しいテストは、クエリに"rUsT"を使用しています。
追加直前のsearch_case_insensitive関数では、"rUsT"というクエリは、
両方ともクエリとは大文字小文字が異なるのに、大文字Rの"Rust:"を含む行と、
“Trust me.”という行にもマッチするはずです。これが失敗するテストであり、まだsearch_case_insensitive関数を定義していないので、
コンパイルは失敗するでしょう。リスト12-16のsearch関数で行ったのと同様に空のベクタを常に返すような仮実装を追加し、テストがコンパイルされるものの、失敗する様をご自由に確認してください。
search_case_insensitive関数を実装する
search_case_insensitive関数は、リスト12-21に示しましたが、search関数とほぼ同じです。
唯一の違いは、queryと各lineを小文字化していることなので、入力引数の大文字小文字によらず、
行がクエリを含んでいるか確認する際には、同じになるわけです。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { 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 } }
リスト12-21: 比較する前にクエリと行を小文字化するよう、search_case_insensitive関数を定義する
まず、query文字列を小文字化し、同じ名前の覆い隠された変数に保存します。ユーザのクエリが"rust"や"RUST"、
"Rust"、"rUsT"などだったりしても、"rust"であり、大文字小文字を区別しないかのようにクエリを扱えるように、
to_lowercaseをクエリに対して呼び出すことは必須です。
queryは最早、文字列スライスではなくStringであることに注意してください。というのも、
to_lowercaseを呼び出すと、既存のデータを参照するというよりも、新しいデータを作成するからです。
例として、クエリは"rUsT"だとしましょう: その文字列スライスは、小文字のuやtを使えるように含んでいないので、
"rust"を含む新しいStringのメモリを確保しなければならないのです。今、containsメソッドに引数としてqueryを渡すと、
アンド記号を追加する必要があります。containsのシグニチャは、文字列スライスを取るよう定義されているからです。
次に、各lineがqueryを含むか確かめる前にto_lowercaseの呼び出しを追加し、全文字を小文字化しています。
今やlineとqueryを小文字に変換したので、クエリが大文字であろうと小文字であろうとマッチを検索するでしょう。
この実装がテストを通過するか確認しましょう:
running 2 tests
test test::case_insensitive ... ok
test test::case_sensitive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
素晴らしい!どちらも通りました。では、run関数から新しいsearch_case_insensitive関数を呼び出しましょう。
1番目に大文字小文字の区別を切り替えられるよう、Config構造体に設定オプションを追加します。
まだどこでも、このフィールドの初期化をしていないので、追加するとコンパイルエラーが起きます:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Config { pub query: String, pub filename: String, pub case_sensitive: bool, } }
論理値を持つcase_sensitiveフィールドを追加したことに注意してください。次に、run関数に、
case_sensitiveフィールドの値を確認し、search関数かsearch_case_insensitive関数を呼ぶかを決定するのに使ってもらう必要があります。
リスト12-22のようにですね。それでも、これはまだコンパイルできないことに注意してください。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::error::Error; use std::fs::File; use std::io::prelude::*; fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } pub struct Config { query: String, filename: String, case_sensitive: bool, } pub fn run(config: Config) -> Result<(), Box<Error>> { let mut f = File::open(config.filename)?; let mut contents = String::new(); f.read_to_string(&mut contents)?; let results = if config.case_sensitive { search(&config.query, &contents) } else { search_case_insensitive(&config.query, &contents) }; for line in results { println!("{}", line); } Ok(()) } }
リスト12-22: config.case_sensitiveの値に基づいてsearchかsearch_case_insensitiveを呼び出す
最後に、環境変数を確認する必要があります。環境変数を扱う関数は、標準ライブラリのenvモジュールにあるので、
use std::env;行でsrc/lib.rsの冒頭でそのモジュールをスコープに持ってくる必要があります。そして、
envモジュールからvar関数を使用してCASE_INSENSITIVEという環境変数のチェックを行います。
リスト12-23のようにですね。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::env; struct Config { query: String, filename: String, case_sensitive: bool, } // --snip-- impl Config { pub fn new(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let filename = args[2].clone(); let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); Ok(Config { query, filename, case_sensitive }) } } }
リスト12-23: CASE_INSENSITIVEという環境変数のチェックを行う
ここで、case_sensitiveという新しい変数を生成しています。その値をセットするために、
env::var関数を呼び出し、CASE_INSENSITIVE環境変数の名前を渡しています。env::var関数は、
環境変数がセットされていたら、環境変数の値を含むOk列挙子の成功値になるResultを返します。
環境変数がセットされていなければ、Err列挙子を返すでしょう。
Resultのis_errメソッドを使用して、エラーでありゆえに、セットされていないことを確認しています。
これは大文字小文字を区別する検索をすべきことを意味します。CASE_INSENSITIVE環境変数が何かにセットされていれば、
is_errはfalseを返し、プログラムは大文字小文字を区別しない検索を実行するでしょう。環境変数の値はどうでもよく、
セットされているかどうかだけ気にするので、unwrapやexpectあるいは、他のここまで見かけたResultのメソッドではなく、
is_errをチェックしています。
case_sensitive変数の値をConfigインスタンスに渡しているので、リスト12-22で実装したように、
run関数はその値を読み取り、searchかsearch_case_insensitiveを呼び出すか決定できるのです。
試行してみましょう!まず、環境変数をセットせずにクエリはtoでプログラムを実行し、
この時は全て小文字で"to"という言葉を含むあらゆる行が合致するはずです。
$ cargo run to poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
まだ機能しているようです!では、CASE_INSENSITIVEを1にしつつ、同じクエリのtoでプログラムを実行しましょう。
PowerShellを使用しているなら、1コマンドではなく、2コマンドで環境変数をセットし、プログラムを実行する必要があるでしょう:
$ $env:CASE_INSENSITIVE=1
$ cargo run to poem.txt
大文字も含む可能性のある"to"を含有する行が得られるはずです:
$ CASE_INSENSITIVE=1 cargo run to poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
素晴らしい、"To"を含む行も出てきましたね!minigrepプログラムはこれで、
環境変数によって制御できる大文字小文字を区別しない検索も行えるようになりました。もうコマンドライン引数か、
環境変数を使ってオプションを管理する方法も知りましたね。
引数と環境変数で同じ設定を行うことができるプログラムもあります。そのような場合、 プログラムはどちらが優先されるか決定します。自身の別の鍛錬として、コマンドライン引数か、 環境変数で大文字小文字の区別を制御できるようにしてみてください。 片方は大文字小文字を区別するようにセットされ、もう片方は区別しないようにセットしてプログラムが実行された時に、 コマンドライン引数と環境変数のどちらの優先度が高くなるかを決めてください。
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!を使うように変更しましょう。
ファイル名: src/main.rs
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
if let Err(e) = minigrep::run(config) {
eprintln!("Application error: {}", e);
process::exit(1);
}
}
リスト12-24: eprintln!を使って標準出力ではなく、標準エラーにエラーメッセージを書き込む
println!をeprintln!に変えてから、再度同じようにプログラムを実行しましょう。
引数なしかつ、標準出力を>でリダイレクトしてね:
$ 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で入出力処理を行う方法を講義しました。
コマンドライン引数、ファイル、環境変数、そしてエラー出力にeprintln!マクロを使用することで、
もう、コマンドラインアプリケーションを書く準備ができています。以前の章の概念を使用することで、
コードはうまく体系化され、適切なデータ構造に効率的にデータを保存し、エラーをうまく扱い、
よくテストされるでしょう。
次は、関数型言語に影響されたRust機能を一部探究します: クロージャとイテレータです。
関数型言語の機能: イテレータとクロージャ
Rustの設計は、多くの既存の言語やテクニックにインスピレーションを得ていて、 その一つの大きな影響が関数型プログラミングです。関数型でのプログラミングには、しばしば、 引数で渡したり、関数から関数を返したり、関数を後ほど使用するために変数に代入することで関数を値として使用することが含まれます。
この章では、関数型プログラミングがどんなものであったり、なかったりするかという問題については議論しませんが、 代わりに関数型とよく言及される多くの言語の機能に似たRustの機能の一部について議論しましょう。
具体的には、以下を講義します:
- クロージャ、変数に保存できる関数に似た文法要素
- イテレータ、一連の要素を処理する方法
- これら2つの機能を使用して第12章の入出力プロジェクトを改善する方法
- これら2つの機能のパフォーマンス(ネタバレ: 思ったよりも速いです)
パターンマッチングやenumなど、他のRustの機能も関数型に影響されていますが、他の章で講義してきました。 クロージャとイテレータをマスターすることは、慣用的で速いRustコードを書くことの重要な部分なので、 この章を丸ごと捧げます。
クロージャ: 環境をキャプチャできる匿名関数
Rustのクロージャは、変数に保存したり、引数として他の関数に渡すことのできる匿名関数です。 ある場所でクロージャを生成し、それから別の文脈でクロージャを呼び出して評価することができます。 関数と異なり、呼び出されたスコープの値をクロージャは、キャプチャすることができます。 これらのクロージャの機能がコードの再利用や、動作のカスタマイズを行わせてくれる方法を模擬しましょう。
クロージャで動作の抽象化を行う
クロージャを保存して後々使用できるようにするのが有用な場面の例に取り掛かりましょう。その過程で、 クロージャの記法、型推論、トレイトについて語ります。
以下のような架空の場面を考えてください: カスタマイズされたエクササイズのトレーニングプランを生成するアプリを作るスタートアップで働くことになりました。 バックエンドはRustで記述され、トレーニングプランを生成するアルゴリズムは、アプリユーザの年齢や、 BMI、運動の好み、最近のトレーニング、指定された強弱値などの多くの要因を考慮します。 実際に使用されるアルゴリズムは、この例では重要ではありません; 重要なのは、この計算が数秒要することです。 必要なときだけこのアルゴリズムを呼び出し、1回だけ呼び出したいので、必要以上にユーザを待たせないことになります。
リスト13-1に示したsimulated_expensive_calculation関数でこの仮定のアルゴリズムを呼び出すことをシミュレートし、
この関数はcalculating slowlyと出力し、2秒待ってから、渡した数値をなんでも返します。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; fn simulated_expensive_calculation(intensity: u32) -> u32 { // ゆっくり計算します println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); intensity } }
リスト13-1: 実行に約2秒かかる架空の計算の代役を務める関数
次は、この例で重要なトレーニングアプリの部分を含むmain関数です。この関数は、
ユーザがトレーニングプランを要求した時にアプリが呼び出すコードを表します。
アプリのフロントエンドと相互作用する部分は、クロージャの使用と関係ないので、プログラムへの入力を表す値をハードコードし、
その出力を表示します。
必要な入力は以下の通りです:
- ユーザの強弱値、これはユーザがトレーニングを要求して、低強度のトレーニングか、 高強度のトレーニングがしたいかを示したときに指定されます。
- 乱数、これはトレーニングプランにバリエーションを起こします。
出力は、推奨されるトレーニングプランになります。リスト13-2は使用するmain関数を示しています。
ファイル名: src/main.rs
fn main() { let simulated_user_specified_value = 10; let simulated_random_number = 7; generate_workout( simulated_user_specified_value, simulated_random_number ); } fn generate_workout(intensity: u32, random_number: u32) {}
リスト13-2: ユーザ入力や乱数生成をシミュレートするハードコードされた値があるmain関数
簡潔性のために、変数simulated_user_specified_valueは10、変数simulated_random_numberは7とハードコードしました;
実際のプログラムにおいては、強弱値はアプリのフロントエンドから取得し、乱数の生成には、第2章の数当てゲームの例のように、randクレートを使用するでしょう。
main関数は、シミュレートされた入力値とともにgenerate_workout関数を呼び出します。
今や文脈ができたので、アルゴリズムに取り掛かりましょう。リスト13-3のgenerate_workout関数は、
この例で最も気にかかるアプリのビジネスロジックを含んでいます。この例での残りの変更は、
この関数に対して行われるでしょう:
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; fn simulated_expensive_calculation(num: u32) -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num } fn generate_workout(intensity: u32, random_number: u32) { if intensity < 25 { println!( // 今日は{}回腕立て伏せをしてください! "Today, do {} pushups!", simulated_expensive_calculation(intensity) ); println!( // 次に、{}回腹筋をしてください! "Next, do {} situps!", simulated_expensive_calculation(intensity) ); } else { if random_number == 3 { // 今日は休憩してください!水分補給を忘れずに! println!("Take a break today! Remember to stay hydrated!"); } else { println!( // 今日は、{}分間走ってください! "Today, run for {} minutes!", simulated_expensive_calculation(intensity) ); } } } }
リスト13-3: 入力に基づいてトレーニングプランを出力するビジネスロジックと、
simulated_expensive_calculation関数の呼び出し
リスト13-3のコードには、遅い計算を行う関数への呼び出しが複数あります。最初のifブロックが、
simulated_expensive_calculationを2回呼び出し、外側のelse内のifは全く呼び出さず、
2番目のelseケースの内側にあるコードは1回呼び出しています。
generate_workout関数の期待される振る舞いは、まずユーザが低強度のトレーニング(25より小さい数値で表される)か、
高強度のトレーニング(25以上の数値)を欲しているか確認することです。
低強度のトレーニングプランは、シミュレーションしている複雑なアルゴリズムに基づいて、 多くの腕立て伏せや腹筋運動を推奨してきます。
ユーザが高強度のトレーニングを欲していれば、追加のロジックがあります: アプリが生成した乱数がたまたま3なら、 アプリは休憩と水分補給を勧めます。そうでなければ、ユーザは複雑なアルゴリズムに基づいて数分間のランニングをします。
このコードは現在、ビジネスのほしいままに動くでしょうが、データサイエンスチームが、
simulated_expensive_calculation関数を呼び出す方法に何らかの変更を加える必要があると決定したとしましょう。
そのような変更が起きた時に更新を簡略化するため、simulated_expensive_calculation関数を1回だけ呼び出すように、
このコードをリファクタリングしたいです。また、その過程でその関数への呼び出しを増やすことなく無駄に2回、
この関数を現時点で呼んでいるところを切り捨てたくもあります。要するに、結果が必要なければ関数を呼び出したくなく、
それでも1回だけ呼び出したいのです。
関数でリファクタリング
多くの方法でトレーニングプログラムを再構築することもできます。
1番目にsimulated_expensive_calculation関数への重複した呼び出しを変数に抽出しようとしましょう。リスト13-4に示したように。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; fn simulated_expensive_calculation(num: u32) -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num } fn generate_workout(intensity: u32, random_number: u32) { let expensive_result = simulated_expensive_calculation(intensity); if intensity < 25 { println!( "Today, do {} pushups!", expensive_result ); println!( "Next, do {} situps!", expensive_result ); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_result ); } } } }
リスト13-4: 複数のsimulated_expensive_calculationの呼び出しを1箇所に抽出し、
結果をexpensive_result変数に保存する
この変更によりsimulated_expensive_calculationの呼び出しが単一化され、
最初のifブロックが無駄に関数を2回呼んでいた問題を解決します。不幸なことに、これでは、
あらゆる場合にこの関数を呼び出し、その結果を待つことになり、結果値を全く使用しない内側のifブロックでもそうしてしまいます。
プログラムの1箇所でコードを定義したいですが、結果が本当に必要なところでだけコードを実行します。 これは、クロージャのユースケースです!
クロージャでリファクタリングして、コードを保存する
ifブロックの前にいつもsimulated_expensive_calculation関数を呼び出す代わりに、
クロージャを定義し、関数呼び出しの結果を保存するのではなく、そのクロージャを変数に保存できます。リスト13-5のようにですね。
simulated_expensive_calculationの本体全体を実際に、ここで導入しているクロージャ内に移すことができます。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; let expensive_closure = |num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; expensive_closure(5); }
リスト13-5: クロージャを定義し、expensive_closure変数に保存する
クロージャ定義が=に続き、変数expensive_closureに代入されています。クロージャを定義するには、
1組の縦棒から始め、その内部にクロージャの仮引数を指定します; この記法は、SmalltalkやRubyのクロージャ定義と類似していることから、
選択されました。このクロージャには、numという引数が1つあります: 2つ以上引数があるなら、
|param1, param2|のように、カンマで区切ります。
引数の後に、クロージャの本体を保持する波括弧を配置します(これはクロージャ本体が式一つなら省略可能です)。
波括弧の後、クロージャのお尻には、セミコロンが必要で、let文を完成させます。クロージャ本体の最後の行から返る値(num)が、
呼び出された時にクロージャから返る値になります。その行がセミコロンで終わっていないからです;
ちょうど関数の本体みたいですね。
このlet文は、expensive_closureが、匿名関数を呼び出した結果の値ではなく、
匿名関数の定義を含むことを意味することに注意してください。コードを定義して、
1箇所で呼び出し、そのコードを保存し、後々、それを呼び出したいがためにクロージャを使用していることを思い出してください;
呼び出したいコードは、現在、expensive_closureに保存されています。
クロージャが定義されたので、ifブロックのコードを変更して、そのコードを実行するクロージャを呼び出し、結果値を得ることができます。
クロージャは、関数のように呼び出せます: クロージャ定義を含む変数名を指定し、使用したい引数値を含むかっこを続けます。
リスト13-6に示したようにですね。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; fn generate_workout(intensity: u32, random_number: u32) { let expensive_closure = |num| { 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) ); } } } }
リスト13-6: 定義したexpensive_closureを呼び出す
今では、重い計算はたった1箇所でのみ呼び出され、その結果が必要なコードを実行するだけになりました。
ところが、リスト13-3の問題の一つを再浮上させてしまいました: それでも、最初のifブロックでクロージャを2回呼んでいて、
そうすると、重いコードを2回呼び出し、必要な分の2倍ユーザを待たせてしまいます。そのifブロックのみに属する変数を生成して、
クロージャの呼び出し結果を保持するそのifブロックに固有の変数を生成することでこの問題を解消することもできますが、
クロージャは他の解決法も用意してくれます。その解決策については、もう少し先で語りましょう。でもまずは、
クロージャ定義に型注釈がない理由とクロージャに関わるトレイトについて話しましょう。
クロージャの型推論と注釈
クロージャでは、fn関数のように引数の型や戻り値の型を注釈する必要はありません。関数では、
型注釈は必要です。ユーザに露出する明示的なインターフェイスの一部だからです。このインターフェイスを堅実に定義することは、
関数が使用したり、返したりする値の型についてみんなが合意していることを保証するために重要なのです。
しかし、クロージャはこのような露出するインターフェイスには使用されません: 変数に保存され、
名前付けしたり、ライブラリの使用者に晒されることなく、使用されます。
クロージャは通常短く、あらゆる任意の筋書きではなく、狭い文脈でのみ関係します。 このような限定された文脈内では、コンパイラは、多くの変数の型を推論できるのと似たように、 引数や戻り値の型を頼もしく推論することができます。
このような小さく匿名の関数で型をプログラマに注釈させることは煩わしいし、コンパイラがすでに利用可能な情報と大きく被っています。
本当に必要な以上に冗長になることと引き換えに、明示性と明瞭性を向上させたいなら、変数に型注釈を加えることもできます; リスト13-5で定義したクロージャに型を注釈するなら、リスト13-7に示した定義のようになるでしょう。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; let expensive_closure = |num: u32| -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; }
リスト13-7: クロージャの引数と戻り値の省略可能な型注釈を追加する
型注釈を付け加えると、クロージャの記法は、関数の記法により酷似して見えます。以下が、引数に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 ;
1行目が関数定義を示し、2行目がフルに注釈したクロージャ定義を示しています。 3行目は、クロージャ定義から型注釈を取り除き、4行目は、かっこを取り除いていて、 かっこはクロージャの本体がただ1つの式からなるので、省略可能です。これらは全て、 呼び出された時に同じ振る舞いになる合法な定義です。
クロージャ定義には、引数それぞれと戻り値に対して推論される具体的な型が一つあります。例えば、
リスト13-8に引数として受け取った値を返すだけの短いクロージャの定義を示しました。
このクロージャは、この例での目的以外には有用ではありません。この定義には、
何も型注釈を加えていないことに注意してください: それから1回目にStringを引数に、
2回目にu32を引数に使用してこのクロージャを2回呼び出そうとしたら、エラーになります。
ファイル名: src/main.rs
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
リスト13-8: 2つの異なる型で型が推論されるクロージャの呼び出しを試みる
コンパイラは、次のエラーを返します:
error[E0308]: mismatched types
--> src/main.rs
|
| let n = example_closure(5);
| ^ expected struct `std::string::String`, found
integral variable
|
= note: expected type `std::string::String`
found type `{integer}`
String値でexample_closureを呼び出した最初の時点で、コンパイラはxとクロージャの戻り値の型をStringと推論します。
そして、その型がexample_closureのクロージャに閉じ込められ、同じクロージャを異なる型で使用しようとすると、
型エラーが出るのです。
ジェネリック引数とFnトレイトを使用してクロージャを保存する
トレーニング生成アプリに戻りましょう。リスト13-6において、まだコードは必要以上の回数、重い計算のクロージャを呼んでいました。 この問題を解決する一つの選択肢は、重いクロージャの結果を再利用できるように変数に保存し、クロージャを再度呼ぶ代わりに、 結果が必要になる箇所それぞれでその変数を使用することです。しかしながら、この方法は同じコードを大量に繰り返す可能性があります。
運のいいことに、別の解決策もあります。クロージャやクロージャの呼び出し結果の値を保持する構造体を作れるのです。 結果の値が必要な場合のみにその構造体はクロージャを実行し、その結果の値をキャッシュするので、残りのコードは、 結果を保存し、再利用する責任を負わなくて済むのです。このパターンは、メモ化(memoization)または、 遅延評価(lazy evaluation)として知っているかもしれません。
クロージャを保持する構造体を作成するために、クロージャの型を指定する必要があります。 構造体定義は、各フィールドの型を把握しておく必要がありますからね。各クロージャインスタンスには、 独自の匿名の型があります: つまり、たとえ2つのクロージャが全く同じシグニチャでも、その型はそれでも違うものと考えられるということです。 クロージャを使用する構造体、enum、関数引数を定義するには、第10章で議論したように、 ジェネリクスとトレイト境界を使用します。
Fnトレイトは、標準ライブラリで用意されています。全てのクロージャは、以下のいずれかのトレイトを実装しています:
Fn、FnMutまたは、FnOnceです。「クロージャで環境をキャプチャする」節で、これらのトレイト間の差異を議論します;
この例では、Fnトレイトを使えます。
Fnトレイト境界にいくつかの型を追加することで、このトレイト境界に合致するクロージャが持つべき引数と戻り値の型を示します。
今回のクロージャはu32型の引数を一つ取り、u32を返すので、指定するトレイト境界はFn(u32) -> u32になります。
リスト13-9は、クロージャとオプションの結果値を保持するCacher構造体の定義を示しています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { struct Cacher<T> where T: Fn(u32) -> u32 { calculation: T, value: Option<u32>, } }
リスト13-9: クロージャをcalculationに、オプションの結果値をvalueに保持するCacher構造体を定義する
Cacher構造体は、ジェネリックな型Tのcalculationフィールドを持ちます。Tのトレイト境界は、
Fnトレイトを使うことでクロージャであると指定しています。calculationフィールドに保存したいクロージャは全て、
1つのu32引数(Fnの後の括弧内で指定されている)を取り、u32(->の後に指定されている)を返さなければなりません。
注釈: 関数も3つの
Fnトレイト全部を実装します。もし環境から値をキャプチャする必要がなければ、Fnトレイトを実装する何かが必要になるクロージャではなく、関数を使用できます。
valueフィールドの型は、Option<u32>です。クロージャを実行する前に、valueはNoneになるでしょう。
Cacherを使用するコードがクロージャの結果を求めてきたら、その時点でCacherはクロージャを実行し、
その結果をvalueフィールドのSome列挙子に保存します。それから、コードが再度クロージャの結果を求めたら、
クロージャを再実行するのではなく、CacherはSome列挙子に保持された結果を返すでしょう。
たった今解説したvalueフィールド周りのロジックは、リスト13-10で定義されています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { struct Cacher<T> where T: Fn(u32) -> u32 { calculation: T, value: Option<u32>, } impl<T> Cacher<T> where T: Fn(u32) -> u32 { fn new(calculation: T) -> Cacher<T> { Cacher { calculation, value: None, } } fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { let v = (self.calculation)(arg); self.value = Some(v); v }, } } } }
リスト13-10: Cacherのキャッシュ機構
呼び出し元のコードにこれらのフィールドの値を直接変えてもらうのではなく、Cacherに構造体のフィールドの値を管理してほしいので、
これらのフィールドは非公開になっています。
Cacher::new関数はジェネリックな引数のTを取り、Cacher構造体と同じトレイト境界を持つよう定義しました。
それからcalculationフィールドに指定されたクロージャと、
valueフィールドにNone値を保持するCacherインスタンスをCacher::newは返します。
まだクロージャを実行していないからですね。
呼び出し元のコードがクロージャの評価結果を必要としたら、クロージャを直接呼ぶ代わりに、valueメソッドを呼びます。
このメソッドは、結果の値がself.valueのSomeに既にあるかどうか確認します; そうなら、
クロージャを再度実行することなくSome内の値を返します。
self.valueがNoneなら、コードはself.calculationに保存されたクロージャを呼び出し、
結果を将来使えるようにself.valueに保存し、その値を返しもします。
リスト13-11は、リスト13-6の関数generate_workoutでこのCacher構造体を使用する方法を示しています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; struct Cacher<T> where T: Fn(u32) -> u32 { calculation: T, value: Option<u32>, } impl<T> Cacher<T> where T: Fn(u32) -> u32 { fn new(calculation: T) -> Cacher<T> { Cacher { calculation, value: None, } } fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { let v = (self.calculation)(arg); self.value = Some(v); v }, } } } fn generate_workout(intensity: u32, random_number: u32) { let mut expensive_result = Cacher::new(|num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }); if intensity < 25 { println!( "Today, do {} pushups!", expensive_result.value(intensity) ); println!( "Next, do {} situps!", expensive_result.value(intensity) ); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_result.value(intensity) ); } } } }
リスト13-11: generate_workout関数内でCacherを使用し、キャッシュ機構を抽象化する
クロージャを変数に直接保存する代わりに、クロージャを保持するCacherの新規インスタンスを保存しています。
そして、結果が必要な場所それぞれで、そのCacherインスタンスに対してvalueメソッドを呼び出しています。
必要なだけvalueメソッドを呼び出したり、全く呼び出さないこともでき、重い計算は最大でも1回しか走りません。
リスト13-2のmain関数とともにこのプログラムを走らせてみてください。
simulated_user_specified_valueとsimulated_random_number変数の値を変えて、
いろんなifやelseブロックの場合全てで、calculating slowlyは1回だけ、必要な時にのみ出現することを実証してください。
必要以上に重い計算を呼び出さないことを保証するのに必要なロジックの面倒をCacherは見るので、
generate_workoutはビジネスロジックに集中できるのです。
Cacher実装の限界
値をキャッシュすることは、コードの他の部分でも異なるクロージャで行いたくなる可能性のある一般的に有用な振る舞いです。
しかし、現在のCacherの実装には、他の文脈で再利用することを困難にしてしまう問題が2つあります。
1番目の問題は、Cacherインスタンスが、常にvalueメソッドの引数argに対して同じ値になると想定していることです。
言い換えると、Cacherのこのテストは、失敗するでしょう:
#[test]
fn call_with_different_values() {
let mut c = Cacher::new(|a| a);
let v1 = c.value(1);
let v2 = c.value(2);
assert_eq!(v2, 2);
}
このテストは、渡された値を返すクロージャを伴うCacherインスタンスを新しく生成しています。
このCacherインスタンスに対して1というarg値で呼び出し、それから2というarg値で呼び出し、
2というarg値のvalue呼び出しは2を返すべきと期待しています。
このテストをリスト13-9とリスト13-10のCacher実装で動かすと、assert_eqからこんなメッセージが出て、
テストは失敗します:
thread 'call_with_different_values' panicked at 'assertion failed: `(left == right)`
left: `1`,
right: `2`', src/main.rs
問題は、初めてc.valueを1で呼び出した時に、Cacherインスタンスはself.valueにSome(1)を保存したことです。
その後valueメソッドに何を渡しても、常に1を返すわけです。
単独の値ではなく、ハッシュマップを保持するようにCacherを改変してみてください。ハッシュマップのキーは、
渡されるarg値になり、ハッシュマップの値は、そのキーでクロージャを呼び出した結果になるでしょう。
self.valueが直接SomeかNone値であることを調べる代わりに、value関数はハッシュマップのargを調べ、
存在するならその値を返します。存在しないなら、Cacherはクロージャを呼び出し、
arg値に紐づけてハッシュマップに結果の値を保存します。
現在のCacher実装の2番目の問題は、引数の型にu32を一つ取り、u32を返すクロージャしか受け付けないことです。
例えば、文字列スライスを取り、usizeを返すクロージャの結果をキャッシュしたくなるかもしれません。
この問題を修正するには、Cacher機能の柔軟性を向上させるためによりジェネリックな引数を導入してみてください。
クロージャで環境をキャプチャする
トレーニング生成の例においては、クロージャをインラインの匿名関数として使っただけでした。しかし、 クロージャには、関数にはない追加の能力があります: 環境をキャプチャし、 自分が定義されたスコープの変数にアクセスできるのです。
リスト13-12は、equal_to_x変数に保持されたクロージャを囲む環境からx変数を使用するクロージャの例です。
ファイル名: src/main.rs
fn main() { let x = 4; let equal_to_x = |z| z == x; let y = 4; assert!(equal_to_x(y)); }
リスト13-12: 内包するスコープの変数を参照するクロージャの例
ここで、xはequal_to_xの引数でもないのに、
equal_to_xが定義されているのと同じスコープで定義されているx変数をequal_to_xクロージャは使用できています。
同じことを関数では行うことができません; 以下の例で試したら、コードはコンパイルできません:
ファイル名: src/main.rs
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool { z == x }
let y = 4;
assert!(equal_to_x(y));
}
エラーが出ます:
error[E0434]: can't capture dynamic environment in a fn item; use the || { ...
} closure form instead
(エラー: fn要素では動的な環境をキャプチャできません; 代わりに|| { ... }のクロージャ形式を
使用してください)
--> src/main.rs
|
4 | fn equal_to_x(z: i32) -> bool { z == x }
| ^
コンパイラは、この形式はクロージャでのみ動作することさえも思い出させてくれています!
クロージャが環境から値をキャプチャすると、メモリを使用してクロージャ本体で使用できるようにその値を保存します。 このメモリ使用は、環境をキャプチャしないコードを実行するようなもっと一般的な場合には払いたくないオーバーヘッドです。 関数は、絶対に環境をキャプチャすることが許可されていないので、関数を定義して使えば、このオーバーヘッドを招くことは絶対にありません。
クロージャは、3つの方法で環境から値をキャプチャでき、この方法は関数が引数を取れる3つの方法に直に対応します:
所有権を奪う、可変で借用する、不変で借用するです。これらは、以下のように3つのFnトレイトでコード化されています:
FnOnceは、クロージャの環境として知られている内包されたスコープからキャプチャした変数を消費します。 キャプチャした変数を消費するために、定義された際にクロージャはこれらの変数の所有権を奪い、 自身にムーブするのです。名前のうち、Onceの部分は、 このクロージャは同じ変数の所有権を2回以上奪うことができないという事実を表しているので、1回しか呼ぶことができないのです。FnMutは、可変で値を借用するので、環境を変更することができます。Fnは、環境から値を不変で借用します。
クロージャを生成する時、クロージャが環境を使用する方法に基づいて、コンパイラはどのトレイトを使用するか推論します。
少なくとも1回は呼び出されるので、全てのクロージャはFnOnceを実装しています。キャプチャした変数をムーブしないクロージャは、
FnMutも実装し、キャプチャした変数に可変でアクセスする必要のないクロージャは、Fnも実装しています。
リスト13-12では、equal_to_xクロージャはxを不変で借用しています(ゆえにequal_to_xはFnトレイトです)。
クロージャの本体は、xを読む必要しかないからです。
環境でクロージャが使用している値の所有権を奪うことをクロージャに強制したいなら、引数リストの前にmoveキーワードを使用できます。
このテクニックは、新しいスレッドにデータが所有されるように、クロージャを新しいスレッドに渡して、
データをムーブする際に大概は有用です。
並行性について語る第16章で、moveクロージャの例はもっと多く出てきます。とりあえず、
こちらがmoveキーワードがクロージャ定義に追加され、整数の代わりにベクタを使用するリスト13-12からのコードです。
整数はムーブではなく、コピーされてしまいますからね; このコードはまだコンパイルできないことに注意してください。
ファイル名: src/main.rs
fn main() {
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x;
// ここでは、xを使用できません: {:?}
println!("can't use x here: {:?}", x);
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
}
以下のようなエラーを受けます:
error[E0382]: use of moved value: `x`
(エラー: ムーブされた値の使用: `x`)
--> src/main.rs:6:40
|
4 | let equal_to_x = move |z| z == x;
| -------- value moved (into closure) here
(値はここで(クロージャに)ムーブされた)
5 |
6 | println!("can't use x here: {:?}", x);
| ^ value used here after move
(ムーブ後、値はここで使用された)
|
= note: move occurs because `x` has type `std::vec::Vec<i32>`, which does not
implement the `Copy` trait
(注釈: `x`が`std::vec::Vec<i32>`という`Copy`トレイトを実装しない型のため、ムーブが起きました)
クロージャが定義された際に、クロージャにxの値はムーブされています。moveキーワードを追加したからです。
そして、クロージャはxの所有権を持ち、mainがprintln!でxを使うことはもう叶わないのです。
println!を取り除けば、この例は修正されます。
Fnトレイトのどれかを指定するほとんどの場合、Fnから始めると、コンパイラがクロージャ本体内で起こっていることにより、
FnMutやFnOnceが必要な場合、教えてくれるでしょう。
環境をキャプチャできるクロージャが関数の引数として有用な場面を説明するために、次のトピックに移りましょう: イテレータです。
一連の要素をイテレータで処理する
イテレータパターンにより、一連の要素に順番に何らかの作業を行うことができます。イテレータは、 各要素を繰り返し、シーケンスが終わったことを決定するロジックの責任を負います。イテレータを使用すると、 自身でそのロジックを再実装する必要がなくなるのです。
Rustにおいて、イテレータは怠惰です。つまり、イテレータを使い込んで消費するメソッドを呼ぶまで何の効果もないということです。
例えば、リスト13-13のコードは、Vec<T>に定義されたiterメソッドを呼ぶことでv1ベクタの要素に対するイテレータを生成しています。
このコード単独では、何も有用なことはしません。
#![allow(unused)] fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); }
リスト13-13: イテレータを生成する
一旦イテレータを生成したら、いろんな手段で使用することができます。第3章のリスト3-5では、
ここまでiterの呼び出しが何をするかごまかしてきましたが、forループでイテレータを使い、
各要素に何かコードを実行しています。
リスト13-14の例は、イテレータの生成とforループでイテレータを使用することを区別しています。
イテレータは、v1_iter変数に保存され、その時には繰り返しは起きていません。v1_iterのイテレータで、
forループが呼び出された時に、イテレータの各要素がループの繰り返しで使用され、各値が出力されます。
#![allow(unused)] fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { // {}でした println!("Got: {}", val); } }
リスト13-14: forループでイテレータを使用する
標準ライブラリにより提供されるイテレータが存在しない言語では、変数を添え字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で、
これらはこのトレイトとの関連型(associated type)を定義しています。関連型についての詳細は、第19章で語ります。
とりあえず、知っておく必要があることは、このコードがIteratorトレイトを実装するには、Item型も定義する必要があり、
そして、このItem型がnextメソッドの戻り値の型に使われていると述べていることです。換言すれば、
Item型がイテレータから返ってくる型になるだろうということです。
Iteratorトレイトは、一つのメソッドを定義することを実装者に要求することだけします: nextメソッドで、
これは1度にSomeに包まれたイテレータの1要素を返し、繰り返しが終わったら、Noneを返します。
イテレータに対して直接nextメソッドを呼び出すこともできます; リスト13-15は、
ベクタから生成されたイテレータのnextを繰り返し呼び出した時にどんな値が返るかを模擬しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[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); } }
リスト13-15: イテレータに対してnextメソッドを呼び出す
v1_iterを可変にする必要があったことに注目してください: イテレータのnextメソッドを呼び出すと、
今シーケンスのどこにいるかを追いかけるためにイテレータが使用している内部の状態が変わります。
つまり、このコードはイテレータを消費、または使い込むのです。
nextの各呼び出しは、イテレータの要素を一つ、食います。forループを使用した時には、
v1_iterを可変にする必要はありませんでした。というのも、ループがv1_iterの所有権を奪い、
陰で可変にしていたからです。
また、nextの呼び出しで得られる値は、ベクタの値への不変な参照であることにも注目してください。
iterメソッドは、不変参照へのイテレータを生成します。v1の所有権を奪い、所有された値を返すイテレータを生成したいなら、
iterではなくinto_iterを呼び出すことができます。同様に、可変参照を繰り返したいなら、
iterではなくiter_mutを呼び出せます。
イテレータを消費するメソッド
Iteratorトレイトには、標準ライブラリが提供してくれているデフォルト実装のある多くの異なるメソッドがあります;
Iteratorトレイトの標準ライブラリのAPIドキュメントを検索することで、これらのメソッドについて知ることができます。
これらのメソッドの中には、定義内でnextメソッドを呼ぶものもあり、故にIteratorトレイトを実装する際には、
nextメソッドを実装する必要があるのです。
nextを呼び出すメソッドは、消費アダプタ(consuming adaptors)と呼ばれます。呼び出しがイテレータの使い込みになるからです。
一例は、sumメソッドで、これはイテレータの所有権を奪い、nextを繰り返し呼び出すことで要素を繰り返し、
故にイテレータを消費するのです。繰り返しが進むごとに、各要素を一時的な合計に追加し、
繰り返しが完了したら、その合計を返します。リスト13-16は、sumの使用を説明したテストです:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[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); } }
リスト13-16: sumメソッドを呼び出してイテレータの全要素の合計を得る
sumは呼び出し対象のイテレータの所有権を奪うので、sum呼び出し後にv1_iterを使用することはできません。
他のイテレータを生成するメソッド
Iteratorトレイトに定義された他のメソッドは、イテレータアダプタ(iterator adaptors)として知られていますが、
イテレータを別の種類のイテレータに変えさせてくれます。イテレータアダプタを複数回呼ぶ呼び出しを連結して、
複雑な動作を読みやすい形で行うことができます。ですが、全てのイテレータは怠惰なので、消費アダプタメソッドのどれかを呼び出し、
イテレータアダプタの呼び出しから結果を得なければなりません。
リスト13-17は、イテレータアダプタメソッドのmapの呼び出し例を示し、各要素に対して呼び出すクロージャを取り、
新しいイテレータを生成します。ここのクロージャは、ベクタの各要素が1インクリメントされる新しいイテレータを作成します。
ところが、このコードは警告を発します:
ファイル名: src/main.rs
#![allow(unused)] fn main() { let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); }
リスト13-17: イテレータアダプタのmapを呼び出して新規イテレータを作成する
出る警告は以下の通りです:
warning: unused `std::iter::Map` which must be used: iterator adaptors are lazy
and do nothing unless consumed
(警告: 使用されねばならない`std::iter::Map`が未使用です: イテレータアダプタは怠惰で、
消費されるまで何もしません)
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: #[warn(unused_must_use)] on by default
リスト13-17のコードは何もしません; 指定したクロージャは、決して呼ばれないのです。警告が理由を思い出させてくれています: イテレータアダプタは怠惰で、ここでイテレータを消費する必要があるのです。
これを修正し、イテレータを消費するには、collectメソッドを使用しますが、これは第12章のリスト12-1でenv::argsとともに使用しました。
このメソッドはイテレータを消費し、結果の値をコレクションデータ型に集結させます。
リスト13-18において、map呼び出しから返ってきたイテレータを繰り返した結果をベクタに集結させています。
このベクタは、最終的に元のベクタの各要素に1を足したものが含まれます。
ファイル名: src/main.rs
#![allow(unused)] 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]); }
リスト13-18: mapメソッドを呼び出して新規イテレータを作成し、
それからcollectメソッドを呼び出してその新規イテレータを消費し、ベクタを生成する
mapはクロージャを取るので、各要素に対して行いたいどんな処理も指定することができます。
これは、Iteratorトレイトが提供する繰り返し動作を再利用しつつ、
クロージャにより一部の動作をカスタマイズできる好例になっています。
環境をキャプチャするクロージャを使用する
イテレータが出てきたので、filterイテレータアダプタを使って環境をキャプチャするクロージャの一般的な使用をデモすることができます。
イテレータのfilterメソッドは、イテレータの各要素を取り、論理値を返すクロージャを取ります。
このクロージャがtrueを返せば、filterが生成するイテレータにその値が含まれます。クロージャがfalseを返したら、
結果のイテレータにその値は含まれません。
リスト13-19では、環境からshoe_size変数をキャプチャするクロージャでfilterを使って、
Shoe構造体インスタンスのコレクションを繰り返しています。指定したサイズの靴だけを返すわけです。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[derive(PartialEq, Debug)] struct Shoe { size: u32, style: String, } fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> { shoes.into_iter() .filter(|s| s.size == shoe_size) .collect() } #[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_my_size(shoes, 10); assert_eq!( in_my_size, vec![ Shoe { size: 10, style: String::from("sneaker") }, Shoe { size: 10, style: String::from("boot") }, ] ); } }
リスト13-19: shoe_sizeをキャプチャするクロージャでfilterメソッドを使用する
shoes_in_my_size関数は、引数として靴のベクタとサイズの所有権を奪います。指定されたサイズの靴だけを含むベクタを返します。
shoes_in_my_sizeの本体で、into_iterを呼び出してベクタの所有権を奪うイテレータを作成しています。
そして、filterを呼び出してそのイテレータをクロージャがtrueを返した要素だけを含む新しいイテレータに適合させます。
クロージャは、環境からshoe_size引数をキャプチャし、指定されたサイズの靴だけを保持しながら、
その値を各靴のサイズと比較します。最後に、collectを呼び出すと、
関数により返ってきたベクタに適合させたイテレータから返ってきた値が集まるのです。
shoes_in_my_sizeを呼び出した時に、指定した値と同じサイズの靴だけが得られることをテストは示しています。
Iteratorトレイトで独自のイテレータを作成する
ベクタに対し、iter、into_iter、iter_mutを呼び出すことでイテレータを作成できることを示してきました。
ハッシュマップなどの標準ライブラリの他のコレクション型からもイテレータを作成できます。
Iteratorトレイトを自分で実装することで、したいことを何でもするイテレータを作成することもできます。
前述の通り、定義を提供する必要のある唯一のメソッドは、nextメソッドなのです。一旦、そうしてしまえば、
Iteratorトレイトが用意しているデフォルト実装のある他の全てのメソッドを使うことができるのです!
デモ用に、絶対に1から5をカウントするだけのイテレータを作成しましょう。まず、値を保持する構造体を生成し、
Iteratorトレイトを実装することでこの構造体をイテレータにし、その実装内の値を使用します。
リスト13-20は、Counter構造体とCounterのインスタンスを作るnew関連関数の定義です:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { struct Counter { count: u32, } impl Counter { fn new() -> Counter { Counter { count: 0 } } } }
リスト13-20: Counter構造体とcountに対して0という初期値でCounterのインスタンスを作るnew関数を定義する
Counter構造体には、countというフィールドがあります。このフィールドは、
1から5までの繰り返しのどこにいるかを追いかけるu32値を保持しています。Counterの実装にその値を管理してほしいので、
countフィールドは非公開です。countフィールドは常に0という値から新規インスタンスを開始するという動作をnew関数は強要します。
次に、nextメソッドの本体をこのイテレータが使用された際に起きてほしいことを指定するように定義して、
Counter型に対してIteratorトレイトを実装します。リスト13-21のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { struct Counter { count: u32, } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { self.count += 1; if self.count < 6 { Some(self.count) } else { None } } } }
リスト13-21: Counter構造体にIteratorトレイトを実装する
イテレータのItem関連型をu32に設定しました。つまり、イテレータは、u32の値を返します。
ここでも、まだ関連型について心配しないでください。第19章で講義します。
イテレータに現在の状態に1を足してほしいので、まず1を返すようにcountを0に初期化しました。
countの値が5以下なら、nextはSomeに包まれた現在の値を返しますが、
countが6以上なら、イテレータはNoneを返します。
Counterイテレータのnextメソッドを使用する
一旦Iteratorトレイトを実装し終わったら、イテレータの出来上がりです!リスト13-22は、
リスト13-15のベクタから生成したイテレータと全く同様に、直接nextメソッドを呼び出すことで、
Counter構造体のイテレータ機能を使用できることをデモするテストを示しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { struct Counter { count: u32, } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { self.count += 1; if self.count < 6 { Some(self.count) } else { None } } } #[test] fn calling_next_directly() { let mut counter = Counter::new(); assert_eq!(counter.next(), Some(1)); assert_eq!(counter.next(), Some(2)); assert_eq!(counter.next(), Some(3)); assert_eq!(counter.next(), Some(4)); assert_eq!(counter.next(), Some(5)); assert_eq!(counter.next(), None); } }
リスト13-22: nextメソッド実装の機能をテストする
このテストは、counter変数に新しいCounterインスタンスを生成し、
それからイテレータにほしい動作が実装し終わっていることを実証しながら、nextを繰り返し呼び出しています:
1から5の値を返すことです。
他のIteratorトレイトメソッドを使用する
nextメソッドを定義してIteratorトレイトを実装したので、今では、標準ライブラリで定義されているように、
どんなIteratorトレイトメソッドのデフォルト実装も使えるようになりました。全てnextメソッドの機能を使っているからです。
例えば、何らかの理由で、Counterインスタンスが生成する値を取り、最初の値を飛ばしてから、
別のCounterインスタンスが生成する値と一組にし、各ペアを掛け算し、3で割り切れる結果だけを残し、
全結果の値を足し合わせたくなったら、リスト13-23のテストに示したように、そうすることができます:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { struct Counter { count: u32, } impl Counter { fn new() -> Counter { Counter { count: 0 } } } impl Iterator for Counter { // このイテレータはu32を生成します // Our iterator will produce u32s type Item = u32; fn next(&mut self) -> Option<Self::Item> { // カウントをインクリメントする。故に0から始まる // increment our count. This is why we started at zero. self.count += 1; // カウントが終わったかどうか確認する // check to see if we've finished counting or not. if self.count < 6 { Some(self.count) } else { None } } } #[test] fn using_other_iterator_trait_methods() { let sum: u32 = Counter::new().zip(Counter::new().skip(1)) .map(|(a, b)| a * b) .filter(|x| x % 3 == 0) .sum(); assert_eq!(18, sum); } }
リスト13-23: Counterイテレータに対していろんなIteratorトレイトのメソッドを使用する
zipは4組しか生成しないことに注意してください; 理論的な5番目の組の(5, None)は、
入力イテレータのどちらかがNoneを返したら、zipはNoneを返却するため、決して生成されることはありません。
nextメソッドの動作方法を指定し、標準ライブラリがnextを呼び出す他のメソッドにデフォルト実装を提供しているので、
これらのメソッド呼び出しは全て可能です。
入出力プロジェクトを改善する
このイテレータに関する新しい知識があれば、イテレータを使用してコードのいろんな場所をより明確で簡潔にすることで、
第12章の入出力プロジェクトを改善することができます。イテレータがConfig::new関数とsearch関数の実装を改善する方法に目を向けましょう。
イテレータを使用してcloneを取り除く
リスト12-6において、スライスに添え字アクセスして値をクローンすることで、Config構造体に値を所有させながら、
String値のスライスを取り、Config構造体のインスタンスを作るコードを追記しました。リスト13-24では、
リスト12-23のようなConfig::newの実装を再現しました:
ファイル名: src/lib.rs
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config { query, filename, case_sensitive })
}
}
リスト13-24: リスト12-23からConfig::new関数の再現
その際、将来的に除去する予定なので、非効率的なclone呼び出しを憂慮するなと述べました。
えっと、その時は今です!
引数argsにString要素のスライスがあるためにここでcloneが必要だったのですが、
new関数はargsを所有していません。Configインスタンスの所有権を返すためには、
Configインスタンスがその値を所有できるように、Configのqueryとfilenameフィールドから値をクローンしなければなりませんでした。
イテレータについての新しい知識があれば、new関数をスライスを借用する代わりに、
引数としてイテレータの所有権を奪うように変更することができます。スライスの長さを確認し、
特定の場所に添え字アクセスするコードの代わりにイテレータの機能を使います。これにより、
イテレータは値にアクセスするので、Config::new関数がすることが明確化します。
ひとたび、Config::newがイテレータの所有権を奪い、借用する添え字アクセス処理をやめたら、
cloneを呼び出して新しくメモリ確保するのではなく、イテレータからのString値をConfigにムーブできます。
返却されるイテレータを直接使う
入出力プロジェクトのsrc/main.rsファイルを開いてください。こんな見た目のはずです:
ファイル名: src/main.rs
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
// --snip--
}
リスト12-24のようなmain関数の冒頭をリスト13-25のコードに変更します。
これは、Config::newも更新するまでコンパイルできません。
ファイル名: src/main.rs
fn main() {
let config = Config::new(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
// --snip--
}
リスト13-25: env::argsの戻り値をConfig::newに渡す
env::args関数は、イテレータを返します!イテレータの値をベクタに集結させ、それからスライスをConfig::newに渡すのではなく、
今ではenv::argsから返ってくるイテレータの所有権を直接Config::newに渡しています。
次に、Config::newの定義を更新する必要があります。入出力プロジェクトのsrc/lib.rsファイルで、
Config::newのシグニチャをリスト13-26のように変えましょう。関数本体を更新する必要があるので、
それでもコンパイルはできません。
ファイル名: src/lib.rs
impl Config {
pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
// --snip--
リスト13-26: Config::newのシグニチャをイテレータを期待するように更新する
env::args関数の標準ライブラリドキュメントは、自身が返すイテレータの型は、std::env::Argsであると表示しています。
Config::new関数のシグニチャを更新したので、引数argsの型は、&[String]ではなく、
std::env::Argsになりました。argsの所有権を奪い、繰り返しを行うことでargsを可変化する予定なので、
args引数の仕様にmutキーワードを追記でき、可変にします。
添え字の代わりにIteratorトレイトのメソッドを使用する
次に、Config::newの本体を修正しましょう。標準ライブラリのドキュメントは、
std::env::ArgsがIteratorトレイトを実装していることにも言及しているので、
それに対してnextメソッドを呼び出せることがわかります!リスト13-27は、
リスト12-23のコードをnextメソッドを使用するように更新したものです:
ファイル名: src/lib.rs
fn main() {} use std::env; struct Config { query: String, filename: String, case_sensitive: bool, } impl Config { pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> { args.next(); let query = match args.next() { Some(arg) => arg, // クエリ文字列を取得しませんでした None => return Err("Didn't get a query string"), }; let filename = match args.next() { Some(arg) => arg, // ファイル名を取得しませんでした None => return Err("Didn't get a file name"), }; let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); Ok(Config { query, filename, case_sensitive }) } }
リスト13-27: Config::newの本体をイテレータメソッドを使うように変更する
env::argsの戻り値の1番目の値は、プログラム名であることを思い出してください。それは無視し、
次の値を取得したいので、まずnextを呼び出し、戻り値に対して何もしません。2番目に、
nextを呼び出してConfigのqueryフィールドに置きたい値を得ます。nextがSomeを返したら、
matchを使用してその値を抜き出します。Noneを返したら、十分な引数が与えられなかったということなので、
Err値で早期リターンします。filename値に対しても同じことをします。
イテレータアダプタでコードをより明確にする
入出力プロジェクトのsearch関数でも、イテレータを活用することができます。その関数はリスト12-19に示していますが、以下のリスト13-28に再掲します。
ファイル名: src/lib.rs
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
}
リスト13-28: リスト12-19のsearch関数の実装
イテレータアダプタメソッドを使用して、このコードをもっと簡潔に書くことができます。そうすれば、
可変な中間のresultsベクタをなくすこともできます。関数型プログラミングスタイルは、可変な状態の量を最小化することを好み、
コードを明瞭化します。可変な状態を除去すると、検索を同時並行に行うという将来的な改善をするのが、
可能になる可能性があります。なぜなら、resultsベクタへの同時アクセスを管理する必要がなくなるからです。
リスト13-29は、この変更を示しています:
ファイル名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents.lines()
.filter(|line| line.contains(query))
.collect()
}
リスト13-29: search関数の実装でイテレータアダプタのメソッドを使用する
search関数の目的は、queryを含むcontentsの行全てを返すことであることを思い出してください。
リスト13-19のfilter例に酷似して、このコードはfilterアダプタを使用してline.contains(query)が真を返す行だけを残すことができます。
それから、合致した行を別のベクタにcollectで集結させます。ずっと単純です!ご自由に、
同じ変更を行い、search_case_insensitive関数でもイテレータメソッドを使うようにしてください。
次の論理的な疑問は、自身のコードでどちらのスタイルを選ぶかと理由です: リスト13-28の元の実装とリスト13-29のイテレータを使用するバージョンです。 多くのRustプログラマは、イテレータスタイルを好みます。とっかかりが少し困難ですが、 いろんなイテレータアダプタとそれがすることの感覚を一度掴めれば、イテレータの方が理解しやすいこともあります。 いろんなループを少しずつもてあそんだり、新しいベクタを構築する代わりに、コードは、ループの高難度の目的に集中できるのです。 これは、ありふれたコードの一部を抽象化するので、イテレータの各要素が通過しなければならないふるい条件など、 このコードに独特の概念を理解しやすくなります。
ですが、本当に2つの実装は等価なのでしょうか?直観的な仮説は、より低レベルのループの方がより高速ということかもしれません。 パフォーマンスに触れましょう。
パフォーマンス比較: ループVSイテレータ
ループを使うべきかイテレータを使うべきか決定するために、search関数のうち、どちらのバージョンが速いか知る必要があります:
明示的なforループがあるバージョンと、イテレータのバージョンです。
サー・アーサー・コナン・ドイル(Sir Arthur Conan Doyle)の、
シャーロックホームズの冒険(The Adventures of Sherlock Homes)全体を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つの実装がパフォーマンス的にどう比較されるかを大まかに把握することだからです。
より包括的なベンチマークとするためには、いろんなサイズの様々なテキストをcontentsとして、異なる単語、異なる長さの単語をqueryとして、
他のあらゆる種類のバリエーションを確認するべきです。重要なのは: イテレータは、
高度な抽象化にも関わらず、低レベルのコードを自身で書いているかのように、ほぼ同じコードにコンパイルされることです。
イテレータは、Rustのゼロコスト抽象化の一つであり、これは、抽象化を使うことが追加の実行時オーバーヘッドを生まないことを意味しています。
このことは、C++の元の設計者であり実装者のビャーネ・ストロヴストルップ(Bjarne Stroustrup)が、
ゼロオーバーヘッドを「C++の基礎(2012)」で定義したのと類似しています。
一般的に、C++の実装は、ゼロオーバーヘッド原則を遵守します: 使用しないものには、支払わなくてよい。 さらに: 実際に使っているものに対して、コードをそれ以上うまく渡すことはできない。
別の例として、以下のコードは、オーディオデコーダから取ってきました。デコードアルゴリズムは、
線形予測数学演算を使用して、以前のサンプルの線形関数に基づいて未来の値を予測します。このコードは、
イテレータ連結をしてスコープにある3つの変数に計算を行っています: bufferというデータのスライス、
12のcoefficients(係数)の配列、qlp_shiftでデータをシフトする量です。この例の中で変数を宣言しましたが、
値は与えていません; このコードは、文脈の外では大して意味を持ちませんが、
それでもRustが高レベルな考えを低レベルなコードに翻訳する簡潔で現実的な例になっています:
let buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;
for i in 12..buffer.len() {
let prediction = coefficients.iter()
.zip(&buffer[i - 12..i])
.map(|(&c, &s)| c * s as i64)
.sum::<i64>() >> qlp_shift;
let delta = buffer[i];
buffer[i] = prediction as i32 + delta;
}
predictionの値を算出するために、このコードは、coefficientsの12の値を繰り返し、zipメソッドを使用して、
係数値を前のbufferの12の値と組にします。それから各組について、その値をかけ合わせ、結果を全て合計し、
合計のビットをqlp_shiftビット分だけ右にシフトさせます。
オーディオデコーダのようなアプリケーションの計算は、しばしばパフォーマンスに最も重きを置きます。
ここでは、イテレータを作成し、2つのアダプタを使用し、それから値を消費しています。
このRustコードは、どんな機械語コードにコンパイルされるのでしょうか?えー、執筆時点では、
手作業で書いたものと同じ機械語にコンパイルされます。coefficientsの値の繰り返しに対応するループは全く存在しません:
コンパイラは、12回繰り返しがあることを把握しているので、ループを「展開」します。
ループの展開は、ループ制御コードのオーバーヘッドを除去し、代わりにループの繰り返しごとに同じコードを生成する最適化です。
係数は全てレジスタに保存されます。つまり、値に非常に高速にアクセスします。実行時に配列の境界チェックをすることもありません。 コンパイラが適用可能なこれらの最適化全てにより、結果のコードは究極的に効率化されます。このことがわかったので、 もうイテレータとクロージャを恐れなしに使用することができますね!それらのおかげでコードは、高レベルだけれども、 そうすることに対して実行時のパフォーマンスを犠牲にしないようになります。
まとめ
クロージャとイテレータは、関数型言語の考えに着想を得たRustの機能です。低レベルのパフォーマンスで、 高レベルの考えを明確に表現するというRustの能力に貢献しています。クロージャとイテレータの実装は、 実行時のパフォーマンスが影響されないようなものです。これは、ゼロ代償抽象化を提供するのに努力を惜しまないRustの目標の一部です。
今や入出力プロジェクトの表現力を改善したので、プロジェクトを世界と共有するのに役に立つcargoの機能にもっと目を向けましょう。
CargoとCrates.ioについてより詳しく
今までCargoのビルド、実行、コードのテストを行うという最も基礎的な機能のみを使ってきましたが、 他にもできることはたくさんあります。この章では、そのような他のより高度な機能の一部を議論し、 以下のことをする方法をお見せしましょう:
- リリースプロファイルでビルドをカスタマイズする
- crates.ioでライブラリを公開する
- ワークスペースで巨大なプロジェクトを体系化する
- crates.ioからバイナリをインストールする
- 独自のコマンドを使用してCargoを拡張する
また、Cargoはこの章で講義する以上のこともできるので、機能の全解説を見るには、 ドキュメンテーションを参照されたし。
リリースプロファイルでビルドをカスタマイズする
Rustにおいて、リリースプロファイルとは、プログラマがコードのコンパイルオプションについてより制御可能にしてくれる、 定義済みのカスタマイズ可能なプロファイルです。各プロファイルは、それぞれ独立して設定されます。
Cargoには2つの主なプロファイルが存在します: devプロファイルは、cargo buildコマンドを実行したときに使用され、
releaseプロファイルは、cargo build --releaseコマンドを実行したときに使用されます。
devプロファイルは、開発中に役に立つデフォルト設定がなされており、releaseプロファイルは、
リリース用の設定がなされています。
これらのプロファイル名は、ビルドの出力で馴染みのある可能性があります:
$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
$ cargo build --release
Finished release [optimized] target(s) in 0.0 secs
このビルド出力で表示されているdevとreleaseは、コンパイラが異なるプロファイルを使用していることを示しています。
プロジェクトのCargo.tomlファイルに[profile.*]セクションが存在しない際に適用される各プロファイル用のデフォルト設定が、
Cargoには存在します。カスタマイズしたいプロファイル用の[profile.*]セクションを追加することで、
デフォルト設定の一部を上書きすることができます。例えば、こちらがdevとreleaseプロファイルのopt-level設定のデフォルト値です:
ファイル名: Cargo.toml
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
opt-level設定は、0から3の範囲でコンパイラがコードに適用する最適化の度合いを制御します。
最適化を多くかけると、コンパイル時間が延びるので、開発中に頻繁にコードをコンパイルするのなら、
たとえ出力結果のコードの動作速度が遅くなっても早くコンパイルが済んでほしいですよね。
これが、devのopt-levelのデフォルト設定が0になっている唯一の理由です。
コードのリリース準備ができたら、より長い時間をコンパイルにかけるのが最善の策です。
リリースモードでコンパイルするのはたった1回ですが、コンパイル結果のプログラムは何度も実行するので、
リリースモードでは、長いコンパイル時間と引き換えに、生成したコードが速く動作します。
そのため、releaseのopt-levelのデフォルト設定が3になっているのです。
デフォルト設定に対してCargo.tomlで異なる値を追加すれば、上書きすることができます。
例として、開発用プロファイルで最適化レベル1を使用したければ、以下の2行をプロジェクトのCargo.tomlファイルに追加できます:
ファイル名: Cargo.toml
[profile.dev]
opt-level = 1
このコードは、デフォルト設定の0を上書きします。こうすると、cargo buildを実行したときに、
devプロファイル用のデフォルト設定に加えて、Cargoはopt-levelの変更を適用します。
opt-levelを1に設定したので、Cargoはデフォルトよりは最適化を行いますが、リリースビルドほどではありません。
設定の選択肢と各プロファイルのデフォルト設定の一覧は、Cargoのドキュメンテーションを参照されたし。
Crates.ioにクレートを公開する
プロジェクトの依存としてcrates.ioのパッケージを使用しましたが、 自分のパッケージを公開することで他の人とコードを共有することもできます。 crates.ioのクレート登録所は、自分のパッケージのソースコードを配布するので、 主にオープンソースのコードをホストします。
RustとCargoは、公開したパッケージを人が使用し、そもそも見つけやすくしてくれる機能を有しています。 これらの機能の一部を次に語り、そして、パッケージの公開方法を説明します。
役に立つドキュメンテーションコメントを行う
パッケージを正確にドキュメントすることで、他のユーザがパッケージを使用する方法や、いつ使用すべきかを理解する手助けをすることになるので、
ドキュメンテーションを書くことに時間を費やす価値があります。第3章で、2連スラッシュ、//でRustのコードにコメントをつける方法を議論しました。
Rustには、ドキュメンテーション用のコメントも用意されていて、便利なことにドキュメンテーションコメントとして知られ、
HTMLドキュメントを生成します。クレートの実装法とは対照的にクレートの使用法を知ることに興味のあるプログラマ向けの、
公開API用のドキュメンテーションコメントの中身をこのHTMLは表示します。
ドキュメンテーションコメントは、2つではなく、3連スラッシュ、///を使用し、テキストを整形するMarkdown記法もサポートしています。
ドキュメント対象の要素の直前にドキュメンテーションコメントを配置してください。
リスト14-1は、my_crateという名のクレートのadd_one関数用のドキュメンテーションコメントを示しています:
ファイル名: src/lib.rs
/// Adds one to the number given.
/// 与えられた数値に1を足す。
///
/// # Examples
///
/// ```
/// let five = 5;
///
/// assert_eq!(6, my_crate::add_one(5));
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
リスト14-1: 関数のドキュメンテーションコメント
ここで、add_one関数がすることの説明を与え、Examplesというタイトルでセクションを開始し、
add_one関数の使用法を模擬するコードを提供しています。このドキュメンテーションコメントからcargo docを実行することで、
HTMLドキュメントを生成することができます。このコマンドはコンパイラとともに配布されているrustdocツールを実行し、
生成されたHTMLドキュメントをtarget/docディレクトリに配置します。
利便性のために、cargo doc --openを走らせれば、現在のクレートのドキュメント用のHTML(と、
自分のクレートが依存している全てのドキュメント)を構築し、その結果をWebブラウザで開きます。
add_one関数まで下り、図14-1に示したように、ドキュメンテーションコメントのテキストがどう描画されるかを確認しましょう:
図14-1: add_one関数のHTMLドキュメント
よく使われるセクション
# Examplesマークダウンのタイトルをリスト14-1で使用し、「例」というタイトルのセクションをHTMLに生成しました。
こちらがこれ以外にドキュメントでよくクレート筆者が使用するセクションです:
- Panics: ドキュメント対象の関数が
panic!する可能性のある筋書きです。プログラムをパニックさせたくない関数の使用者は、 これらの状況で関数が呼ばれないことを確かめる必要があります。 - Errors: 関数が
Resultを返すなら、起きうるエラーの種類とどんな条件がそれらのエラーを引き起こす可能性があるのか解説すると、 呼び出し側の役に立つので、エラーの種類によって処理するコードを変えて書くことができます。 - Safety: 関数が呼び出すのに
unsafe(unsafeについては第19章で議論します)なら、 関数が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
さて、例のassert_eq!がパニックするように、関数か例を変更し、再度cargo testを実行したら、
docテストが、例とコードがお互いに同期されていないことを捕捉するところを目撃するでしょう!
含まれている要素にコメントする
docコメントの別スタイル、//!は、コメントに続く要素にドキュメンテーションを付け加えるのではなく、
コメントを含む要素にドキュメンテーションを付け加えます。典型的には、クレートのルートファイル(慣例的には、src/lib.rs)内部や、
モジュールの内部で使用して、クレートやモジュール全体にドキュメントをつけます。
例えば、add_one関数を含むmy_crateクレートの目的を解説するドキュメンテーションを追加したいのなら、
//!で始まるドキュメンテーションコメントをsrc/lib.rsファイルの先頭につけることができます。
リスト14-2に示したようにですね:
ファイル名: src/lib.rs
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.
//! #自分のクレート
//!
//! `my_crate`は、ユーティリティの集まりであり、特定の計算をより便利に行うことができます。
/// Adds one to the number given.
// --snip--
リスト14-2: 全体としてmy_crateクレートにドキュメントをつける
//!で始まる最後の行のあとにコードがないことに注目してください。///ではなく、//!でコメントを開始しているので、
このコメントに続く要素ではなく、このコメントを含む要素にドキュメントをつけているわけです。
今回の場合、このコメントを含む要素はsrc/lib.rsファイルであり、クレートのルートです。
これらのコメントは、クレート全体を解説しています。
cargo doc --openを実行すると、これらのコメントは、my_crateのドキュメントの最初のページ、
クレートの公開要素のリストの上部に表示されます。図14-2のようにですね:
図14-2: クレート全体を解説するコメントを含むmy_crateの描画されたドキュメンテーション
要素内のドキュメンテーションコメントは、特にクレートやモジュールを解説するのに有用です。 コンテナの全体の目的を説明し、クレートの使用者がクレートの体系を理解する手助けをするのに使用してください。
pub useで便利な公開APIをエクスポートする
第7章において、modキーワードを使用してモジュールにコードを体系化する方法、pubキーワードで要素を公開にする方法、
useキーワードで要素をスコープに導入する方法について講義しました。しかしながら、クレートの開発中に、
自分にとって意味のある構造は、ユーザにはあまり便利ではない可能性があります。複数階層を含む階層で、
自分の構造体を体系化したくなるかもしれませんが、それから階層の深いところで定義した型を使用したい人は、
型が存在することを見つけ出すのに困難を伴う可能性もあります。また、そのような人は、
use my_crate::UsefulTypeの代わりにuse my_crate::some_module::another_module::UsefulType;と入力するのを煩わしく感じる可能性もあります。
自分の公開APIの構造は、クレートを公開する際に考慮すべき点です。自分のクレートを使用したい人は、 自分よりもその構造に馴染みがないですし、クレートのモジュール階層が大きければ、使用したい部分を見つけるのが困難になる可能性があります。
嬉しいお知らせは、構造が他人が他のライブラリから使用するのに便利ではない場合、内部的な体系を再構築する必要はないということです:
代わりに、要素を再エクスポートし、pub useで自分の非公開構造とは異なる公開構造にできます。
再エクスポートは、ある場所の公開要素を一つ取り、別の場所で定義されているかのように別の場所で公開します。
例えば、芸術的な概念をモデル化するためにartという名のライブラリを作ったとしましょう。
このライブラリ内には、2つのモジュールがあります: PrimaryColorとSecondaryColorという名前の2つのenumを含む、
kindsモジュールとmixという関数を含むutilsモジュールです。リスト14-3のようにですね:
ファイル名: src/lib.rs
//! # Art
//!
//! A library for modeling artistic concepts.
//! #芸術
//!
//! 芸術的な概念をモデル化するライブラリ。
pub mod kinds {
/// The primary colors according to the RYB color model.
/// RYBカラーモデルによる主色
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
/// RYBカラーモデルによる副色
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
use kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
///2つの主色を同じ割合で混合し、副色にする
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --snip--
}
}
リスト14-3: kindsとutilsモジュールに体系化される要素を含むartライブラリ
図14-3は、cargo docにより生成されるこのクレートのドキュメンテーションの最初のページがどんな見た目になるか示しています:
図14-3: kindsとutilsモジュールを列挙するartのドキュメンテーションのトップページ
PrimaryColor型もSecondaryColor型も、mix関数もトップページには列挙されていないことに注意してください。
kindsとutilsをクリックしなければ、参照することができません。
このライブラリに依存する別のクレートは、現在定義されているモジュール構造を指定して、
artの要素をインポートするuse文が必要になるでしょう。リスト14-4は、
artクレートからPrimaryColorとmix要素を使用するクレートの例を示しています:
ファイル名: src/main.rs
extern crate art;
use art::kinds::PrimaryColor;
use art::utils::mix;
fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
リスト14-4: 内部構造がエクスポートされてartクレートの要素を使用するクレート
リスト14-4はartクレートを使用していますが、このコードの筆者は、PrimaryColorがkindsモジュールにあり、
mixがutilsモジュールにあることを理解しなければなりませんでした。artクレートのモジュール構造は、
artクレートの使用者よりも、artクレートに取り組む開発者などに関係が深いです。
クレートの一部をkindsモジュールとutilsモジュールに体系化する内部構造は、artクレートの使用方法を理解しようとする人には、
何も役に立つ情報を含んでいません。代わりに、開発者がどこを見るべきか計算する必要があるので、
artクレートのモジュール構造は混乱を招き、また、開発者はモジュール名をuse文で指定しなければならないので、
この構造は不便です。
公開APIから内部体系を除去するために、リスト14-3のartクレートコードを変更し、pub use文を追加して、
最上位で要素を再エクスポートすることができます。リスト14-5みたいにですね:
ファイル名: src/lib.rs
//! # Art
//!
//! A library for modeling artistic concepts.
pub use kinds::PrimaryColor;
pub use kinds::SecondaryColor;
pub use utils::mix;
pub mod kinds {
// --snip--
}
pub mod utils {
// --snip--
}
リスト14-5: pub use文を追加して要素を再エクスポートする
このクレートに対してcargo docが生成するAPIドキュメンテーションは、これで図14-4のようにトップページに再エクスポートを列挙しリンクするので、
PrimaryColor型とSecondaryColor型とmix関数を見つけやすくします。
図14-4: 再エクスポートを列挙するartのドキュメンテーションのトップページ
artクレートのユーザは、それでも、リスト14-4にデモされているように、リスト14-3の内部構造を見て使用することもできますし、
リスト14-5のより便利な構造を使用することもできます。リスト14-6に示したようにですね:
ファイル名: src/main.rs
extern crate art;
use art::PrimaryColor;
use art::mix;
fn main() {
// --snip--
}
リスト14-6: artクレートの再エクスポートされた要素を使用するプログラム
ネストされたモジュールがたくさんあるような場合、最上位階層でpub useにより型を再エクスポートすることは、
クレートの使用者の経験に大きな違いを生みます。
役に立つ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にローカルに保存します。 このトークンは、秘密です: 他人とは共有しないでください。なんらかの理由で他人と実際に共有してしまったら、 古いものを破棄してcrates.ioで新しいトークンを生成するべきです。
新しいクレートにメタデータを追加する
アカウントはできたので、公開したいクレートがあるとしましょう。公開前に、
Cargo.tomlファイルの[package]セクションに追加することでクレートにメタデータを追加する必要があるでしょう。
クレートには、独自の名前が必要でしょう。クレートをローカルで作成している間、
クレートの名前はなんでもいい状態でした。ところが、crates.ioのクレート名は、
最初に来たもの勝ちの精神で付与されていますので、一旦クレート名が取られてしまったら、
その名前のクレートを他の人が公開することは絶対できません。もう使われているか、
サイトで使いたい名前を検索してください。まだなら、Cargo.tomlファイルの[package]以下の名前を編集して、
名前を公開用に使ってください。以下のように:
ファイル名: Cargo.toml
[package]
name = "guessing_game"
たとえ、独自の名前を選択していたとしても、この時点でcargo publishを実行すると、警告とエラーが出ます:
$ cargo publish
Updating registry `https://github.com/rust-lang/crates.io-index`
warning: manifest has no description, license, license-file, documentation,
homepage or repository.
(警告: マニフェストに説明、ライセンス、ライセンスファイル、ドキュメンテーション、ホームページ、
リポジトリがありません)
--snip--
error: api errors: missing or empty metadata fields: description, license.
(エラー: APIエラー: 存在しないメタデータフィールド: description, license)
原因は、大事な情報を一部入れていないからです: 説明とライセンスは、 他の人があなたのクレートは何をし、どんな条件の元で使っていいのかを知るために必要なのです。 このエラーを解消するには、Cargo.tomlファイルにこの情報を入れ込む必要があります。
1文か2文程度の説明をつけてください。これは、検索結果に表示されますからね。
licenseフィールドには、ライセンス識別子を与える必要があります。
Linux団体の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 newが追加した筆者の詳細、説明、ライセンスが追加され、
公開準備のできたプロジェクト用のCargo.tomlファイルは以下のような見た目になっていることでしょう:
ファイル名: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
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 registry `https://github.com/rust-lang/crates.io-index`
Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
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 [unoptimized + debuginfo] target(s) in 0.19 secs
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
おめでとうございます!Rustコミュニティとコードを共有し、誰でもあなたのクレートを依存として簡単に追加できます。
既存のクレートの新バージョンを公開する
クレートに変更を行い、新バージョンをリリースする準備ができたら、
Cargo.tomlファイルに指定されたversionの値を変更し、再公開します。
セマンティックバージョンルールを使用して加えた変更の種類に基づいて次の適切なバージョン番号を決定してください。
そして、cargo publishを実行し、新バージョンをアップロードします。
cargo yankでCrates.ioからバージョンを削除する
以前のバージョンのクレートを削除することはできないものの、将来のプロジェクトがこれに新たに依存することを防ぐことはできます。 これは、なんらかの理由により、クレートバージョンが壊れている場合に有用です。そのような場面において、 Cargoはクレートバージョンの 取り下げ(yank) をサポートしています。
バージョンを取り下げると、既存のプロジェクトは、引き続きダウンロードしたりそのバージョンに依存したりしつづけられますが、 新規プロジェクトが新しくそのバージョンに依存しだすことは防止されます。つまるところ、取り下げは、 すでにCargo.lockが存在するプロジェクトは壊さないが、将来的に生成されたCargo.lockファイルは 取り下げられたバージョンを使わない、ということを意味します。
あるバージョンのクレートを取り下げるには、cargo yankを実行し、取り下げたいバージョンを指定します:
$ cargo yank --vers 1.0.1
--undoをコマンドに付与することで、取り下げを取り消し、再度あるバージョンにプロジェクトを依存させ始めることもできます:
$ cargo yank --vers 1.0.1 --undo
取り下げは、コードの削除は一切しません。例として、取り下げ機能は、誤ってアップロードされた秘密鍵を削除するためのものではありません。 もしそうなってしまったら、即座に秘密鍵をリセットしなければなりません。
Cargoのワークスペース
第12章で、バイナリクレートとライブラリクレートを含むパッケージを構築しました。プロジェクトの開発が進むにつれて、 ライブラリクレートの肥大化が続き、その上で複数のライブラリクレートにパッケージを分割したくなることでしょう。 この場面において、Cargoはワークスペースという協調して開発された関連のある複数のパッケージを管理するのに役立つ機能を提供しています。
ワークスペースを生成する
ワークスペースは、同じCargo.lockと出力ディレクトリを共有する一連のパッケージです。
ワークスペースを使用したプロジェクトを作成し、ワークスペースの構造に集中できるよう、瑣末なコードを使用しましょう。
ワークスペースを構築する方法は複数ありますが、一般的な方法を提示しましょう。バイナリ1つとライブラリ2つを含むワークスペースを作ります。
バイナリは、主要な機能を提供しますが、2つのライブラリに依存しています。
一方のライブラリは、add_one関数を提供し、2番目のライブラリは、add_two関数を提供します。
これら3つのクレートが同じワークスペースの一部になります。ワークスペース用の新しいディレクトリを作ることから始めましょう:
$ mkdir add
$ cd add
次にaddディレクトリにワークスペース全体を設定するCargo.tomlファイルを作成します。
このファイルには、他のCargo.tomlファイルで見かけるような[package]セクションやメタデータはありません。
代わりにバイナリクレートへのパスを指定することでワークスペースにメンバを追加させてくれる[workspace]セクションから開始します;
今回の場合、そのパスはadderです:
ファイル名: Cargo.toml
[workspace]
members = [
"adder",
]
次に、addディレクトリ内でcargo newを実行することでadderバイナリクレートを作成しましょう:
$ cargo new --bin adder
Created binary (application) `adder` project
この時点で、cargo buildを走らせるとワークスペースを構築できます。addディレクトリに存在するファイルは、
以下のようになるはずです:
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
ワークスペースには、コンパイルした生成物を置けるように最上位にtargetのディレクトリがあります;
adderクレートにはtargetディレクトリはありません。
adderディレクトリ内部からcargo buildを走らせることになっていたとしても、コンパイルされる生成物は、
add/adder/targetではなく、add/targetに落ち着くでしょう。ワークスペースのクレートは、
お互いに依存しあうことを意味するので、Cargoはワークスペースのtargetディレクトリをこのように構成します。
各クレートがtargetディレクトリを持っていたら、各クレートがワークスペースの他のクレートを再コンパイルし、
targetディレクトリに生成物がある状態にしなければならないでしょう。一つのtargetディレクトリを共有することで、
クレートは不必要な再ビルドを回避できるのです。
ワークスペース内に2番目のクレートを作成する
次に、ワークスペースに別のメンバクレートを作成し、add-oneと呼びましょう。
最上位のCargo.tomlを変更してmembersリストでadd-oneパスを指定するようにしてください:
ファイル名: Cargo.toml
[workspace]
members = [
"adder",
"add-one",
]
それから、add-oneという名前のライブラリクレートを生成してください:
$ cargo new add-one --lib
Created library `add-one` project
これで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
#![allow(unused)] fn main() { pub fn add_one(x: i32) -> i32 { x + 1 } }
ワークスペースにライブラリクレートが存在するようになったので、バイナリクレートadderをライブラリクレートのadd-oneに依存させられます。
まず、add-oneへのパス依存をadder/Cargo.tomlに追加する必要があります:
ファイル名: adder/Cargo.toml
[dependencies]
add-one = { path = "../add-one" }
Cargoはワークスペースのクレートが、お互いに依存しているとは想定していないので、 クレート間の依存関係について明示する必要があります。
次に、adderクレートのadd-oneクレートからadd_one関数を使用しましょう。adder/src/main.rsファイルを開き、
冒頭にextern crate行を追加して新しいadd-oneライブラリクレートをスコープに導入してください。
それからmain関数を変更し、add_one関数を呼び出します。リスト14-7のようにですね:
ファイル名: adder/src/main.rs
extern crate add_one;
fn main() {
let num = 10;
// こんにちは世界!{}+1は{}!
println!("Hello, world! {} plus one is {}!", num, add_one::add_one(num));
}
リスト14-7: adderクレートからadd-oneライブラリクレートを使用する
最上位の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 [unoptimized + debuginfo] target(s) in 0.68 secs
addディレクトリからバイナリクレートを実行するには、-p引数とパッケージ名をcargo runと共に使用して、
使用したいワークスペースのパッケージを指定する必要があります:
$ cargo run -p adder
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
これにより、adder/src/main.rsのコードが実行され、これはadd_oneクレートに依存しています。
ワークスペースの外部クレートに依存する
ワークスペースには、各クレートのディレクトリそれぞれにCargo.lockが存在するのではなく、
ワークスペースの最上位階層にただ一つのCargo.lockが存在するだけのことに注目してください。
これにより、全クレートが全依存の同じバージョンを使用していることが確認されます。
randクレートをadder/Cargo.tomlとadd-one/Cargo.tomlファイルに追加すると、
Cargoは両者をあるバージョンのrandに解決し、それを一つのCargo.lockに記録します。
ワークスペースの全クレートに同じ依存を使用させるということは、
ワークスペースのクレートが相互に互換性を常に維持するということになります。
add-one/Cargo.tomlファイルの[dependencies]セクションにrandクレートを追加して、
add-oneクレートでrandクレートを使用できます:
ファイル名: add-one/Cargo.toml
[dependencies]
rand = "0.3.14"
これで、add-one/src/lib.rsファイルにextern crate rand;を追加でき、
addディレクトリでcargo buildを実行することでワークスペース全体をビルドすると、
randクレートを持ってきてコンパイルするでしょう:
$ cargo build
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading rand v0.3.14
--snip--
Compiling rand v0.3.14
Compiling add-one v0.1.0 (file:///projects/add/add-one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 10.18 secs
さて、最上位のCargo.lockは、randに対するadd-oneの依存の情報を含むようになりました。
ですが、randはワークスペースのどこかで使用されているにも関わらず、それぞれのCargo.tomlファイルにも、
randを追加しない限り、ワークスペースの他のクレートでそれを使用することはできません。
例えば、adderクレートのadder/src/main.rsファイルにextern crate rand;を追加すると、
エラーが出ます:
$ cargo build
Compiling adder v0.1.0 (file:///projects/add/adder)
error: use of unstable library feature 'rand': use `rand` from crates.io (see
issue #27703)
(エラー: 不安定なライブラリの機能'rand'を使用しています: crates.ioの`rand`を使用してください)
--> adder/src/main.rs:1:1
|
1 | extern crate rand;
これを修正するには、adderクレートのCargo.tomlファイルを編集し、同様にそのクレートがrandに依存していることを示してください。
adderクレートをビルドすると、randをCargo.lockのadderの依存一覧に追加しますが、
randのファイルが追加でダウンロードされることはありません。Cargoが、ワークスペースのrandを使用するどのクレートも、
同じバージョンを使っていることを確かめてくれるのです。ワークスペース全体でrandの同じバージョンを使用することにより、
複数のコピーが存在しないのでスペースを節約し、ワークスペースのクレートが相互に互換性を維持することを確かめます。
ワークスペースにテストを追加する
さらなる改善として、add_oneクレート内にadd_one::add_one関数のテストを追加しましょう:
ファイル名: add-one/src/lib.rs
#![allow(unused)] fn main() { 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
Compiling add-one v0.1.0 (file:///projects/add/add-one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.27 secs
Running target/debug/deps/add_one-f0253159197f7841
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/adder-f88af9d2cc175a5e
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests add-one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
出力の最初の区域が、add-oneクレートのit_worksテストが通ったことを示しています。
次の区域には、adderクレートにはテストが見つからなかったことが示され、
さらに最後の区域には、add-oneクレートにドキュメンテーションテストは見つからなかったと表示されています。
このような構造をしたワークスペースでcargo testを走らせると、ワークスペースの全クレートのテストを実行します。
-pフラグを使用し、テストしたいクレートの名前を指定することで最上位ディレクトリから、
ワークスペースのある特定のクレート用のテストを実行することもできます:
$ cargo test -p add-one
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/add_one-b3235fea9a156f74
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests add-one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
この出力は、cargo testがadd-oneクレートのテストのみを実行し、adderクレートのテストは実行しなかったことを示しています。
ワークスペースのクレートを https://crates.io/ に公開したら、ワークスペースのクレートは個別に公開される必要があります。
cargo publishコマンドには--allフラグや-pフラグはないので、各クレートのディレクトリに移動して、
ワークスペースの各クレートをcargo publishして、公開しなければなりません。
鍛錬を積むために、add-oneクレートと同様の方法でワークスペースにadd-twoクレートを追加してください!
プロジェクトが肥大化してきたら、ワークスペースの使用を考えてみてください: 大きな一つのコードの塊よりも、 微細で個別のコンポーネントの方が理解しやすいです。またワークスペースにクレートを保持することは、 同時に変更されることが多いのなら、協調しやすくなることにも繋がります。
cargo installでCrates.ioからバイナリをインストールする
cargo installコマンドにより、バイナリクレートをローカルにインストールし、使用することができます。
これは、システムパッケージを置き換えることを意図したものではありません。即ち、
Rustの開発者が、他人がcrates.ioに共有したツールをインストールするのに便利な方法を意味するのです。
バイナリターゲットを持つパッケージのみインストールできることに注意してください。バイナリターゲットとは、
クレートがsrc/main.rsファイルやバイナリとして指定された他のファイルを持つ場合に生成される実行可能なプログラムのことであり、
単独では実行不可能なものの、他のプログラムに含むのには適しているライブラリターゲットとは一線を画します。
通常、クレートには、READMEファイルに、クレートがライブラリかバイナリターゲットか、両方をもつかという情報があります。
cargo installでインストールされるバイナリは全て、インストールのルートのbinフォルダに保持されます。
Rustをrustupを使用し、独自の設定を何も行なっていなければ、このディレクトリは、$HOME/.cargo/binになります。
cargo installでインストールしたプログラムを実行できるようにするためには、そのディレクトリが$PATHに含まれていることを確かめてください。
例えば、第12章で、ファイルを検索するripgrepというgrepツールのRust版があることに触れました。
ripgrepをインストールしたかったら、以下を実行することができます:
$ cargo install ripgrep
Updating registry `https://github.com/rust-lang/crates.io-index`
Downloading ripgrep v0.3.2
--snip--
Compiling ripgrep v0.3.2
Finished release [optimized + debuginfo] target(s) in 97.91 secs
Installing ~/.cargo/bin/rg
出力の最後の行が、インストールされたバイナリの位置と名前を示していて、ripgrepの場合、rgです。
インストールディレクトリが$PATHに存在する限り、前述したように、rg --helpを走らせて、
より高速でRustらしいファイル検索ツールを使用し始めることができます!
独自のコマンドで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において、参照とスマートポインタにはもう1つ違いがあります。参照はデータを借用するだけのポインタなのです。 対照的に多くの場合、スマートポインタは指しているデータを所有します。
私達はすでに、この本の中でいくつかのスマートポインタに遭遇してきました。例えば第8章のStringやVec<T>です。ただし、私達はそれらをスマートポインタとは呼んでいませんでした。
これらの型がどちらもスマートポインタに数えられるのは、あるメモリを所有しそれを弄ることができるからです。
また、メタデータ(キャパシティなど)や追加の能力、あるいは保証(Stringならデータが常に有効なUTF-8であると保証することなど)もあります。
スマートポインタは普通、構造体を使用して実装されています。スマートポインタを通常の構造体と区別する特徴は、
スマートポインタがDerefとDropトレイトを実装していることです。Derefトレイトにより、スマートポインタ構造体のインスタンスは、
参照のように振る舞うことができるので、参照あるいはスマートポインタのどちらとも動作するコードを書くことができます。
Dropトレイトにより、スマートポインタのインスタンスがスコープを外れた時に走るコードをカスタマイズすることができます。
この章では、どちらのトレイトについても議論し、これらのトレイトがスマートポインタにとって重要な理由を説明します。
スマートポインタパターンがRustにおいてよく使われる一般的なデザインパターンであることを考えれば、この章で既存のスマートポインタを全て取り扱うことなどできません。 多くのライブラリに独自のスマートポインタがあり、自分だけのスマートポインタを書くことさえできるのです。 ここでは標準ライブラリの最もありふれたスマートポインタを取り扱っていきます。
- ヒープに値を確保する
Box<T> - 複数の所有権を可能にする参照カウント型の
Rc<T> RefCell<T>を通してアクセスされ、コンパイル時ではなく実行時に借用規則を強制する型のRef<T>とRefMut<T>
さらに、内部可変性パターンも扱います。そこでは不変な型が、内部の値を変更するためのAPIを公開するのです。 また、循環参照についても議論します。つまり、循環参照によっていかにしてメモリがリークするのか、そしてどうやってそれを回避するのかを議論します。
さあ、飛び込みましょう!
ヒープのデータを指すBox<T>を使用する
最も素直なスマートポインタはボックスであり、その型はBox<T>と記述されます。
ボックスにより、スタックではなくヒープにデータを格納することができます。スタックに残るのは、
ヒープデータへのポインタです。スタックとヒープの違いを再確認するには、第4章を参照されたし。
ボックスは、データをスタックの代わりにヒープに格納する以外は、パフォーマンスのオーバーヘッドはありません。 しかし、特別な能力がたくさんあるわけでもありません。以下のような場面で最もよく使われるでしょう。
- コンパイル時にはサイズを知ることができない型があり、正確なサイズを要求する文脈でその型の値を使用する時
- 多くのデータがあり、その所有権を移したいが、その際にデータがコピーされないようにしたい時
- 値を所有する必要があり、特定の型であることではなく、特定のトレイトを実装する型であることのみ気にかけている時
「ボックスで再帰的な型を可能にする」節で1つ目の場合について実際に説明します。 2番目の場合、多くのデータの所有権を転送するには、データがスタック上でコピーされるので、長い時間がかかり得ます。 この場面でパフォーマンスを向上させるために、多くのデータをヒープ上にボックスとして格納することができます。 そして、小さなポインタのデータのみがスタック上でコピーされる一方、それが参照しているデータはヒープ上の1箇所に留まります。 3番目のケースはトレイトオブジェクトとして知られています。第17章の「トレイトオブジェクトで異なる型の値を許容する」の節は、 すべてその話題に捧げられています。 従って、ここで学ぶことは第17章でもまた使うことになります!
Box<T>を使ってヒープにデータを格納する
Box<T>のこのユースケースを議論する前に、Box<T>の記法と、Box<T>内に格納された値を読み書きする方法について講義しましょう。
リスト15-1は、ボックスを使用してヒープにi32の値を格納する方法を示しています。
ファイル名: src/main.rs
fn main() { let b = Box::new(5); println!("b = {}", b); }
リスト15-1: ボックスを使用してi32の値をヒープに格納する
変数bを定義してBoxの値を保持します。Boxは値5を指し、値5はヒープに確保されています。このプログラムは、b = 5と出力するでしょう。つまりこの場合、このデータがスタックにあるのと同じような方法でボックスのデータにアクセスできます。
所有された値と全く同じでスコープを抜けるとき、実際bはmainの終わりで抜けるのですが、
ボックスはメモリから解放されます。メモリの解放は(スタックに格納されている)ボックスと(ヒープに格納されている)指しているデータに対して起きます。
ヒープに単独の値を置いても嬉しいことはほとんどないので、このように単独でボックスを使用することはあまりありません。
単独のi32のような値はデフォルトではスタックに置かれます。ほとんどの場合ではその方が適切です。
ボックスのおかげで定義できるようになる型を見てみましょう。ボックスがなければそれらの型は定義できません。
ボックスで再帰的な型を可能にする
コンパイル時にコンパイラが知っておかねばならないのは、ある型が占有する領域の大きさです。コンパイル時にサイズがわからない型の1つ として 再帰的な型があります。この型の値は、値の一部として同じ型の他の値を持つ場合があります。値のこうしたネストは、理論的には無限に続く可能性があるので、コンパイラは再帰的な型の値が必要とする領域を知ることができないのです。 しかしながら、ボックスのサイズはわかっているので、再帰的な型の定義にボックスを挟むことで再帰的な型を作ることができます。
コンスリストは関数型プログラミング言語では一般的なデータ型ですが、これを再帰的な型の例として探究しましょう。 我々が定義するコンスリストは、再帰を除けば素直です。故に、これから取り掛かる例に現れる概念は、 再帰的な型が関わるもっと複雑な場面に遭遇したときには必ず役に立つでしょう。
コンスリストについてもっと詳しく
コンスリストは、Lispプログラミング言語とその方言に由来するデータ構造です。Lispでは、
cons関数("construct function"の省略形です)は2つの引数から新しいペアを構成します。
この引数は通常、単独の値と別のペアからなります。これらのペアを含むペアがリストをなすのです。
cons関数という概念は、より一般的な関数型プログラミングの俗語にもなっています。"to cons x onto y"はコンテナyの先頭に要素xを置くことで新しいコンテナのインスタンスを生成することを意味します。
コンスリストの各要素は、2つの要素を含みます。現在の要素の値と次の要素です。リストの最後の要素は、
Nilと呼ばれる値だけを含み、次の要素を持ちません。コンスリストは、繰り返しcons関数を呼び出すことで生成されます。
繰り返しの基底ケースを示すのに標準的に使われる名前はNilです。これは第6章の"null"や"nil"の概念とは異なることに注意してください。
"null"や"nil"は、無効だったり存在しない値です。
関数型プログラミング言語ではコンスリストは頻繁に使われますが、Rustではあまり使用されないデータ構造です。
Rustで要素のリストがあるときはほとんど、Vec<T>を使用するのがよりよい選択になります。
より複雑な他の再帰的なデータ型は様々な場面で役に立ちます。しかしコンスリストから始めることで、
ボックスのおかげで再帰的なデータ型を定義できるわけを、あまり気を散らすことなく調べることができるのです。
リスト15-2には、コンスリストのenum定義が含まれています。このコードはまだコンパイルできないことに注意してください。
List型のサイズが分からないからです。
これについてはこの後説明します。
ファイル名: src/main.rs
enum List {
Cons(i32, List),
Nil,
}
リスト15-2: i32値のコンスリストデータ構造を表すenumを定義する最初の試行
注釈: この例のために
i32値だけを保持するコンスリストを実装します。第10章で議論したように、 ジェネリクスを使用してどんな型の値も格納できるコンスリストを定義して実装することもできたでしょう。
このList型を使用してリスト1, 2, 3を格納すると、リスト15-3のコードのような見た目になるでしょう。
ファイル名: src/main.rs
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
リスト15-3: List enumを使用してリスト1, 2, 3を格納する
最初のCons値は、1と別のList値を保持しています。このList値は別のCons値で、
2とまた別のList値を保持しています。このList値はまたまた別のCons値で、
3とList値を保持していますが、このList値でついにNilになります。Nilはリストの終端を通知する非再帰的な列挙子です。
リスト15-3のコードをコンパイルしようとすると、リスト15-4に示したエラーが出ます。
error[E0072]: recursive type `List` has infinite size
(エラー: 再帰的な型`List`は無限のサイズです)
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ recursive type has infinite size
2 | Cons(i32, List),
| ----- recursive without indirection
|
= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to
make `List` representable
(助言: 間接参照(例: `Box`、`Rc`、あるいは`&`)をどこかに挿入して、`List`を表現可能にしてください)
リスト15-4: 再帰的なenumを定義しようとすると得られるエラー
エラーは、この型は「無限のサイズである」と表示しています。理由は、再帰的な列挙子を含むListを定義したからです。
つまり、Listは自身の別の値を直接保持しているのです。結果として、コンパイラはList値を格納するのに必要な領域が計算できません。
このエラーが出た理由を少し噛み砕きましょう。まず、非再帰的な型の値を格納するのに必要な領域をどうコンパイラが決定しているかを見ましょう。
非再帰的な型のサイズを計算する
第6章でenum定義を議論した時にリスト6-2で定義したMessage enumを思い出してください。
#![allow(unused)] fn main() { enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } }
Message値一つにメモリを確保するために必要な領域を決定するために、コンパイラは、
各列挙子を見てどの列挙子が最も領域を必要とするかを確認します。コンパイラは、
Message::Quitは全く領域を必要とせず、Message::Moveはi32値を2つ格納するのに十分な領域が必要、などと確かめます。
ただ1つの列挙子しか使用されないので、Message値一つが必要とする最大の領域は、
最大の列挙子を格納するのに必要になる領域です。
これをコンパイラがリスト15-2のList enumのような再帰的な型が必要とする領域を決定しようとする時に起こることと比較してください。
コンパイラはCons列挙子を見ることから始めます。この列挙子には、型i32値が一つと型Listの値が一つ保持されます。
故に、Consは1つのi32とListのサイズに等しい領域を必要とします。Listが必要とするメモリ量を計算するのに、
コンパイラはCons列挙子から列挙子を観察します。Cons列挙子は型i32を1つと型Listの値1つを保持し、
この過程は無限に続きます。図15-1のようにですね。
図15-1: 無限のCons列挙子からなる無限のList
Box<T>で既知のサイズの再帰的な型を得る
コンパイラは、再帰的に定義された型に必要なメモリ量を計算できないので、リスト15-4ではエラーを返します。 しかし、エラーにはこんな役立つ提案が含まれているのです。
= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to
make `List` representable
この提案において「間接参照」は、値を直接格納するのではなく、データ構造を変更して値を間接的に格納することを意味します。これは値の代わりに値へのポインタを格納することによって可能になります。
Box<T>はポインタなので、コンパイラにはBox<T>が必要とする領域が必ずわかります。すなわち、ポインタのサイズは指しているデータの量に左右されません。つまり、別のList値を直接置く代わりに、
Cons列挙子の中にBox<T>を配置することができます。Box<T>は、
Cons列挙子の中ではなく、ヒープに置かれる次のList値を指します。概念的には、
依然として我々のリストは他のリストを「保持する」リストによって作られたものです。
しかし、今やこの実装は、要素をお互いの中に配置するというより、隣り合うように配置するような感じになります。
リスト15-2のList enumの定義とリスト15-3のListの使用をリスト15-5のコードに変更することができ、
これはコンパイルが通ります。
ファイル名: src/main.rs
enum List { Cons(i32, Box<List>), Nil, } use List::{Cons, Nil}; fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); }
リスト15-5: 既知のサイズにするためにBox<T>を使用するListの定義
Cons列挙子は、1つのi32のサイズに加えてボックスのポインタデータを格納する領域を必要とするでしょう。
Nil列挙子は値を格納しないので、Cons列挙子よりも必要な領域は小さいです。これで、
どんなList値もi321つのサイズに加えてボックスのポインタデータのサイズを必要とすることがわかりました。
ボックスを使うことで無限に続く再帰の連鎖を断ち切ったので、コンパイラはList値を格納するのに必要なサイズを計算できます。
図15-2は、Cons列挙子の今の見た目を示しています。
図15-2: ConsがBoxを保持しているので、無限にサイズがあるわけではないList
ボックスは、間接参照とヒープメモリ確保だけを提供します。他のスマートポインタ型に見られるような別の特別な能力は何もありません。 これらの特別な能力が招くパフォーマンスのオーバーヘッドもないので、 コンスリストのように間接参照だけが必要な機能である場合には便利でしょう。 より多くのボックスのユースケースは第17章でもお見かけするでしょう。
Box<T>型がスマートポインタなのは、Derefトレイトを実装しているからです。
このトレイトによりBox<T>の値を参照のように扱うことができます。
Box<T>値がスコープを抜けると、ボックスが参照しているヒープデータも片付けられます。これはDropトレイト実装のおかげです。
これら2つのトレイトをより詳しく探究しましょう。これら2つのトレイトは、他のスマートポインタ型が提供する機能にとってさらに重要なものです。それらついてはこの章の残りで議論します。
Derefトレイトでスマートポインタを普通の参照のように扱う
Derefトレイトを実装することで、参照外し演算子の*(掛け算やグロブ演算子とは違います)の振る舞いをカスタマイズできます。
Derefを実装してスマートポインタを普通の参照みたいに扱えるようにすれば、
参照に対して処理を行うコードを書いて、そのコードをスマートポインタに対しても使うことができるのです。
まずは、参照外し演算子が普通の参照に対して動作するところを見ましょう。それから、Box<T>のように振る舞う独自の型を定義してみましょう。
参照とは異なり、新しく定義した型には参照外し演算子を使えません。その理由を確認します。
Derefトレイトを実装すればスマートポインタは参照と同じように機能するので、そのやり方を調べましょう。
そして、Rustには参照外し型強制という機能があり、その機能のおかげで参照やスマートポインタをうまく使うことができるので、それに目を向けてみましょう。
参照外し演算子で値までポインタを追いかける
普通の参照は1種のポインタであり、ポインタはどこか他の場所に格納された値への矢印と見なすことができます。
リスト15-6では、i32値への参照を生成してから参照外し演算子を使ってデータまで参照を辿ります。
ファイル名: src/main.rs
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
リスト15-6: 参照外し演算子を使用して参照をi32値まで追いかける
変数xはi32値の5を保持しています。yはxへの参照として設定します。xは5に等しいとアサートできます。
しかしながら、yの値に関するアサートを行いたい場合、*yを使用して参照が指している値まで追いかけなければなりません(そのため参照外しです)。
一旦yの参照を外せば、yが指している整数値にアクセスできます。これは5と比較可能です。
代わりにassert_eq!(5, y);と書こうとしたら、こんなコンパイルエラーが出るでしょう。
error[E0277]: the trait bound `{integer}: std::cmp::PartialEq<&{integer}>` is
not satisfied
(エラー: トレイト境界`{integer}: std::cmp::PartialEq<&{integer}>`は満たされていません)
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^^ can't compare `{integer}` with `&{integer}`
|
= help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for
`{integer}`
(助言: トレイト`std::cmp::PartialEq<&{integer}>`は`{integer}`に対して実装されていません)
数値と数値への参照の比較は許されていません。これらは異なる型だからです。参照外し演算子を使用して、 参照が指している値まで追いかけなければならないのです。
Box<T>を参照のように使う
リスト15-6のコードを、参照の代わりにBox<T>を使うように書き直すことができます。
参照外し演算子は、リスト15-7に示したように動くでしょう。
ファイル名: src/main.rs
fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
リスト15-7: Box<i32>に対して参照外し演算子を使用する
リスト15-7とリスト15-6の唯一の違いは、ここではyが、xの値を指す参照ではなく、
xの値を指すボックスのインスタンスとして設定されている点にあります。
最後のアサートでは、参照外し演算子を使ってボックスのポインタを辿ることができます。これはyが参照だった時と同じやり方です。
参照外し演算子が使える以上Box<T>には特別な何かがあるので、次はそれについて調べることにします。そのために、独自にボックス型を定義します。
独自のスマートポインタを定義する
標準ライブラリが提供しているBox<T>型に似たスマートポインタを作りましょう。そうすれば、スマートポインタがそのままだと
参照と同じ様には振る舞わないことがわかります。それから、どうすれば参照外し演算子を使えるようになるのか見てみましょう。
Box<T>型は突き詰めると(訳註:データがヒープに置かれることを無視すると)1要素のタプル構造体のような定義になります。なのでリスト15-8ではそのようにMyBox<T>型を定義しています。
また、Box<T>に定義されたnew関数に対応するnew関数も定義しています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } }
リスト15-8: MyBox<T>型を定義する
MyBoxという構造体を定義し、ジェネリック引数のTを宣言しています。この型にどんな型の値も持たせたいからです。
MyBox型は型Tの要素を1つ持つタプル構造体です。MyBox::new関数は型Tの引数を1つ取り、
渡した値を持つMyBoxのインスタンスを返します。
試しにリスト15-7のmain関数をリスト15-8に追加し、定義したMyBox<T>型をBox<T>の代わりに使うよう変更してみてください。
コンパイラはMyBoxを参照外しする方法がわからないので、リスト15-9のコードはコンパイルできません。
ファイル名: src/main.rs
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
リスト15-9: 参照とBox<T>を使ったのと同じようにMyBox<T>を使おうとする
こちらが結果として出るコンパイルエラーです。
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
(エラー: 型`MyBox<{integer}>`は参照外しできません)
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
MyBox<T>の参照を外すことはできません。そのための実装を与えていないからです。*演算子で参照外しできるようにするには、
Derefトレイトを実装します。
Derefトレイトを実装して型を参照のように扱う
第10章で議論したように、トレイトを実装するにはトレイトの必須メソッドに実装を与える必要があります。
Derefトレイトは標準ライブラリで提供されており、derefという1つのメソッドの実装を要求します。derefはselfを借用し、
内部のデータへの参照を返すメソッドです。
リスト15-10には、MyBoxの定義に付け足すDerefの実装が含まれています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::ops::Deref; struct MyBox<T>(T); impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } }
リスト15-10: MyBox<T>にDerefを実装する
type Target = T;という記法は、Derefトレイトが使用する関連型を定義しています。関連型はまた少し違ったやり方でジェネリック引数を宣言するためのものですが、今は気にする必要はありません。第19章でより詳しく扱います。
derefメソッドの本体は&self.0だけなので、derefが返すのは私達が*演算子でアクセスしたい値への参照なわけです。
リスト15-9のMyBox<T>に*を呼び出すmain関数はこれでコンパイルでき、アサートも通ります!
Derefトレイトがないと、コンパイラは&参照しか参照外しできません。
derefメソッドのおかげで、コンパイラはDerefを実装している型の値を取り、derefメソッドを呼ぶことで、参照外しが可能な&参照を得られるようになります。
リスト15-9に*yを入力した時、水面下でRustは実際にはこのようなコードを走らせていました。
*(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メソッドの呼び出しが、提供した型を引数が必要とする型に変換します。
参照外し型強制は、関数やメソッド呼び出しを書くプログラマが&や*を多くの明示的な参照や参照外しとして追記する必要がないように、
Rustに追加されました。また、参照外し型強制のおかげで参照あるいはスマートポインタのどちらかで動くコードをもっと書くことができます。
参照外し型強制が実際に動いていることを確認するため、リスト15-8で定義したMyBox<T>と、
リスト15-10で追加したDerefの実装を使用しましょう。リスト15-11は、
文字列スライス引数のある関数の定義を示しています:
ファイル名: src/main.rs
#![allow(unused)] fn main() { fn hello(name: &str) { println!("Hello, {}!", name); } }
リスト15-11: 型&strの引数nameのあるhello関数
hello関数は、文字列スライスを引数として呼び出すことができます。例えば、hello("Rust")などです。
参照外し型強制により、helloを型MyBox<String>の値への参照とともに呼び出すことができます。リスト15-12のようにですね:
ファイル名: src/main.rs
use std::ops::Deref; struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } fn hello(name: &str) { println!("Hello, {}!", name); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&m); }
リスト15-12: helloをMyBox<String>値とともに呼び出し、参照外し型強制のおかげで動く
ここで、hello関数を引数&mとともに呼び出しています。この引数は、MyBox<String>値への参照です。
リスト15-10でMyBox<T>にDerefトレイトを実装したので、コンパイラはderefを呼び出すことで、
&MyBox<String>を&Stringに変換できるのです。標準ライブラリは、Stringに文字列スライスを返すDerefの実装を提供していて、
この実装は、DerefのAPIドキュメンテーションに載っています。コンパイラはさらにderefを呼び出して、
&Stringを&strに変換し、これはhello関数の定義と合致します。
Rustに参照外し型強制が実装されていなかったら、リスト15-12のコードの代わりにリスト15-13のコードを書き、
型&MyBox<String>の値でhelloを呼び出さなければならなかったでしょう。
ファイル名: src/main.rs
use std::ops::Deref; struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } fn hello(name: &str) { println!("Hello, {}!", name); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&(*m)[..]); }
リスト15-13: Rustに参照外し型強制がなかった場合に書かなければならないであろうコード
(*m)がMyBox<String>をStringに参照外ししています。そして、&と[..]により、
文字列全体と等しいStringの文字列スライスを取り、helloのシグニチャと一致するわけです。
参照外し型強制のないコードは、これらの記号が関係するので、読むのも書くのも理解するのもより難しくなります。
参照外し型強制により、コンパイラはこれらの変換を自動的に扱えるのです。
Derefトレイトが関係する型に定義されていると、コンパイラは、型を分析し必要なだけDeref::derefを使用して、
参照を得、引数の型と一致させます。Deref::derefが挿入される必要のある回数は、コンパイル時に解決されるので、
参照外し型強制を活用するための実行時の代償は何もありません。
参照外し型強制が可変性と相互作用する方法
Derefトレイトを使用して不変参照に対して*をオーバーライドするように、
DerefMutトレイトを使用して可変参照の*演算子をオーバーライドできます。
以下の3つの場合に型やトレイト実装を見つけた時にコンパイラは、参照外し型強制を行います:
T: Deref<Target=U>の時、&Tから&UT: DerefMut<Target=U>の時、&mut Tから&mut UT: Deref<Target=U>の時、&mut Tから&U
前者2つは、可変性を除いて一緒です。最初のケースは、&Tがあり、Tが何らかの型UへのDerefを実装しているなら、
透過的に&Uを得られると述べています。2番目のケースは、同じ参照外し型強制が可変参照についても起こることを述べています。
3番目のケースはもっと巧妙です: Rustはさらに、可変参照を不変参照にも型強制するのです。ですが、逆はできません: 不変参照は、絶対に可変参照に型強制されないのです。借用規則により、可変参照があるなら、 その可変参照がそのデータへの唯一の参照に違いありません(でなければ、プログラムはコンパイルできません)。 1つの可変参照を1つの不変参照に変換することは、借用規則を絶対に破壊しません。 不変参照を可変参照にするには、そのデータへの不変参照がたった1つしかないことが必要ですが、 借用規則はそれを保証してくれません。故に、不変参照を可変参照に変換することが可能であるという前提を敷けません。
Dropトレイトで片付け時にコードを走らせる
スマートポインタパターンにとって重要な2番目のトレイトは、Dropであり、
これのおかげで値がスコープを抜けそうになった時に起こることをカスタマイズできます。
どんな型に対してもDropトレイトの実装を提供することができ、指定したコードは、
ファイルやネットワーク接続などのリソースを解放するのに活用できます。
Dropをスマートポインタの文脈で導入しています。Dropトレイトの機能は、ほぼ常にスマートポインタを実装する時に使われるからです。
例えば、Box<T>はDropをカスタマイズしてボックスが指しているヒープの領域を解放しています。
ある言語では、プログラマがスマートポインタのインスタンスを使い終わる度にメモリやリソースを解放するコードを呼ばなければなりません。 忘れてしまったら、システムは詰め込みすぎになりクラッシュする可能性があります。Rustでは、 値がスコープを抜ける度に特定のコードが走るよう指定でき、コンパイラはこのコードを自動的に挿入します。 結果として、特定の型のインスタンスを使い終わったプログラムの箇所全部にクリーンアップコードを配置するのに配慮する必要はありません。 それでもリソースをリークすることはありません。
Dropトレイトを実装することで値がスコープを抜けた時に走るコードを指定してください。
Dropトレイトは、selfへの可変参照を取るdropという1つのメソッドを実装する必要があります。
いつRustがdropを呼ぶのか確認するために、今はprintln!文のあるdropを実装しましょう。
リスト15-14は、唯一の独自の機能が、インスタンスがスコープを抜ける時にDropping CustomSmartPointer!と出力するだけの、
CustomSmartPointer構造体です。この例は、コンパイラがいつdrop関数を走らせるかをデモしています。
ファイル名: src/main.rs
struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { // CustomSmartPointerをデータ`{}`とともにドロップするよ 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."); // CustomSmartPointerが生成された }
リスト15-14: クリーンアップコードを配置するDropトレイトを実装するCustomSmartPointer構造体
Dropトレイトは、初期化処理に含まれるので、インポートする必要はありません。
CustomSmartPointerにDropトレイトを実装し、println!を呼び出すdropメソッドの実装を提供しています。
drop関数の本体は、自分の型のインスタンスがスコープを抜ける時に走らせたいあらゆるロジックを配置する場所です。
ここで何らかのテキストを出力し、コンパイラがいつdropを呼ぶのかデモしています。
mainで、CustomSmartPointerのインスタンスを2つ作り、それからCustomSmartPointers created.と出力しています。
mainの最後で、CustomSmartPointerのインスタンスはスコープを抜け、コンパイラは最後のメッセージを出力しながら、
dropメソッドに置いたコードを呼び出します。dropメソッドを明示的に呼び出す必要はなかったことに注意してください。
このプログラムを実行すると、以下のような出力が出ます:
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
インスタンスがスコープを抜けた時に指定したコードを呼び出しながらコンパイラは、dropを自動的に呼び出してくれました。
変数は、生成されたのと逆の順序でドロップされるので、dはcより先にドロップされました。
この例は、dropメソッドの動き方を見た目で案内するだけですが、通常は、メッセージ出力ではなく、
自分の型が走らせる必要のあるクリーンアップコードを指定するでしょう。
std::mem::dropで早期に値をドロップする
残念ながら、自動的なdrop機能を無効化することは、単純ではありません。通常、dropを無効化する必要はありません;
Dropトレイトの最重要な要点は、自動的に考慮されることです。ですが、時として、値を早期に片付けたくなる可能性があります。
一例は、ロックを管理するスマートポインタを使用する時です: 同じスコープの他のコードがロックを獲得できるように、
ロックを解放するdropメソッドを強制的に走らせたくなる可能性があります。Rustは、
Dropトレイトのdropメソッドを手動で呼ばせてくれません; スコープが終わる前に値を強制的にドロップさせたいなら、
代わりに標準ライブラリが提供するstd::mem::drop関数を呼ばなければなりません。
リスト15-14のmain関数を変更して手動でDropトレイトのdropメソッドを呼び出そうとしたら、
コンパイルエラーになるでしょう。リスト15-15のようにですね:
ファイル名: src/main.rs
fn main() {
let c = CustomSmartPointer { data: String::from("some data") };
println!("CustomSmartPointer created.");
c.drop();
// mainの終端の前にCustomSmartPointerがドロップされた
println!("CustomSmartPointer dropped before the end of main.");
}
リスト15-15: Dropトレイトからdropメソッドを手動で呼び出し、早期に片付けようとする
このコードをコンパイルしてみようとすると、こんなエラーが出ます:
error[E0040]: explicit use of destructor method
(エラー: デストラクタメソッドを明示的に使用しています)
--> src/main.rs:14:7
|
14 | c.drop();
| ^^^^ explicit destructor calls not allowed
明示的にdropを呼び出すことは許されていないことをこのエラーメッセージは述べています。
エラーメッセージはデストラクタという専門用語を使っていて、これは、
インスタンスを片付ける関数の一般的なプログラミング専門用語です。デストラクタは、
コンストラクタに類似していて、これはインスタンスを生成します。Rustのdrop関数は、
1種の特定のデストラクタです。
コンパイラはそれでも、mainの終端で値に対して自動的にdropを呼び出すので、dropを明示的に呼ばせてくれません。
コンパイラが2回同じ値を片付けようとするので、これは二重解放エラーになるでしょう。
値がスコープを抜けるときにdropが自動的に挿入されるのを無効化できず、dropメソッドを明示的に呼ぶこともできません。
よって、値を早期に片付けさせる必要があるなら、std::mem::drop関数を使用できます。
std::mem::drop関数は、Dropトレイトのdropメソッドとは異なります。
早期に強制的にドロップさせたい値を引数で渡すことで呼びます。この関数は初期化処理に含まれているので、
リスト15-15のmainを変更してdrop関数を呼び出せます。リスト15-16のようにですね:
ファイル名: src/main.rs
struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomSmartPointer!"); } } fn main() { let c = CustomSmartPointer { data: String::from("some data") }; println!("CustomSmartPointer created."); drop(c); // CustomSmartPointerはmainが終わる前にドロップされた println!("CustomSmartPointer dropped before the end of main."); }
リスト15-16: 値がスコープを抜ける前に明示的にドロップするためにstd::mem::dropを呼び出す
このコードを実行すると、以下のように出力されます:
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.テキストの間に出力されるので、
dropメソッドのコードがその時点で呼び出されてcをドロップしたことを示しています。
Dropトレイト実装で指定されたコードをいろんな方法で使用し、片付けを便利で安全にすることができます:
例を挙げれば、これを使用して独自のメモリアロケータを作ることもできるでしょう!DropトレイトとRustの所有権システムがあれば、
コンパイラが自動的に行うので、片付けを覚えておく必要はなくなります。
まだ使用中の値を間違って片付けてしまうことに起因する問題を心配する必要もなくて済みます:
参照が常に有効であると確認してくれる所有権システムが、値が最早使用されなくなった時にdropが1回だけ呼ばれることを保証してくれるのです。
これでBox<T>とスマートポインタの特徴の一部を調査したので、標準ライブラリに定義されている他のスマートポインタをいくつか見ましょう。
Rc<T>は、参照カウント方式のスマートポインタ
大多数の場合、所有権は明らかです: 一体どの変数が与えられた値を所有しているかわかるのです。 ところが、単独の値が複数の所有者を持つ可能性のある場合もあります。例えば、グラフデータ構造では、 複数の辺が同じノードを指す可能性があり、概念的にそのノードはそれを指す全ての辺に所有されるわけです。 指す辺がなくならない限り、ノードは片付けられるべきではありません。
複数の所有権を可能にするため、RustにはRc<T>という型があり、これは、reference counting(参照カウント)の省略形です。
Rc<T>型は、値がまだ使用中かどうか決定する値への参照の数を追跡します。値への参照が0なら、どの参照も無効にすることなく、
値は片付けられます。
Rc<T>を家族部屋のテレビと想像してください。1人がテレビを見に部屋に入ったら、テレビをつけます。
他の人も部屋に入ってテレビを観ることができます。最後の人が部屋を離れる時、
もう使用されていないので、テレビを消します。他の人がまだ観ているのに誰かがテレビを消したら、
残りのテレビ視聴者が騒ぐでしょう!
ヒープにプログラムの複数箇所で読む何らかのデータを確保したいけれど、
コンパイル時にはどの部分が最後にデータを使用し終わるか決定できない時にRc<T>型を使用します。
どの部分が最後に終わるかわかっているなら、
単にその部分をデータの所有者にして、コンパイル時に強制される普通の所有権ルールが効果を発揮するでしょう。
Rc<T>は、シングルスレッドの筋書きで使用するためだけのものであることに注意してください。
第16章で並行性について議論する時に、マルチスレッドプログラムで参照カウントをする方法を講義します。
Rc<T>でデータを共有する
リスト15-5のコンスリストの例に回帰しましょう。Box<T>を使って定義したことを思い出してください。
今回は、両方とも3番目のリストの所有権を共有する2つのリストを作成します。
これは概念的には図15-3のような見た目になります:
図15-3: 3番目のリスト、aの所有権を共有する2つのリスト、bとc
5と10を含むリストaを作ります。さらにもう2つリストを作ります: 3で始まるbと4で始まるcです。
bとcのどちらもそれから5と10を含む最初のaリストに続きます。換言すれば、
どちらのリストも5と10を含む最初のリストを共有しています。
Listの定義を使用してBox<T>とともにこの筋書きを実装しようとしても、うまくいきません。
リスト15-17のようにですね:
ファイル名: src/main.rs
enum List {
Cons(i32, Box<List>),
Nil,
}
use 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));
}
リスト15-17: 3番目のリストの所有権を共有しようとするBox<T>を使った2つのリストを存在させることはできないとデモする
このコードをコンパイルすると、こんなエラーが出ます:
error[E0382]: use of moved value: `a`
--> src/main.rs:13:30
|
12 | let b = Cons(3, Box::new(a));
| - value moved here
13 | let c = Cons(4, Box::new(a));
| ^ value used here after move
|
= note: move occurs because `a` has type `List`, which does not implement
the `Copy` trait
Cons列挙子は、保持しているデータを所有するので、bリストを作成する時に、
aがbにムーブされ、bがaを所有します。それからcを作る際に再度aを使用しようとすると、
aはムーブ済みなので、できないわけです。
Consの定義を代わりに参照を保持するように変更することもできますが、そうしたら、
ライフタイム引数を指定しなければなりません。ライフタイム引数を指定することで、
リストの各要素が最低でもリスト全体と同じ期間だけ生きることを指定することになります。
例えば、借用チェッカーはlet a = Cons(10, &Nil);をコンパイルさせてくれません。
一時的なNil値が、aが参照を得られるより前にドロップされてしまうからです。
代わりに、Listの定義をリスト15-18のように、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にならない限りデータは片付けられません。
ファイル名: src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use 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)); }
リスト15-18: Rc<T>を使用するListの定義
初期化処理に含まれていないので、use文を追加してRc<T>をスコープに導入する必要があります。
mainで5と10を保持するリストを作成し、aの新しいRc<List>に格納しています。それから、
bとcを作成する際に、Rc::clone関数を呼び出し、引数としてaのRc<List>への参照を渡しています。
Rc::clone(&a)ではなく、a.clone()を呼ぶこともできますが、Rustのしきたりは、この場合Rc::cloneを使うことです。
Rc::cloneの実装は、多くの型のclone実装のように、全てのデータのディープコピーをすることではありません。
Rc::cloneの呼び出しは、参照カウントをインクリメントするだけであり、時間はかかりません。
データのディープコピーは時間がかかることもあります。参照カウントにRc::cloneを使うことで、
視覚的にディープコピーをする類のクローンと参照カウントを増やす種類のクローンを区別することができます。
コード内でパフォーマンスの問題を探す際、ディープコピーのクローンだけを考慮し、Rc::cloneの呼び出しを無視できるのです。
Rc<T>をクローンすると、参照カウントが増える
aのRc<List>への参照を作ったりドロップする毎に参照カウントが変化するのが確かめられるように、
リスト15-18の動く例を変更しましょう。
リスト15-19で、リストcを囲む内側のスコープができるようmainを変更します;
そうすれば、cがスコープを抜けるときに参照カウントがどう変化するか確認できます。
ファイル名: src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); // a生成後のカウント = {} println!("count after creating a = {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); // b生成後のカウント = {} println!("count after creating b = {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); // c生成後のカウント = {} println!("count after creating c = {}", Rc::strong_count(&a)); } // cがスコープを抜けた後のカウント = {} println!("count after c goes out of scope = {}", Rc::strong_count(&a)); }
リスト15-19: 参照カウントを出力する
プログラム内で参照カウントが変更される度に、参照カウントを出力します。参照カウントは、
Rc::strong_count関数を呼び出すことで得られます。Rc<T>型にはweak_countもあるので、
この関数はcountではなくstrong_countと命名されています; weak_countの使用目的は、
「循環参照を回避する」節で確かめます。
このコードは、以下の出力をします:
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トレイトの実装が自動的に参照カウントを減らします。
この例でわからないことは、bそしてaが、mainの終端でスコープを抜ける時に、カウントが0になり、
その時点でRc<List>が完全に片付けられることです。Rc<T>を使用すると、単独の値に複数の所有者を持たせることができ、
所有者のいずれかが存在している限り、値が有効であり続けることをカウントは保証します。
不変参照経由で、Rc<T>は読み取り専用にプログラムの複数箇所間でデータを共有させてくれます。
Rc<T>が複数の可変参照を存在させることも許可してくれたら、第4章で議論した借用ルールの1つを侵害する虞があります:
同じ場所への複数の可変借用は、データ競合や矛盾を引き起こすことがあるのです。しかし、
データを可変化する能力はとても有用です!次の節では、内部可変性パターンと、
Rc<T>と絡めて使用してこの不変性制限を手がけられるRefCell<T>型について議論します。
RefCell<T>と内部可変性パターン
内部可変性は、そのデータへの不変参照がある時でさえもデータを可変化できるRustでのデザインパターンです:
普通、この行動は借用規則により許可されません。データを可変化するために、このパターンは、データ構造内でunsafeコードを使用して、
可変性と借用を支配するRustの通常の規則を捻じ曲げています。まだ、unsafeコードについては講義していません;
第19章で行います。たとえ、コンパイラが保証できなくても、借用規則に実行時に従うことが保証できる時、
内部可変性パターンを使用した型を使用できます。関係するunsafeコードはそうしたら、安全なAPIにラップされ、
外側の型は、それでも不変です。
内部可変性パターンに従うRefCell<T>型を眺めてこの概念を探究しましょう。
RefCell<T>で実行時に借用規則を強制する
Rc<T>と異なり、RefCell<T>型は、保持するデータに対して単独の所有権を表します。では、
どうしてRefCell<T>がBox<T>のような型と異なるのでしょうか?第4章で学んだ借用規則を思い出してください:
- いかなる時も(以下の両方ではなく、)1つの可変参照かいくつもの不変参照のどちらかが可能になる
- 参照は常に有効でなければならない。
参照とBox<T>では、借用規則の不変条件は、コンパイル時に強制されています。RefCell<T>では、
これらの不変条件は、実行時に強制されます。参照でこれらの規則を破ったら、コンパイルエラーになりました。
RefCell<T>でこれらの規則を破ったら、プログラムはパニックし、終了します。
コンパイル時に借用規則を精査することの利点は、エラーが開発過程の早い段階で捕捉されることと、 あらかじめ全ての分析が終わるので、実行パフォーマンスへの影響がないことです。それらの理由により、 多くの場合でコンパイル時に借用規則を精査することが最善の選択肢であり、これが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>が不変でも、RefCell<T>内の値を可変化できる。
不変な値の中の値を可変化することは、内部可変性パターンです。内部可変性が有用になる場面を見て、 それが可能になる方法を調査しましょう。
内部可変性: 不変値への可変借用
借用規則の結果は、不変値がある時、可変で借用することはできないということです。 例えば、このコードはコンパイルできません:
fn main() {
let x = 5;
let y = &mut x;
}
このコードをコンパイルしようとしたら、以下のようなエラーが出るでしょう:
error[E0596]: cannot borrow immutable local variable `x` as mutable
(エラー: 不変なローカル変数`x`を可変で借用することはできません)
--> src/main.rs:3:18
|
2 | let x = 5;
| - consider changing this to `mut x`
3 | let y = &mut x;
| ^ cannot borrow mutably
ですが、メソッド内で値が自身を可変化するけれども、他のコードにとっては、
不変に見えることが有用な場面もあります。その値のメソッドの外のコードは、その値を可変化することはできないでしょう。
RefCell<T>を使うことは、内部可変性を取得する能力を得る1つの方法です。しかし、
RefCell<T>は借用規則を完全に回避するものではありません: コンパイラの借用チェッカーは、内部可変性を許可し、
借用規則は代わりに実行時に精査されます。この規則を侵害したら、コンパイルエラーではなくpanic!になるでしょう。
RefCell<T>を使用して不変値を可変化する実践的な例に取り組み、それが役に立つ理由を確認しましょう。
内部可変性のユースケース: モックオブジェクト
テストダブルは、テスト中に別の型の代わりに使用される型の一般的なプログラミングの概念です。 モックオブジェクトは、テスト中に起きることを記録するテストダブルの特定の型なので、 正しい動作が起きたことをアサートできます。
編注: テストダブルとは、ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品のこと。
Rustには、他の言語でいうオブジェクトは存在せず、また、他の言語のように標準ライブラリにモックオブジェクトの機能が組み込まれてもいません。 ですが、同じ目的をモックオブジェクトとして提供する構造体を作成することは確実にできます。
以下が、テストを行う筋書きです: 値を最大値に対して追跡し、現在値がどれくらい最大値に近いかに基づいてメッセージを送信するライブラリを作成します。 このライブラリは、ユーザが行うことのできるAPIコールの数の割り当てを追跡するのに使用することができるでしょう。
作成するライブラリは、値がどれくらい最大に近いかと、いつどんなメッセージになるべきかを追いかける機能を提供するだけです。
このライブラリを使用するアプリケーションは、メッセージを送信する機構を提供すると期待されるでしょう:
アプリケーションは、アプリケーションにメッセージを置いたり、メールを送ったり、テキストメッセージを送るなどできるでしょう。
ライブラリはその詳細を知る必要はありません。必要なのは、提供するMessengerと呼ばれるトレイトを実装している何かなのです。
リスト15-20は、ライブラリのコードを示しています:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub trait Messenger { fn send(&self, msg: &str); } pub struct LimitTracker<'a, T: 'a + Messenger> { messenger: &'a T, value: usize, max: usize, } impl<'a, T> LimitTracker<'a, T> where T: Messenger { pub fn new(messenger: &T, max: usize) -> LimitTracker<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 >= 0.75 && percentage_of_max < 0.9 { // 警告: 割り当ての75%以上を使用してしまいました self.messenger.send("Warning: You've used up over 75% of your quota!"); } else if percentage_of_max >= 0.9 && percentage_of_max < 1.0 { // 切迫した警告: 割り当ての90%以上を使用してしまいました self.messenger.send("Urgent warning: You've used up over 90% of your quota!"); } else if percentage_of_max >= 1.0 { // エラー: 割り当てを超えています self.messenger.send("Error: You are over your quota!"); } } } }
リスト15-20: 値が最大値にどれくらい近いかを追跡し、特定のレベルの時に警告するライブラリ
このコードの重要な部分の1つは、Messengerトレイトには、selfへの不変参照とメッセージのテキストを取るsendというメソッドが1つあることです。
これが、モックオブジェクトが持つ必要のあるインターフェイスなのです。もう1つの重要な部分は、
LimitTrackerのset_valueメソッドの振る舞いをテストしたいということです。value引数に渡すものを変えることができますが、
set_valueはアサートを行えるものは何も返してくれません。LimitTrackerをMessengerトレイトを実装する何かと、
maxの特定の値で生成したら、valueに異なる数値を渡した時にメッセンジャーは適切なメッセージを送ると指示されると言えるようになりたいです。
sendを呼び出す時にメールやテキストメッセージを送る代わりに送ると指示されたメッセージを追跡するだけのモックオブジェクトが必要です。
モックオブジェクトの新規インスタンスを生成し、モックオブジェクトを使用するLimitTrackerを生成し、
LimitTrackerのset_valueを呼び出し、それからモックオブジェクトに期待しているメッセージがあることを確認できます。
リスト15-21は、それだけをするモックオブジェクトを実装しようとするところを示しますが、借用チェッカーが許可してくれません:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[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); } } }
リスト15-21: 借用チェッカーが許可してくれないMockMessengerを実装しようとする
このテストコードはStringのVecで送信すると指示されたメッセージを追跡するsent_messagesフィールドのあるMockMessenger構造体を定義しています。
また、空のメッセージリストから始まる新しいMockMessenger値を作るのを便利にしてくれる関連関数のnewも定義しています。
それからMockMessengerにMessengerトレイトを実装しているので、LimitTrackerにMockMessengerを与えられます。
sendメソッドの定義で引数として渡されたメッセージを取り、sent_messagesのMockMessengerリストに格納しています。
テストでは、max値の75%以上になる何かにvalueをセットしろとLimitTrackerが指示される時に起きることをテストしています。
まず、新しいMockMessengerを生成し、空のメッセージリストから始まります。そして、
新しいLimitTrackerを生成し、新しいMockMessengerの参照と100というmax値を与えます。
LimitTrackerのset_valueメソッドは80という値で呼び出し、これは100の75%を上回っています。
そして、MockMessengerが追いかけているメッセージのリストが、今は1つのメッセージを含んでいるはずとアサートします。
ところが、以下のようにこのテストには1つ問題があります:
error[E0596]: cannot borrow immutable field `self.sent_messages` as mutable
(エラー: 不変なフィールド`self.sent_messages`を可変で借用できません)
--> src/lib.rs:52:13
|
51 | fn send(&self, message: &str) {
| ----- use `&mut self` here to make mutable
52 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ cannot mutably borrow immutable field
sendメソッドはselfへの不変参照を取るので、MockMessengerを変更してメッセージを追跡できないのです。
代わりに&mut selfを使用するというエラーテキストからの提言を選ぶこともできないのです。
そうしたら、sendのシグニチャが、Messengerトレイト定義のシグニチャと一致しなくなるからです(気軽に試してエラーメッセージを確認してください)。
これは、内部可変性が役に立つ場面なのです!sent_messagesをRefCell<T>内部に格納し、
そうしたらsendメッセージは、sent_messagesを変更して見かけたメッセージを格納できるようになるでしょう。
リスト15-22は、それがどんな感じかを示しています:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[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(75); assert_eq!(mock_messenger.sent_messages.borrow().len(), 1); } } }
リスト15-22: 外側の値は不変と考えられる一方でRefCell<T>で内部の値を可変化する
さて、sent_messagesフィールドは、Vec<String>ではなく、型RefCell<Vec<String>>になりました。
new関数で、空のベクタの周りにRefCell<Vec<String>>を新しく作成しています。
sendメソッドの実装については、最初の引数はそれでもselfへの不変借用で、トレイト定義と合致しています。
RefCell<Vec<String>>のborrow_mutをself.sent_messagesに呼び出し、
RefCell<Vec<String>>の中の値への可変参照を得て、これはベクタになります。
それからベクタへの可変参照にpushを呼び出して、テスト中に送られるメッセージを追跡しています。
行わなければならない最後の変更は、アサート内部にあります: 内部のベクタにある要素の数を確認するため、
RefCell<Vec<String>>にborrowを呼び出し、ベクタへの不変参照を得ています。
RefCell<T>の使用法を見かけたので、動作の仕方を深掘りしましょう!
RefCell<T>で実行時に借用を追いかける
不変および可変参照を作成する時、それぞれ&と&mut記法を使用します。RefCell<T>では、
borrowとborrow_mutメソッドを使用し、これらはRefCell<T>に所属する安全なAPIの一部です。
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>の実装は実行時にパニックするでしょう。リスト15-23は、リスト15-22のsend実装に対する変更を示しています。
同じスコープで2つの可変借用が活動するようわざと生成し、RefCell<T>が実行時にこれをすることを阻止してくれるところを説明しています。
ファイル名: src/lib.rs
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));
}
}
リスト15-23: 同じスコープで2つの可変参照を生成してRefCell<T>がパニックすることを確かめる
borrow_mutから返ってきたRefMut<T>スマートポインタに対して変数one_borrowを生成しています。
そして、同様にして変数two_borrowにも別の可変借用を生成しています。これにより同じスコープで2つの可変参照ができ、
これは許可されないことです。このテストを自分のライブラリ用に走らせると、リスト15-23のコードはエラーなくコンパイルできますが、
テストは失敗するでしょう:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at
'already borrowed: BorrowMutError', src/libcore/result.rs:906:4
(スレッド'tests::it_sends_an_over_75_percent_warning_message'は、
'すでに借用されています: BorrowMutError', src/libcore/result.rs:906:4でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.
コードは、already borrowed: BorrowMutErrorというメッセージとともにパニックしたことに注目してください。
このようにしてRefCell<T>は実行時に借用規則の侵害を扱うのです。
コンパイル時ではなく実行時に借用エラーをキャッチするということは、開発過程の遅い段階でコードのミスを発見し、
コードをプロダクションにデプロイする時まで発見しない可能性もあることを意味します。また、
コンパイル時ではなく、実行時に借用を追いかける結果として、少し実行時にパフォーマンスを犠牲にするでしょう。
しかしながら、RefCell<T>を使うことで、不変値のみが許可される文脈で使用しつつ、
自身を変更して見かけたメッセージを追跡するモックオブジェクトを書くことが可能になります。
代償はありますが、RefCell<T>を使用すれば、普通の参照よりも多くの機能を得ることができるわけです。
Rc<T>とRefCell<T>を組み合わせることで可変なデータに複数の所有者を持たせる
RefCell<T>の一般的な使用法は、Rc<T>と組み合わせることにあります。Rc<T>は何らかのデータに複数の所有者を持たせてくれるけれども、
そのデータに不変のアクセスしかさせてくれないことを思い出してください。RefCell<T>を抱えるRc<T>があれば、
複数の所有者を持ちそして、可変化できる値を得ることができるのです。
例を挙げれば、Rc<T>を使用して複数のリストに別のリストの所有権を共有させたリスト15-18のコンスリストの例を思い出してください。
Rc<T>は不変値だけを抱えるので、一旦生成したら、リストの値はどれも変更できません。RefCell<T>を含めて、
リストの値を変更する能力を得ましょう。RefCell<T>をCons定義で使用することで、
リスト全てに格納されている値を変更できることをリスト15-24は示しています:
ファイル名: src/main.rs
#[derive(Debug)] enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, } use List::{Cons, Nil}; use std::rc::Rc; use std::cell::RefCell; 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(6)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a)); *value.borrow_mut() += 10; println!("a after = {:?}", a); println!("b after = {:?}", b); println!("c after = {:?}", c); }
リスト15-24: Rc<RefCell<i32>>で可変化できるListを生成する
Rc<RefCell<i32>>のインスタンスの値を生成し、valueという名前の変数に格納しているので、
直接後ほどアクセスすることができます。そして、aにvalueを持つCons列挙子でListを生成しています。
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という値になっていることがわかります。
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
このテクニックは非常に綺麗です!RefCell<T>を使用することで表面上は不変なList値を持てます。
しかし、内部可変性へのアクセスを提供するRefCell<T>のメソッドを使用できるので、必要な時にはデータを変更できます。
借用規則を実行時に精査することでデータ競合を防ぎ、時としてデータ構造でちょっとのスピードを犠牲にこの柔軟性を得るのは価値があります。
標準ライブラリには、Cell<T>などの内部可変性を提供する他の型もあり、この型は、内部値への参照を与える代わりに、
値はCell<T>の内部や外部へコピーされる点を除き似ています。またMutex<T>もあり、
これはスレッド間で使用するのが安全な内部可変性を提供します; 第16章でその使いみちについて議論しましょう。
これらの型の違いをより詳しく知るには、標準ライブラリのドキュメンテーションをチェックしてください。
循環参照は、メモリをリークすることもある
Rustのメモリ安全保証により誤って絶対に片付けられることのないメモリ(メモリリークとして知られています)を生成してしまいにくくなりますが、
不可能にはなりません。コンパイル時にデータ競合を防ぐのと同じようにメモリリークを完全に回避することは、
Rustの保証の一つではなく、メモリリークはRustにおいてはメモリ安全であることを意味します。
Rustでは、Rc<T>とRefCell<T>を使用してメモリリークを許可するとわかります:
要素がお互いに循環して参照する参照を生成することも可能ということです。循環の各要素の参照カウントが絶対に0にならないので、
これはメモリリークを起こし、値は絶対にドロップされません。
循環参照させる
リスト15-25のList enumの定義とtailメソッドから始めて、どう循環参照が起こる可能性があるのかとその回避策を見ましょう:
ファイル名: src/main.rs
fn main() {} use std::rc::Rc; use std::cell::RefCell; use List::{Cons, Nil}; #[derive(Debug)] enum List { Cons(i32, RefCell<Rc<List>>), Nil, } impl List { fn tail(&self) -> Option<&RefCell<Rc<List>>> { match *self { Cons(_, ref item) => Some(item), Nil => None, } } }
リスト15-25: Cons列挙子が参照しているものを変更できるようにRefCell<T>を抱えているコンスリストの定義
リスト15-5のList定義の別バリエーションを使用しています。Cons列挙子の2番目の要素はこれでRefCell<Rc<List>>になり、
リスト15-24のようにi32値を変更する能力があるのではなく、Cons列挙子が指しているList値の先を変えたいということです。
また、tailメソッドを追加してCons列挙子があるときに2番目の要素にアクセスするのが便利になるようにしています。
リスト15-26でリスト15-25の定義を使用するmain関数を追加しています。このコードは、aにリストを、
bにaのリストを指すリストを作成します。それからaのリストを変更してbを指し、循環参照させます。
その流れの中に過程のいろんな場所での参照カウントを示すprintln!文が存在しています。
ファイル名: src/main.rs
use List::{Cons, Nil}; use std::rc::Rc; use std::cell::RefCell; #[derive(Debug)] enum List { Cons(i32, RefCell<Rc<List>>), Nil, } impl List { fn tail(&self) -> Option<&RefCell<Rc<List>>> { match *self { Cons(_, ref item) => Some(item), Nil => None, } } } fn main() { let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil)))); // aの最初の参照カウント = {} println!("a initial rc count = {}", Rc::strong_count(&a)); // aの次の要素は = {:?} println!("a next item = {:?}", a.tail()); let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a)))); // b作成後のaの参照カウント = {} println!("a rc count after b creation = {}", Rc::strong_count(&a)); // bの最初の参照カウント = {} println!("b initial rc count = {}", Rc::strong_count(&b)); // bの次の要素 = {:?} println!("b next item = {:?}", b.tail()); if let Some(link) = a.tail() { *link.borrow_mut() = Rc::clone(&b); } // aを変更後のbの参照カウント = {} println!("b rc count after changing a = {}", Rc::strong_count(&b)); // aを変更後のaの参照カウント = {} 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()); // aの次の要素 = {:?} }
リスト15-26: 2つのList値がお互いを指して循環参照する
最初のリストが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!を今だけコメントアウトしたまま、このコードを実行すると、こんな出力が得られます:
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の終端で、コンパイラはまずbをドロップしようとし、aとbの各Rc<List>インスタンスのカウントを1減らします。
しかしながら、それでもaはbにあったRc<List>を参照しているので、そのRc<List>のカウントは0ではなく1になり、
そのRc<List>がヒープに確保していたメモリはドロップされません。メモリはただ、カウント1のままそこに永遠に居座るのです。
この循環参照を可視化するために、図15-4に図式を作成しました:
図15-4: お互いを指すリストaとbの循環参照
最後のprintln!のコメントを外してプログラムを実行したら、aがbを指して、bがaを指してと、
スタックがオーバーフローするまでコンパイラはこの循環を出力しようとするでしょう。
この場合、循環参照を作る直後にプログラムは終了します。この循環の結果は、それほど悲壮なものではありません。しかしながら、 より複雑なプログラムが多くのメモリを循環で確保し長い間その状態を保ったら、プログラムは必要以上のメモリを使用し、 使用可能なメモリを枯渇させてシステムを参らせてしまう可能性があります。
循環参照は簡単にできることではありませんが、不可能というわけでもありません。
Rc<T>値を含むRefCell<T>値があるなどの内部可変性と参照カウントのある型がネストして組み合わさっていたら、
循環していないことを保証しなければなりません; コンパイラがそれを捕捉することを信頼できないのです。
循環参照をするのは、自動テストやコードレビューなどの他のソフトウェア開発手段を使用して最小化すべきプログラム上のロジックバグでしょう。
循環参照を回避する別の解決策は、ある参照は所有権を表現して他の参照はしないというようにデータ構造を再構成することです。
結果として、所有権のある関係と所有権のない関係からなる循環ができ、所有権のある関係だけが、値がドロップされうるかどうかに影響します。
リスト15-25では、常にCons列挙子にリストを所有してほしいので、データ構造を再構成することはできません。
親ノードと子ノードからなるグラフを使った例に目を向けて、どんな時に所有権のない関係が循環参照を回避するのに適切な方法になるか確認しましょう。
循環参照を回避する: Rc<T>をWeak<T>に変換する
ここまで、Rc::cloneを呼び出すとRc<T>インスタンスのstrong_countが増えることと、
strong_countが0になった時にRc<T>インスタンスは片付けられることをデモしてきました。
Rc::downgradeを呼び出し、Rc<T>への参照を渡すことで、Rc<T>インスタンス内部の値への弱い参照(weak reference)を作ることもできます。
Rc::downgradeを呼び出すと、型Weak<T>のスマートポインタが得られます。
Rc<T>インスタンスのstrong_countを1増やす代わりに、Rc::downgradeを呼び出すと、weak_countが1増えます。
strong_count同様、Rc<T>型はweak_countを使用して、幾つのWeak<T>参照が存在しているかを追跡します。
違いは、Rc<T>が片付けられるのに、weak_countが0である必要はないということです。
強い参照は、Rc<T>インスタンスの所有権を共有する方法です。弱い参照は、所有権関係を表現しません。
ひとたび、関係する値の強い参照カウントが0になれば、弱い参照が関わる循環はなんでも破壊されるので、
循環参照にはなりません。
Weak<T>が参照する値はドロップされてしまっている可能性があるので、Weak<T>が指す値に何かをするには、
値がまだ存在することを確認しなければなりません。Weak<T>のupgradeメソッドを呼び出すことでこれをしてください。
このメソッドはOption<Rc<T>>を返します。Rc<T>値がまだドロップされていなければ、Someの結果が、
Rc<T>値がドロップ済みなら、Noneの結果が得られます。upgradeがOption<T>を返すので、
コンパイラは、SomeケースとNoneケースが扱われていることを確かめてくれ、無効なポインタは存在しません。
例として、要素が次の要素を知っているだけのリストを使うのではなく、要素が子要素と親要素を知っている木を作りましょう。
木データ構造を作る: 子ノードのあるNode
手始めに子ノードを知っているノードのある木を構成します。独自のi32値と子供のNode値への参照を抱えるNodeという構造体を作ります:
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::rc::Rc; use std::cell::RefCell; #[derive(Debug)] struct Node { value: i32, children: RefCell<Vec<Rc<Node>>>, } }
Nodeに子供を所有してほしく、木の各Nodeに直接アクセスできるよう、その所有権を変数と共有したいです。
こうするために、Vec<T>要素を型Rc<Node>の値になるよう定義しています。どのノードが他のノードの子供になるかも変更したいので、
Vec<Rc<Node>>の周りのchildrenをRefCell<T>にしています。
次にこの構造体定義を使って値3と子供なしのleafという1つのNodeインスタンスと、
値5とleafを子要素の一つとして持つbranchという別のインスタンスを作成します。
リスト15-27のようにですね:
ファイル名: src/main.rs
use std::rc::Rc; use std::cell::RefCell; #[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)]), }); }
リスト15-27: 子供なしのleafノードとleafを子要素に持つbranchノードを作る
leafのRc<Node>をクローンし、branchに格納しているので、leafのNodeはleafとbranchという2つの所有者を持つことになります。
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
#![allow(unused)] fn main() { use std::rc::{Rc, Weak}; use std::cell::RefCell; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } }
ノードは親ノードを参照できるものの、所有はしないでしょう。リスト15-28で、
leafノードが親のbranchを参照できるよう、この新しい定義を使用するようにmainを更新します:
ファイル名: src/main.rs
use std::rc::{Rc, Weak}; use std::cell::RefCell; #[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![]), }); // leafの親 = {:?} 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: 親ノードのbranchへの弱い参照があるleafノード
leafノードを作成することは、parentフィールドの例外を除いてリスト15-27でのleafノードの作成法の見た目に似ています:
leafは親なしで始まるので、新しく空のWeak<Node>参照インスタンスを作ります。
この時点でupgradeメソッドを使用してleafの親への参照を得ようとすると、None値になります。
このことは、最初のprintln!文の出力でわかります:
leaf parent = None
branchノードを作る際、branchには親ノードがないので、こちらもparentフィールドには新しいWeak<Node>参照が入ります。
それでも、leafはbranchの子供になっています。一旦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への変更を可視化する
新しい内部スコープを作り、branchの作成をそのスコープに移動することで、
Rc<Node>インスタンスのstrong_countとweak_count値がどう変化するかを眺めましょう。
そうすることで、branchが作成され、それからスコープを抜けてドロップされる時に起こることが確認できます。
変更は、リスト15-29に示してあります:
ファイル名: src/main.rs
use std::rc::{Rc, Weak}; use std::cell::RefCell; #[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_count = {}, weak_count = {} "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_count = {}, weak_count = {} "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), ); }
リスト15-29: 内側のスコープでbranchを作成し、強弱参照カウントを調査する
leaf作成後、そのRc<Node>の強カウントは1、弱カウントは0になります。内側のスコープでbranchを作成し、
leafに紐付け、この時点でカウントを出力すると、branchのRc<Node>の強カウントは1、
弱カウントも1になります(leaf.parentがWeak<Node>でbranchを指しているため)。
leafのカウントを出力すると、強カウントが2になっていることがわかります。branchが今は、
branch.childrenに格納されたleafのRc<Node>のクローンを持っているからですが、
それでも弱カウントは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”をチェックしてください。
訳注: 日本語版のThe Rustonomiconはこちらです。
次は、Rustでの並行性について語ります。もういくつか新しいスマートポインタについてさえも学ぶでしょう。
恐れるな!並行性
並行性を安全かつ効率的に扱うことは、Rustの別の主な目標です。並行プログラミングは、プログラムの異なる部分が独立して実行することであり、 並列プログラミングはプログラムの異なる部分が同時に実行することですが、多くのコンピュータが複数のプロセッサの利点を生かすようになるにつれ、 重要度を増しています。歴史的に、これらの文脈で行うプログラミングは困難で、エラーが起きやすいものでした: Rustはこれを変えると願っています。
当初、Rustチームは、メモリ安全性を保証することと、並行性問題を回避することは、 異なる方法で解決すべき別々の課題だと考えていました。時間とともに、チームは、所有権と型システムは、 メモリ安全性と並行性問題を管理する役に立つ一連の強力な道具であることを発見しました。 所有権と型チェックを活用することで、多くの並行性エラーは、実行時エラーではなくコンパイル時エラーになります。 故に、実行時に並行性のバグが起きた状況と全く同じ状況を再現しようと時間を浪費させるよりも、 不正なコードはコンパイルを拒み、問題を説明するエラーを提示するでしょう。結果として、 プロダクトになった後でなく、作業中にコードを修正できます。 Rustのこの方向性を恐れるな!並行性とニックネーム付けしました。これにより、潜在的なバグがなく、かつ、 新しいバグを導入することなく簡単にリファクタリングできるコードを書くことができます。
注釈: 簡潔性のため、並行または並列と述べることで正確を期するのではなく、 多くの問題を並行と割り切ってしまいます。この本がもし並行性あるいは並列性に関した本ならば、 詳述していたでしょう。この章に対しては、並行を使ったら、 脳内で並行または並列と置き換えてください。
多くの言語は、自分が提供する並行性問題を扱う解決策について独断的です。例えば、Erlangには、 メッセージ受け渡しの並行性に関する素晴らしい機能がありますが、スレッド間で状態を共有することに関しては、 曖昧な方法しかありません。可能な解決策の一部のみをサポートすることは、高級言語にとっては合理的な施策です。 なぜなら、高級言語は一部の制御を失う代わりに抽象化することから恩恵を受けるからです。ところが、 低級言語は、どんな場面でも最高のパフォーマンスで解決策を提供すると想定され、ハードウェアに関してほとんど抽象化はしません。 そのため、Rustは、自分の状況と必要性に適した方法が何であれ、問題をモデル化するためのいろんな道具を備えています。
こちらが、この章で講義する話題です:
- スレッドを生成して、複数のコードを同時に走らせる方法
- チャンネルがスレッド間でメッセージを送るメッセージ受け渡し並行性
- 複数のスレッドが何らかのデータにアクセスする状態共有並行性
- 標準ライブラリが提供する型だけでなく、ユーザが定義した型に対してもRustの並行性の安全保証を拡張する
SyncとSendトレイト
スレッドを使用してコードを同時に走らせる
多くの現代のOSでは、実行中のプログラムのコードはプロセスで走り、OSは同時に複数のプロセスを管理します。 自分のプログラム内で、独立した部分を同時に実行できます。これらの独立した部分を走らせる機能をスレッドと呼びます。
プログラム内の計算を複数のスレッドに分けると、パフォーマンスが改善します。プログラムが同時に複数の作業をするからですが、 複雑度も増します。スレッドは同時に走らせることができるので、異なるスレッドのコードが走る順番に関して、 本来的に保証はありません。これは例えば以下のような問題を招きます:
- スレッドがデータやリソースに矛盾した順番でアクセスする競合状態
- 2つのスレッドがお互いにもう一方が持っているリソースを使用し終わるのを待ち、両者が継続するのを防ぐデッドロック
- 特定の状況でのみ起き、確実な再現や修正が困難なバグ
Rustは、スレッドを使用する際の悪影響を軽減しようとしていますが、それでも、マルチスレッドの文脈でのプログラミングでは、 注意深い思考とシングルスレッドで走るプログラムとは異なるコード構造が必要です。
プログラミング言語によってスレッドはいくつかの方法で実装されています。多くのOSで、新規スレッドを生成するAPIが提供されています。 言語がOSのAPIを呼び出してスレッドを生成するこのモデルを時に1:1と呼び、1つのOSスレッドに対して1つの言語スレッドを意味します。
多くのプログラミング言語がスレッドの独自の特別な実装を提供しています。プログラミング言語が提供するスレッドは、
グリーンスレッドとして知られ、このグリーンスレッドを使用する言語は、それを異なる数のOSスレッドの文脈で実行します。
このため、グリーンスレッドのモデルはM:Nモデルと呼ばれます: M個のグリーンスレッドに対して、
N個のOSスレッドがあり、MとNは必ずしも同じ数字ではありません。
各モデルには、それだけの利点と代償があり、Rustにとって最も重要な代償は、ランタイムのサポートです。 ランタイムは、混乱しやすい用語で文脈によって意味も変わります。
この文脈でのランタイムとは、言語によって全てのバイナリに含まれるコードのことを意味します。 言語によってこのコードの大小は決まりますが、非アセンブリ言語は全てある量の実行時コードを含みます。 そのため、口語的に誰かが「ノーランタイム」と言ったら、「小さいランタイム」のことを意味することがしばしばあります。 ランタイムが小さいと機能も少ないですが、バイナリのサイズも小さくなるという利点があり、 その言語を他の言語とより多くの文脈で組み合わせることが容易になります。多くの言語では、 より多くの機能と引き換えにランタイムのサイズが膨れ上がるのは、受け入れられることですが、 Rustにはほとんどゼロのランタイムが必要でパフォーマンスを維持するためにCコードを呼び出せることを妥協できないのです。
M:Nのグリーンスレッドモデルは、スレッドを管理するのにより大きな言語ランタイムが必要です。よって、 Rustの標準ライブラリは、1:1スレッドの実装のみを提供しています。Rustはそのような低級言語なので、 例えば、むしろどのスレッドがいつ走るかのより詳細な制御や、より低コストの文脈切り替えなどの一面をオーバーヘッドと引き換えるなら、 M:Nスレッドの実装をしたクレートもあります。
今やRustにおけるスレッドを定義したので、標準ライブラリで提供されているスレッド関連のAPIの使用法を探究しましょう。
spawnで新規スレッドを生成する
新規スレッドを生成するには、thread::spawn関数を呼び出し、
新規スレッドで走らせたいコードを含むクロージャ(クロージャについては第13章で語りました)を渡します。
リスト16-1の例は、メインスレッドと新規スレッドからテキストを出力します:
ファイル名: src/main.rs
use std::thread; use std::time::Duration; fn main() { thread::spawn(|| { for i in 1..10 { // やあ!立ち上げたスレッドから数字{}だよ! println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { // メインスレッドから数字{}だよ! println!("hi number {} from the main thread!", i); thread::sleep(Duration::from_millis(1)); } }
リスト16-1: メインスレッドが別のものを出力する間に新規スレッドを生成して何かを出力する
この関数では、新しいスレッドは、実行が終わったかどうかにかかわらず、メインスレッドが終了したら停止することに注意してください。 このプログラムからの出力は毎回少々異なる可能性がありますが、だいたい以下のような感じでしょう:
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を呼び出すと、少々の間、スレッドの実行を止め、違うスレッドを走らせることができます。
スレッドはおそらく切り替わるでしょうが、保証はありません: OSがスレッドのスケジュールを行う方法によります。
この実行では、コード上では立ち上げられたスレッドのprint文が先に現れているのに、メインスレッドが先に出力しています。また、
立ち上げたスレッドにはiが9になるまで出力するよう指示しているのに、メインスレッドが終了する前の5までしか到達していません。
このコードを実行してメインスレッドの出力しか目の当たりにできなかったり、オーバーラップがなければ、 範囲の値を増やしてOSがスレッド切り替えを行う機会を増やしてみてください。
joinハンドルで全スレッドの終了を待つ
リスト16-1のコードは、メインスレッドが終了するためにほとんどの場合、立ち上げたスレッドがすべて実行されないだけでなく、 立ち上げたスレッドが実行されるかどうかも保証できません。原因は、スレッドの実行順に保証がないからです。
thread::spawnの戻り値を変数に保存することで、立ち上げたスレッドが実行されなかったり、
完全には実行されなかったりする問題を修正することができます。thread::spawnの戻り値の型はJoinHandleです。
JoinHandleは、そのjoinメソッドを呼び出したときにスレッドの終了を待つ所有された値です。
リスト16-2は、リスト16-1で生成したスレッドのJoinHandleを使用し、joinを呼び出して、
mainが終了する前に、立ち上げたスレッドが確実に完了する方法を示しています:
ファイル名: src/main.rs
use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {} from the main thread!", i); thread::sleep(Duration::from_millis(1)); } handle.join().unwrap(); }
リスト16-2: thread::spawnのJoinHandleを保存してスレッドが完了するのを保証する
ハンドルに対して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()呼び出しのためにメインスレッドは待機し、
立ち上げたスレッドが終了するまで終わりません。
ですが、代わりにhandle.join()をforループの前に移動したらどうなるのか確認しましょう。こんな感じに:
ファイル名: src/main.rs
use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } }); handle.join().unwrap(); for i in 1..5 { println!("hi number {} from the main thread!", i); thread::sleep(Duration::from_millis(1)); } }
メインスレッドは、立ち上げたスレッドが終了するまで待ち、それからforループを実行するので、
以下のように出力はもう混ざらないでしょう:
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クロージャを使用する
moveクロージャは、thread::spawnとともによく使用されます。
あるスレッドのデータを別のスレッドで使用できるようになるからです。
第13章で、クロージャの引数リストの前にmoveキーワードを使用して、
クロージャに環境で使用している値の所有権を強制的に奪わせることができると述べました。
このテクニックは、あるスレッドから別のスレッドに値の所有権を移すために新しいスレッドを生成する際に特に有用です。
リスト16-1において、thread::spawnに渡したクロージャには引数がなかったことに注目してください:
立ち上げたスレッドのコードでメインスレッドからのデータは何も使用していないのです。
立ち上げたスレッドでメインスレッドのデータを使用するには、立ち上げるスレッドのクロージャは、
必要な値をキャプチャしなければなりません。リスト16-3は、メインスレッドでベクタを生成し、
立ち上げたスレッドで使用する試みを示しています。しかしながら、すぐにわかるように、これはまだ動きません:
ファイル名: src/main.rs
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
// こちらがベクタ: {:?}
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();
}
リスト16-3: 別のスレッドでメインスレッドが生成したベクタを使用しようとする
クロージャはvを使用しているので、vをキャプチャし、クロージャの環境の一部にしています。
thread::spawnはこのクロージャを新しいスレッドで走らせるので、
その新しいスレッド内でvにアクセスできるはずです。しかし、このコードをコンパイルすると、
以下のようなエラーが出ます:
error[E0373]: closure may outlive the current function, but it borrows `v`,
which is owned by the current function
(エラー: クロージャは現在の関数よりも長生きするかもしれませんが、現在の関数が所有している
`v`を借用しています)
--> 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
|
help: to force the closure to take ownership of `v` (and any other referenced
variables), use the `move` keyword
(助言: `v`(や他の参照されている変数)の所有権をクロージャに奪わせるには、`move`キーワードを使用してください)
|
6 | let handle = thread::spawn(move || {
| ^^^^^^^
Rustはvのキャプチャ方法を推論し、println!はvへの参照のみを必要とするので、クロージャは、
vを借用しようとします。ですが、問題があります: コンパイラには、立ち上げたスレッドがどのくらいの期間走るのかわからないので、
vへの参照が常に有効であるか把握できないのです。
リスト16-4は、vへの参照がより有効でなさそうな筋書きです:
ファイル名: src/main.rs
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();
}
リスト16-4: vをドロップするメインスレッドからvへの参照をキャプチャしようとするクロージャを伴うスレッド
このコードを実行できてしまうなら、立ち上げたスレッドはまったく実行されることなく即座にバックグラウンドに置かれる可能性があります。
立ち上げたスレッドは内部に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キーワードを付することで、コンパイラに値を借用すべきと推論させるのではなく、
クロージャに使用している値の所有権を強制的に奪わせます。リスト16-5に示したリスト16-3に対する変更は、
コンパイルでき、意図通りに動きます:
ファイル名: src/main.rs
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(); }
リスト16-5: moveキーワードを使用してクロージャに使用している値の所有権を強制的に奪わせる
moveクロージャを使用していたら、メインスレッドがdropを呼び出すリスト16-4のコードはどうなるのでしょうか?
moveで解決するのでしょうか?残念ながら、違います; リスト16-4が試みていることは別の理由によりできないので、
違うエラーが出ます。クロージャにmoveを付与したら、vをクロージャの環境にムーブするので、
最早メインスレッドでdropを呼び出すことは叶わなくなるでしょう。代わりにこのようなコンパイルエラーが出るでしょう:
error[E0382]: use of moved value: `v`
(エラー: ムーブされた値の使用: `v`)
--> src/main.rs:10:10
|
6 | let handle = thread::spawn(move || {
| ------- value moved (into closure) here
...
10 | drop(v); // oh no!
| ^ value used here after move
|
= note: move occurs because `v` has type `std::vec::Vec<i32>`, which does
not implement the `Copy` trait
(注釈: `v`の型が`std::vec::Vec<i32>`のためムーブが起きました。この型は、`Copy`トレイトを実装していません)
再三Rustの所有権規則が救ってくれました!リスト16-3のコードはエラーになりました。
コンパイラが一時的に保守的になり、スレッドに対してvを借用しただけだったからで、
これは、メインスレッドは理論上、立ち上げたスレッドの参照を不正化する可能性があることを意味します。
vの所有権を立ち上げたスレッドに移動するとコンパイラに指示することで、
メインスレッドはもうvを使用しないとコンパイラに保証しているのです。リスト16-4も同様に変更したら、
メインスレッドでvを使用しようとする際に所有権の規則に違反することになります。
moveキーワードにより、Rustの保守的な借用のデフォルトが上書きされるのです;
所有権の規則を侵害させてくれないのです。
スレッドとスレッドAPIの基礎知識を得たので、スレッドでできることを見ていきましょう。
メッセージ受け渡しを使ってスレッド間でデータを転送する
人気度を増してきている安全な並行性を保証する一つのアプローチがメッセージ受け渡しで、 スレッドやアクターがデータを含むメッセージを相互に送り合うことでやり取りします。 こちらが、Go言語のドキュメンテーションのスローガンにある考えです: 「メモリを共有することでやり取りするな; 代わりにやり取りすることでメモリを共有しろ」
メッセージ送信並行性を達成するためにRustに存在する一つの主な道具は、チャンネルで、 Rustの標準ライブラリが実装を提供しているプログラミング概念です。プログラミングのチャンネルは、 水の流れのように考えることができます。小川とか川ですね。アヒルのおもちゃやボートみたいなものを流れに置いたら、 水路の終端まで下流に流れていきます。
プログラミングにおけるチャンネルは、2分割できます: 転送機と受信機です。転送機はアヒルのおもちゃを川に置く上流になり、 受信機は、アヒルのおもちゃが行き着く下流になります。コードのある箇所が送信したいデータとともに転送機のメソッドを呼び出し、 別の部分がメッセージが到着していないか受信側を調べます。転送機と受信機のどちらかがドロップされると、 チャンネルは閉じられたと言います。
ここで、1つのスレッドが値を生成し、それをチャンネルに送信し、別のスレッドがその値を受け取り、 出力するプログラムに取り掛かります。チャンネルを使用してスレッド間に単純な値を送り、 機能の説明を行います。一旦、そのテクニックに慣れてしまえば、チャンネルを使用してチャットシステムや、 多くのスレッドが計算の一部を担い、結果をまとめる1つのスレッドにその部分を送るようなシステムを実装できるでしょう。
まず、リスト16-6において、チャンネルを生成するものの、何もしません。 チャンネル越しにどんな型の値を送りたいのかコンパイラがわからないため、 これはまだコンパイルできないことに注意してください。
ファイル名: src/main.rs
use std::sync::mpsc; fn main() { let (tx, rx) = mpsc::channel(); tx.send(()).unwrap(); }
リスト16-6: チャンネルを生成し、2つの部品をtxとrxに代入する
mpsc::channel関数で新しいチャンネルを生成しています; mpscはmultiple producer, single consumerを表しています。
簡潔に言えば、Rustの標準ライブラリがチャンネルを実装している方法は、1つのチャンネルが値を生成する複数の送信側と、
その値を消費するたった1つの受信側を持つことができるということを意味します。
複数の小川が互いに合わさって1つの大きな川になるところを想像してください:
どの小川を通っても、送られたものは最終的に1つの川に行き着きます。今は、1つの生成器から始めますが、
この例が動作するようになったら、複数の生成器を追加します。
mpsc::channel関数はタプルを返し、1つ目の要素は、送信側、2つ目の要素は受信側になります。
txとrxという略称は、多くの分野で伝統的に転送機と受信機にそれぞれ使用されているので、
変数をそのように名付けて、各終端を示します。タプルを分配するパターンを伴うlet文を使用しています;
let文でパターンを使用することと分配については、第18章で議論しましょう。このようにlet文を使うと、
mpsc::channelで返ってくるタプルの部品を抽出するのが便利になります。
立ち上げたスレッドがメインスレッドとやり取りするように、転送機を立ち上げたスレッドに移動し、 1文字列を送らせましょう。リスト16-7のようにですね。川の上流にアヒルのおもちゃを置いたり、 チャットのメッセージをあるスレッドから別のスレッドに送るみたいですね。
ファイル名: src/main.rs
use std::thread; use std::sync::mpsc; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); }); }
リスト16-7: txを立ち上げたスレッドに移動し、「やあ」を送る
今回も、thread::spawnを使用して新しいスレッドを生成し、それからmoveを使用して、
立ち上げたスレッドがtxを所有するようにクロージャにtxをムーブしています。立ち上げたスレッドは、
メッセージをチャンネルを通して送信できるように、チャンネルの送信側を所有する必要があります。
転送側には、送信したい値を取るsendメソッドがあります。sendメソッドはResult<T, E>型を返すので、
既に受信側がドロップされ、値を送信する場所がなければ、送信処理はエラーを返します。
この例では、エラーの場合には、パニックするようにunwrapを呼び出しています。ですが、実際のアプリケーションでは、
ちゃんと扱うでしょう: 第9章に戻ってちゃんとしたエラー処理の方法を再確認してください。
リスト16-8において、メインスレッドのチャンネルの受信側から値を得ます。 アヒルのおもちゃを川の終端で水から回収したり、チャットメッセージを取得するみたいですね。
ファイル名: src/main.rs
use std::thread; use std::sync::mpsc; 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); }
リスト16-8: 「やあ」の値をメインスレッドで受け取り、出力する
チャンネルの受信側には有用なメソッドが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のコードのコンパイルを試みて、このコードが許容されない理由を確認してください:
ファイル名: src/main.rs
use std::thread;
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
// valは{}
println!("val is {}", val);
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
リスト16-9: チャンネルに送信後にvalの使用を試みる
ここで、tx.send経由でチャンネルに送信後にvalを出力しようとしています。これを許可するのは、悪い考えです:
一旦、値が他のスレッドに送信されたら、再度値を使用しようとする前にそのスレッドが変更したりドロップできてしまいます。
可能性として、その別のスレッドの変更により、矛盾していたり存在しないデータのせいでエラーが発生したり、
予期しない結果になるでしょう。ですが、リスト16-9のコードのコンパイルを試みると、Rustはエラーを返します:
error[E0382]: use of moved value: `val`
--> src/main.rs:10:31
|
9 | tx.send(val).unwrap();
| --- value moved here
10 | println!("val is {}", val);
| ^^^ value used here after move
|
= note: move occurs because `val` has type `std::string::String`, which does
not implement the `Copy` trait
並行性のミスがコンパイルエラーを招きました。send関数は引数の所有権を奪い、
値がムーブされると、受信側が所有権を得るのです。これにより、送信後に誤って再度値を使用するのを防いでくれます;
所有権システムが、万事問題ないことを確認してくれます。
複数の値を送信し、受信側が待機するのを確かめる
リスト16-8のコードはコンパイルでき、動きましたが、2つの個別のスレッドがお互いにチャンネル越しに会話していることは、 明瞭に示されませんでした。リスト16-10において、リスト16-8のコードが並行に動いていることを証明する変更を行いました: 立ち上げたスレッドは、複数のメッセージを送信し、各メッセージ間で、1秒待機します。
ファイル名: src/main.rs
use std::thread; use std::sync::mpsc; use std::time::Duration; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { // スレッドからやあ(hi from the thread) 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); } }
リスト16-10: 複数のメッセージを送信し、メッセージ間で停止する
今回は、メインスレッドに送信したい文字列のベクタを立ち上げたスレッドが持っています。
それらを繰り返し、各々個別に送信し、Durationの値1秒とともにthread::sleep関数を呼び出すことで、
メッセージ間で停止します。
メインスレッドにおいて、最早recv関数を明示的に呼んではいません: 代わりに、
rxをイテレータとして扱っています。受信した値それぞれを出力します。
チャンネルが閉じられると、繰り返しも終わります。
リスト16-10のコードを走らせると、各行の間に1秒の待機をしつつ、以下のような出力を目の当たりにするはずです:
Got: hi
Got: from
Got: the
Got: thread
メインスレッドのforループには停止したり、遅れせたりするコードは何もないので、
メインスレッドが立ち上げたスレッドから値を受け取るのを待機していることがわかります。
転送機をクローンして複数の生成器を作成する
mpscは、mutiple producer, single consumerの頭字語であると前述しました。
mpscを使い、リスト16-10のコードを拡張して、全ての値を同じ受信機に送信する複数のスレッドを生成しましょう。
チャンネルの転送の片割れをクローンすることでそうすることができます。リスト16-11のようにですね:
ファイル名: src/main.rs
use std::thread; use std::sync::mpsc; use std::time::Duration; fn main() { // --snip-- let (tx, rx) = mpsc::channel(); let tx1 = mpsc::Sender::clone(&tx); 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 || { // 君のためにもっとメッセージを(more messages for you) 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-- }
リスト16-11: 複数の生成器から複数のメッセージを送信する
今回、最初のスレッドを立ち上げる前に、チャンネルの送信側に対してcloneを呼び出しています。
これにより、最初に立ち上げたスレッドに渡せる新しい送信ハンドルが得られます。
元のチャンネルの送信側は、2番目に立ち上げたスレッドに渡します。これにより2つスレッドが得られ、
それぞれチャンネルの受信側に異なるメッセージを送信します。
コードを実行すると、出力は以下のようなものになるはずです:
Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you
別の順番で値が出る可能性もあります; システム次第です。並行性が面白いと同時に難しい部分でもあります。
異なるスレッドで色々な値を与えてthread::sleepで実験をしたら、走らせるたびにより非決定的になり、
毎回異なる出力をするでしょう。
チャンネルの動作方法を見たので、他の並行性に目を向けましょう。
状態共有並行性
メッセージ受け渡しは、並行性を扱う素晴らしい方法ですが、唯一の方法ではありません。 Go言語ドキュメンテーションのスローガンのこの部分を再び考えてください: 「メモリを共有することでやり取りする。」
メモリを共有することでやり取りするとはどんな感じなのでしょうか?さらに、 なぜメッセージ受け渡しに熱狂的な人は、それを使わず、代わりに全く反対のことをするのでしょうか?
ある意味では、どんなプログラミング言語のチャンネルも単独の所有権に類似しています。 一旦チャンネルに値を転送したら、その値は最早使用することがないからです。 メモリ共有並行性は、複数の所有権に似ています: 複数のスレッドが同時に同じメモリ位置にアクセスできるのです。 第15章でスマートポインタが複数の所有権を可能にするのを目の当たりにしたように、 異なる所有者を管理する必要があるので、複数の所有権は複雑度を増させます。 Rustの型システムと所有権規則は、この管理を正しく行う大きな助けになります。 例として、メモリ共有を行うより一般的な並行性の基本型の一つであるミューテックスを見てみましょう。
ミューテックスを使用して一度に1つのスレッドからデータにアクセスすることを許可する
ミューテックスは、どんな時も1つのスレッドにしかなんらかのデータへのアクセスを許可しないというように、 "mutual exclusion"(相互排他)の省略形です。ミューテックスにあるデータにアクセスするには、 ミューテックスのロックを所望することでアクセスしたいことをまず、スレッドは通知しなければなりません。 ロックとは、現在誰がデータへの排他的アクセスを行なっているかを追跡するミューテックスの一部をなすデータ構造です。 故に、ミューテックスはロックシステム経由で保持しているデータを死守する(guarding)と解説されます。
ミューテックスは、2つの規則を覚えておく必要があるため、難しいという評判があります:
- データを使用する前にロックの獲得を試みなければならない。
- ミューテックスが死守しているデータの使用が終わったら、他のスレッドがロックを獲得できるように、 データをアンロックしなければならない。
ミューテックスを現実世界の物で例えるなら、マイクが1つしかない会議のパネルディスカッションを思い浮かべてください。 パネリストが発言できる前に、マイクを使用したいと申し出たり、通知しなければなりません。マイクを受け取ったら、 話したいだけ話し、それから次に発言を申し出たパネリストにマイクを手渡します。パネリストが発言し終わった時に、 マイクを手渡すのを忘れていたら、誰も他の人は発言できません。共有されているマイクの管理がうまくいかなければ、 パネルは予定通りに機能しないでしょう!
ミューテックスの管理は、正しく行うのに著しく技巧を要することがあるので、多くの人がチャンネルに熱狂的になるわけです。 しかしながら、Rustの型システムと所有権規則のおかげで、ロックとアンロックをおかしくすることはありません。
Mutex<T>のAPI
ミューテックスの使用方法の例として、ミューテックスをシングルスレッドの文脈で使うことから始めましょう。 リスト16-12のようにですね:
ファイル名: src/main.rs
use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut num = m.lock().unwrap(); *num = 6; } println!("m = {:?}", m); }
リスト16-12: 簡潔性のためにMutex<T>のAPIをシングルスレッドの文脈で探究する
多くの型同様、newという関連関数を使用してMutex<T>を生成します。ミューテックス内部のデータにアクセスするには、
lockメソッドを使用してロックを獲得します。この呼び出しは、現在のスレッドをブロックするので、
ロックを得られる順番が来るまで何も作業はできません。
ロックを保持している他のスレッドがパニックしたら、lockの呼び出しは失敗するでしょう。その場合、
誰もロックを取得することは叶わないので、unwrapすると決定し、そのような状況になったら、
このスレッドをパニックさせます。
ロックを獲得した後、今回の場合、numと名付けられていますが、戻り値を中に入っているデータへの可変参照として扱うことができます。
型システムにより、mの値を使用する前にロックを獲得していることが確認されます: Mutex<i32>はi32ではないので、
i32を使用できるようにするには、ロックを獲得しなければならないのです。忘れることはあり得ません;
型システムにより、それ以外の場合に内部のi32にアクセスすることは許されません。
お察しかもしれませんが、Mutex<T>はスマートポインタです。より正確を期すなら、
lockの呼び出しがMutexGuardというスマートポインタを返却します。このスマートポインタが、
内部のデータを指すDerefを実装しています; このスマートポインタはさらにMutexGuardがスコープを外れた時に、
自動的にロックを解除するDrop実装もしていて、これがリスト16-12の内部スコープの終わりで発生します。
結果として、ロックの解除が自動的に行われるので、ロックの解除を忘れ、
ミューテックスが他のスレッドで使用されるのを阻害するリスクを負いません。
ロックをドロップした後、ミューテックスの値を出力し、内部のi32の値を6に変更できたことが確かめられるのです。
複数のスレッド間でMutex<T>を共有する
さて、Mutex<T>を使って複数のスレッド間で値を共有してみましょう。10個のスレッドを立ち上げ、
各々カウンタの値を1ずつインクリメントさせるので、カウンタは0から10まで上がります。
以下の数例は、コンパイルエラーになることに注意し、そのエラーを使用してMutex<T>の使用法と、
コンパイラがそれを正しく活用する手助けをしてくれる方法について学びます。リスト16-13が最初の例です:
ファイル名: src/main.rs
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());
}
リスト16-13: Mutex<T>により死守されているカウンタを10個のスレッドがそれぞれインクリメントする
リスト16-12のように、counter変数を生成してMutex<T>の内部にi32を保持しています。
次に、数値の範囲をマッピングして10個のスレッドを生成しています。thread::spawnを使用して、
全スレッドに同じクロージャを与えています。このクロージャは、スレッド内にカウンタをムーブし、
lockメソッドを呼ぶことでMutex<T>のロックを獲得し、それからミューテックスの値に1を足します。
スレッドがクロージャを実行し終わったら、numはスコープ外に出てロックを解除するので、
他のスレッドが獲得できるわけです。
メインスレッドで全てのjoinハンドルを収集します。それからリスト16-2のように、各々に対してjoinを呼び出し、
全スレッドが終了するのを確かめています。その時点で、メインスレッドはロックを獲得し、このプログラムの結果を出力します。
この例はコンパイルできないでしょうと仄めかしました。では、理由を探りましょう!
error[E0382]: capture of moved value: `counter`
(エラー: ムーブされた値をキャプチャしています: `counter`)
--> src/main.rs:10:27
|
9 | let handle = thread::spawn(move || {
| ------- value moved (into closure) here
10 | let mut num = counter.lock().unwrap();
| ^^^^^^^ value captured here after move
|
= note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
which does not implement the `Copy` trait
error[E0382]: use of moved value: `counter`
--> src/main.rs:21:29
|
9 | let handle = thread::spawn(move || {
| ------- value moved (into closure) here
...
21 | println!("Result: {}", *counter.lock().unwrap());
| ^^^^^^^ value used here after move
|
= note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
which does not implement the `Copy` trait
error: aborting due to 2 previous errors
(エラー: 前述の2つのエラーによりアボート)
エラーメッセージは、counter値はクロージャにムーブされ、それからlockを呼び出したときにキャプチャされていると述べています。
その説明は、所望した動作のように聞こえますが、許可されていないのです!
プログラムを単純化してこれを理解しましょう。forループで10個スレッドを生成する代わりに、
ループなしで2つのスレッドを作るだけにしてどうなるか確認しましょう。
リスト16-13の最初のforループを代わりにこのコードと置き換えてください:
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
let handle2 = thread::spawn(move || {
let mut num2 = counter.lock().unwrap();
*num2 += 1;
});
handles.push(handle2);
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
2つのスレッドを生成し、2番目のスレッドの変数名をhandle2とnum2に変更しています。
今回このコードを走らせると、コンパイラは以下の出力をします:
error[E0382]: capture of moved value: `counter`
--> src/main.rs:16:24
|
8 | let handle = thread::spawn(move || {
| ------- value moved (into closure) here
...
16 | let mut num2 = counter.lock().unwrap();
| ^^^^^^^ value captured here after move
|
= note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
which does not implement the `Copy` trait
error[E0382]: use of moved value: `counter`
--> src/main.rs:26:29
|
8 | let handle = thread::spawn(move || {
| ------- value moved (into closure) here
...
26 | println!("Result: {}", *counter.lock().unwrap());
| ^^^^^^^ value used here after move
|
= note: move occurs because `counter` has type `std::sync::Mutex<i32>`,
which does not implement the `Copy` trait
error: aborting due to 2 previous errors
なるほど!最初のエラーメッセージは、handleに紐づけられたスレッドのクロージャにcounterがムーブされていることを示唆しています。
そのムーブにより、それに対してlockを呼び出し、結果を2番目のスレッドのnum2に保持しようとした時に、
counterをキャプチャすることを妨げています!ゆえに、コンパイラは、counterの所有権を複数のスレッドに移すことはできないと教えてくれています。
これは、以前では確認しづらかったことです。なぜなら、スレッドはループの中にあり、
ループの違う繰り返しにある違うスレッドをコンパイラは指し示せないからです。
第15章で議論した複数所有権メソッドによりコンパイルエラーを修正しましょう。
複数のスレッドで複数の所有権
第15章で、スマートポインタのRc<T>を使用して参照カウントの値を作ることで、1つの値に複数の所有者を与えました。
同じことをここでもして、どうなるか見ましょう。リスト16-14でRc<T>にMutex<T>を包含し、
所有権をスレッドに移す前にRc<T>をクローンします。今やエラーを確認したので、
forループの使用に立ち戻り、クロージャにmoveキーワードを使用し続けます。
ファイル名: src/main.rs
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());
}
リスト16-14: Rc<T>を使用して複数のスレッドにMutex<T>を所有させようとする
再三、コンパイルし……別のエラーが出ました!コンパイラはいろんなことを教えてくれています。
error[E0277]: the trait bound `std::rc::Rc<std::sync::Mutex<i32>>:
std::marker::Send` is not satisfied in `[closure@src/main.rs:11:36:
15:10 counter:std::rc::Rc<std::sync::Mutex<i32>>]`
(エラー: トレイト境界`std::rc::Rc<std::sync::Mutex<i32>>:
std::marker::Send`は`[closure@src/main.rs:11:36:15:10
counter:std::rc::Rc<std::sync::Mutex<i32>>]`で満たされていません)
--> src/main.rs:11:22
|
11 | let handle = thread::spawn(move || {
| ^^^^^^^^^^^^^ `std::rc::Rc<std::sync::Mutex<i32>>`
cannot be sent between threads safely
(`std::rc::Rc<std::sync::Mutex<i32>>`は、スレッド間で安全に送信できません)
|
= help: within `[closure@src/main.rs:11:36: 15:10
counter:std::rc::Rc<std::sync::Mutex<i32>>]`, the trait `std::marker::Send` is
not implemented for `std::rc::Rc<std::sync::Mutex<i32>>`
(ヘルプ: `[closure@src/main.rs:11:36 15:10
counter:std::rc::Rc<std::sync::Mutex<i32>>]`内でトレイト`std::marker::Send`は、
`std::rc::Rc<std::sync::Mutex<i32>>`に対して実装されていません)
= note: required because it appears within the type
`[closure@src/main.rs:11:36: 15:10 counter:std::rc::Rc<std::sync::Mutex<i32>>]`
(注釈: 型`[closure@src/main.rs:11:36 15:10
counter:std::rc::Rc<std::sync::Mutex<i32>>]`内に出現するので必要です)
= note: required by `std::thread::spawn`
(注釈: `std::thread::spawn`により必要とされています)
おお、このエラーメッセージはとても長ったらしいですね!こちらが、注目すべき重要な部分です:
最初のインラインエラーは `std::rc::Rc<std::sync::Mutex<i32>>` cannot be sent between threads safelyと述べています。この理由は、エラーメッセージの次に注目すべき重要な部分にあります。
洗練されたエラーメッセージは、 the trait bound `Send` is not satisfiedと述べています。
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は、ようやくコンパイルでき、動作します:
ファイル名: src/main.rs
use std::sync::{Mutex, Arc}; 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()); }
リスト16-15: Arc<T>を使用してMutex<T>をラップし、所有権を複数のスレッド間で共有できるようにする
このコードは、以下のように出力します:
Result: 10
やりました!0から10まで数え上げました。これは、あまり印象的ではないように思えるかもしれませんが、
本当にMutex<T>とスレッド安全性についていろんなことを教えてくれました。このプログラムの構造を使用して、
カウンタをインクリメントする以上の複雑な処理を行うこともできるでしょう。この手法を使えば、
計算を独立した部分に小分けにし、その部分をスレッドに分割し、それからMutex<T>を使用して、
各スレッドに最終結果を更新させることができます。
RefCell<T>/Rc<T>とMutex<T>/Arc<T>の類似性
counterは不変なのに、その内部にある値への可変参照を得ることができたことに気付いたでしょうか;
つまり、Mutex<T>は、Cell系のように内部可変性を提供するわけです。
第15章でRefCell<T>を使用してRc<T>の内容を可変化できるようにしたのと同様に、
Mutex<T>を使用してArc<T>の内容を可変化しているのです。
気付いておくべき別の詳細は、Mutex<T>を使用する際にあらゆる種類のロジックエラーからは、
コンパイラは保護してくれないということです。第15章でRc<T>は、循環参照を生成してしまうリスクを伴い、
そうすると、2つのRc<T>の値がお互いを参照し合い、メモリリークを引き起こしてしまうことを思い出してください。
同様に、Mutex<T>はデッドロックを生成するリスクを伴っています。これは、処理が2つのリソースをロックする必要があり、
2つのスレッドがそれぞれにロックを1つ獲得して永久にお互いを待ちあってしまうときに起こります。
デッドロックに興味があるのなら、デッドロックのあるRustプログラムを組んでみてください;
それからどんな言語でもいいので、ミューテックスに対してデッドロックを緩和する方法を調べて、
Rustで是非、それを実装してみてください。Mutex<T>とMutexGuardに関する標準ライブラリのAPIドキュメンテーションは、
役に立つ情報を提供してくれます。
SendとSyncトレイトと、それらを独自の型で使用する方法について語って、この章を締めくくります。
SyncとSendトレイトで拡張可能な並行性
面白いことに、Rust言語には、寡少な並行性機能があります。この章でここまでに語った並行性機能のほとんどは、 標準ライブラリの一部であり、言語ではありません。並行性を扱う選択肢は、言語や標準ライブラリに制限されません; 独自の並行性機能を書いたり、他人が書いたものを利用したりできるのです。
ですが、2つの並行性概念が言語に埋め込まれています: std::markerトレイトのSyncとSendです。
Sendでスレッド間の所有権の転送を許可する
Sendマーカートレイトは、Sendを実装した型の所有権をスレッド間で転送できることを示唆します。
Rustのほとんどの型はSendですが、Rc<T>を含めて一部例外があります: この型は、Rc<T>の値をクローンし、
クローンしたものの所有権を別のスレッドに転送しようとしたら、両方のスレッドが同時に参照カウントを更新できてしまうので、
Sendになり得ません。このため、Rc<T>はスレッド安全性のためのパフォーマンスの犠牲を支払わなくても済む、
シングルスレッド環境で使用するために実装されているわけです。
故に、Rustの型システムとトレイト境界により、Rc<T>の値を不安全にスレッド間で誤って送信することが絶対ないよう保証してくれるのです。
リスト16-14でこれを試みた時には、the trait Send is not implemented for Rc<Mutex<i32>>というエラーが出ました。
SendのArc<T>に切り替えたら、コードはコンパイルできたわけです。
完全にSendの型からなる型も全て自動的にSendと印付けされます。生ポインタを除くほとんどの基本型もSendで、
生ポインタについては第19章で議論します。
Syncで複数のスレッドからのアクセスを許可する
Syncマーカートレイトは、Syncを実装した型は、複数のスレッドから参照されても安全であることを示唆します。
言い換えると、&T(Tへの参照)がSendなら、型TはSyncであり、参照が他のスレッドに安全に送信できることを意味します。
Send同様、基本型はSyncであり、Syncの型からのみ構成される型もまたSyncです。
Sendではなかったのと同じ理由で、スマートポインタのRc<T>もまたSyncではありません。
RefCell<T>型(これについては第15章で話しました)と関連するCell<T>系についてもSyncではありません。
RefCell<T>が実行時に行う借用チェックの実装は、スレッド安全ではないのです。
スマートポインタのMutex<T>はSyncで、「複数のスレッド間でMutex<T>を共有する」節で見たように、
複数のスレッドでアクセスを共有するのに使用することができます。
SendとSyncを手動で実装するのは非安全である
SendとSyncトレイトから構成される型は自動的にSendとSyncにもなるので、
それらのトレイトを手動で実装する必要はありません。マーカートレイトとして、
実装すべきメソッドさえも何もありません。並行性に関連する不変条件を強制することに役立つだけなのです。
これらのトレイトを手動で実装するには、unsafeなRustコードを実装することが関わってきます。
unsafeなRustコードを使用することについては第19章で語ります; とりあえず、重要な情報は、
SendとSyncではない部品からなる新しい並行な型を構成するには、安全性保証を保持するために、
注意深い思考が必要になるということです。The Rustonomiconには、
これらの保証とそれを保持する方法についての情報がより多くあります。
訳注: 日本語版のThe Rustonomiconはこちらです。
まとめ
この本において並行性を見かけるのは、これで最後ではありません: 第20章のプロジェクトでは、 この章の概念をここで議論した微小な例よりもより現実的な場面で使用するでしょう。
前述のように、Rustによる並行性の取扱いのごく一部のみが言語仕様なので、多くの並行性の解決策は クレートとして実装されています。これらは標準ライブラリよりも迅速に進化するので、 マルチスレッド環境で使用すべき現在の最先端のクレートを必ずネットで検索してください。
Rustの標準ライブラリは、メッセージ受け渡しにチャンネルを、並行の文脈で安全に使用できる、
Mutex<T>やArc<T>などのスマートポインタ型を提供しています。型システムと借用チェッカーにより、
これらの解決策を使用するコードがデータ競合や無効な参照に行き着かないことを保証してくれます。
一旦コードをコンパイルすることができたら、他の言語ではありふれている追跡困難な類のバグなしに、
複数のスレッドでも喜んで動くので安心できます。並行プログラミングは、もはや恐れるべき概念ではありません:
恐れることなく前進し、プログラムを並行にしてください!
次は、Rustプログラムが肥大化するにつれて問題をモデル化し、解決策を構造化する慣例的な方法について話します。 さらに、Rustのイディオムがオブジェクト指向プログラミングで馴染み深いかもしれないイディオムにどのように関連しているかについても議論します。
Rustのオブジェクト指向プログラミング機能
オブジェクト指向プログラミング(OOP)は、プログラムをモデル化する手段です。オブジェクトは、 1960年代のSimulaに端緒を発しています。このオブジェクトは、 お互いにメッセージを渡し合うというアラン・ケイ(Alan Kay)のプログラミングアーキテクチャに影響を及ぼしました。 彼は、このアーキテクチャを解説するために、オブジェクト指向プログラミングという用語を造語しました。 多くの競合する定義がOOPが何かを解説しています; Rustをオブジェクト指向と区分する定義もありますし、 しない定義もあります。この章では、広くオブジェクト指向と捉えられる特定の特徴と、 それらの特徴がこなれたRustでどう表現されるかを探究します。それからオブジェクト指向のデザインパターンをRustで実装する方法を示し、 そうすることとRustの強みを活用して代わりの解決策を実装する方法の代償を議論します。
オブジェクト指向言語の特徴
言語がオブジェクト指向と考えられるのになければならない機能について、プログラミングコミュニティ内での総意はありません。 RustはOOPを含めた多くのプログラミングパラダイムに影響を受けています; 例えば、 第13章で関数型プログラミングに由来する機能を探究しました。議論はあるかもしれませんが、 OOP言語は特定の一般的な特徴を共有しています。具体的には、オブジェクトやカプセル化、 継承などです。それらの個々の特徴が意味するものとRustがサポートしているかを見ましょう。
オブジェクトは、データと振る舞いを含む
エーリヒ・ガンマ(Enoch Gamma)、リチャード・ヘルム(Richard Helm)、ラルフ・ジョンソン(Ralph Johnson)、
ジョン・ブリシディース(John Vlissides)(アディソン・ワズリー・プロ)により、
1994年に書かれたデザインパターン: 再利用可能なオブジェクト指向ソフトウェアの要素という本は、
俗に4人のギャングの本(訳注: the Gang of Four book; GoFとよく略される)と呼ばれ、オブジェクト指向デザインパターンのカタログです。
そこでは、OOPは以下のように定義されています:
オブジェクト指向プログラムは、オブジェクトで構成される。オブジェクトは、 データとそのデータを処理するプロシージャを梱包している。このプロシージャは、 典型的にメソッドまたはオペレーションと呼ばれる。
この定義を使用すれば、Rustはオブジェクト指向です: 構造体とenumにはデータがありますし、
implブロックが構造体とenumにメソッドを提供します。メソッドのある構造体とenumは、
オブジェクトとは呼ばれないものの、GoFのオブジェクト定義によると、同じ機能を提供します。
カプセル化は、実装詳細を隠蔽する
OOPとよく紐づけられる別の側面は、カプセル化の思想です。これは、オブジェクトの実装詳細は、 そのオブジェクトを使用するコードにはアクセスできないことを意味します。故に、 オブジェクトと相互作用する唯一の手段は、その公開APIを通してです; オブジェクトを使用するコードは、 オブジェクトの内部に到達して、データや振る舞いを直接変更できるべきではありません。 このために、プログラマはオブジェクトの内部をオブジェクトを使用するコードを変更する必要なく、 変更しリファクタリングできます。
カプセル化を制御する方法は、第7章で議論しました: pubキーワードを使用して、
自分のコードのどのモジュールや型、関数、メソッドを公開するか決められ、
既定ではそれ以外のものは全て非公開になります。例えば、
i32値のベクタを含むフィールドのあるAveragedCollectionという構造体を定義できます。
この構造体はさらに、ベクタの値の平均を含むフィールドを持てます。つまり、平均は誰かが必要とする度に、
オンデマンドで計算する必要はないということです。言い換えれば、AveragedCollectionは、
計算した平均をキャッシュしてくれるわけです。リスト17-1には、AveragedCollection構造体の定義があります:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct AveragedCollection { list: Vec<i32>, average: f64, } }
リスト17-1: 整数のリストとコレクションの要素の平均を管理するAveragedCollection構造体
構造体は、他のコードが使用できるようにpubで印づけされていますが、構造体のフィールドは非公開のままです。
値が追加されたりリストから削除される度に、平均も更新されることを保証したいので、今回の場合重要です。
addやremove、averageメソッドを構造体に実装することでこれをします。リスト17-2のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { 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; } } }
リスト17-2: AveragedCollectionのadd、remove、average公開メソッドの実装
add、remove、averageの公開メソッドがAveragedCollectionのインスタンスを変更する唯一の方法になります。
要素がaddメソッドを使用してlistに追加されたり、removeメソッドを使用して削除されたりすると、
各メソッドの実装がaverageフィールドの更新を扱う非公開のupdate_averageメソッドも呼び出します。
listとaverageフィールドを非公開のままにしているので、外部コードが要素をlistフィールドに直接追加したり削除したりする方法はありません;
そうでなければ、averageフィールドは、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つの理由は、型システムに関連しています: 親の型と同じ箇所で子供の型を使用できるようにです。 これは、多相性(polymorphism)とも呼ばれ、複数のオブジェクトが特定の特徴を共有しているなら、 実行時にお互いに代用できることを意味します。
多相性
多くの人にとって、多相性は、継承の同義語です。ですが、実際には複数の型のデータを取り扱えるコードを指すより一般的な概念です。 継承について言えば、それらの型は一般的にはサブクラスです。
Rustは代わりにジェネリクスを使用して様々な可能性のある型を抽象化し、トレイト境界を使用してそれらの型が提供するものに制約を課します。 これは時に、パラメータ境界多相性(bounded parametric polymorphism)と呼ばれます。
継承は、近年、多くのプログラミング言語において、プログラムの設計解決策としては軽んじられています。 というのも、しばしば必要以上にコードを共有してしまう危険性があるからです。サブクラスは、 必ずしも親クラスの特徴を全て共有するべきではないのに、継承ではそうなってしまうのです。 これにより、プログラムの設計の柔軟性を失わせることもあります。また、道理に合わなかったり、メソッドがサブクラスには適用されないために、 エラーを発生させるようなサブクラスのメソッドの呼び出しを引き起こす可能性が出てくるのです。 さらに、サブクラスに1つのクラスからだけ継承させる言語もあり、さらにプログラムの設計の柔軟性が制限されます。
これらの理由により、継承ではなくトレイトオブジェクトを使用してRustは異なるアプローチを取っています。 Rustにおいて、トレイトオブジェクトがどう多相性を可能にするかを見ましょう。
トレイトオブジェクトで異なる型の値を許容する
第8章で、ベクタの1つの制限は、たった1つの型の要素を保持することしかできないことだと述べました。
リスト8-10で整数、浮動小数点数、テキストを保持する列挙子のあるSpreadsheetCell enumを定義して、
これを回避しました。つまり、各セルに異なる型のデータを格納しつつ、1行のセルを表すベクタを保持するということです。
コンパイル時にわかるある固定されたセットの型にしか取り替え可能な要素がならない場合には、
完璧な解決策です。
ところが、時として、ライブラリの使用者が特定の場面で合法になる型のセットを拡張できるようにしたくなることがあります。
これをどう実現する可能性があるか示すために、各アイテムにdrawメソッドを呼び出してスクリーンに描画するという、
GUIツールで一般的なテクニックをしてあるリストの要素を走査する例のGUIツールを作ります。
GUIライブラリの構造を含むguiと呼ばれるライブラリクレートを作成します。
このクレートには、他人が使用できるButtonやTextFieldなどの型が包含されるかもしれません。
さらに、guiの使用者は、描画可能な独自の型を作成したくなるでしょう: 例えば、
ある人はImageを追加し、別の人はSelectBoxを追加するかもしれません。
この例のために本格的なGUIライブラリは実装するつもりはありませんが、部品がどう組み合わさるかは示します。
ライブラリの記述時点では、他のプログラマが作成したくなる可能性のある型全てを知る由もなければ、定義することもできません。
しかし、guiは異なる型の多くの値を追いかけ、この異なる型の値に対してdrawメソッドを呼び出す必要があることは、
確かにわかっています。drawメソッドを呼び出した時に正確に何が起きるかを知っている必要はありません。
値にそのメソッドが呼び出せるようあることだけわかっていればいいのです。
継承のある言語でこれを行うには、drawという名前のメソッドがあるComponentというクラスを定義するかもしれません。
Button、Image、SelectBoxなどの他のクラスは、Componentを継承し、故にdrawメソッドを継承します。
個々にdrawメソッドをオーバーライドして、独自の振る舞いを定義するものの、フレームワークは、
Componentインスタンスであるかのようにその型全部を扱い、この型に対してdrawを呼び出します。
ですが、Rustに継承は存在しないので、使用者に新しい型で拡張してもらうためにguiライブラリを構成する他の方法が必要です。
一般的な振る舞いにトレイトを定義する
guiに欲しい振る舞いを実装するには、drawという1つのメソッドを持つDrawというトレイトを定義します。
それからトレイトオブジェクトを取るベクタを定義できます。トレイトオブジェクトは、
指定したトレイトを実装するある型のインスタンスを指します。&参照やBox<T>スマートポインタなどの、
何らかのポインタを指定し、それから関係のあるトレイトを指定する(トレイトオブジェクトがポインタを使用しなければならない理由については、
第19章の「動的サイズ決定型とSizedトレイト」節で語ります)ことでトレイトオブジェクトを作成します。
ジェネリックまたは具体的な型があるところにトレイトオブジェクトは使用できます。どこでトレイトオブジェクトを使用しようと、
Rustの型システムは、コンパイル時にその文脈で使用されているあらゆる値がそのトレイトオブジェクトのトレイトを実装していることを保証します。
結果としてコンパイル時に可能性のある型を全て知る必要はなくなるのです。
Rustでは、構造体とenumを他の言語のオブジェクトと区別するために「オブジェクト」と呼ぶことを避けていることに触れましたね。
構造体やenumにおいて、構造体のフィールドのデータやimplブロックの振る舞いは区分けされているものの、
他の言語では1つの概念に押し込められるデータと振る舞いは、しばしばオブジェクトと分類されます。
しかしながら、トレイトオブジェクトは、データと振る舞いをごちゃ混ぜにするという観点で他の言語のオブジェクトに近いです。
しかし、トレイトオブジェクトは、データを追加できないという点で伝統的なオブジェクトと異なっています。
トレイトオブジェクトは、他の言語のオブジェクトほど一般的に有用ではありません:
その特定の目的は、共通の振る舞いに対して抽象化を行うことです。
リスト17-3は、drawという1つのメソッドを持つDrawというトレイトを定義する方法を示しています:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub trait Draw { fn draw(&self); } }
リスト17-3: Drawトレイトの定義
この記法は、第10章のトレイトの定義方法に関する議論で馴染み深いはずです。その次は、新しい記法です:
リスト17-4では、componentsというベクタを保持するScreenという名前の構造体を定義しています。
このベクタの型はBox<Draw>で、これはトレイトオブジェクトです; Drawトレイトを実装するBox内部の任意の型に対する代役です。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub trait Draw { fn draw(&self); } pub struct Screen { pub components: Vec<Box<Draw>>, } }
リスト17-4: Drawトレイトを実装するトレイトオブジェクトのベクタを保持するcomponentsフィールドがある
Screen構造体の定義
Screen構造体に、componentsの各要素に対してdrawメソッドを呼び出すrunというメソッドを定義します。
リスト17-5のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub trait Draw { fn draw(&self); } pub struct Screen { pub components: Vec<Box<Draw>>, } impl Screen { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } } }
リスト17-5: 各コンポーネントに対してdrawメソッドを呼び出すScreenのrunメソッド
これは、トレイト境界を伴うジェネリックな型引数を使用する構造体を定義するのとは異なる動作をします。
ジェネリックな型引数は、一度に1つの具体型にしか置き換えられないのに対して、トレイトオブジェクトは、
実行時にトレイトオブジェクトに対して複数の具体型で埋めることができます。例として、
ジェネリックな型とトレイト境界を使用してリスト17-6のようにScreen構造体を定義することもできました:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { 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(); } } } }
リスト17-6: ジェネリクスとトレイト境界を使用したScreen構造体とrunメソッドの対立的な実装
こうすると、全てのコンポーネントの型がButtonだったり、TextFieldだったりするScreenのインスタンスに制限されてしまいます。
絶対に同種のコレクションしか持つ予定がないのなら、ジェネリクスとトレイト境界は、
定義がコンパイル時に具体的な型を使用するように単相化されるので、望ましいです。
一方で、メソッドがトレイトオブジェクトを使用すると、1つのScreenインスタンスが、
Box<Button>とBox<TextField>を含むVec<T>を保持できます。
この動作方法を見、それから実行時性能の裏の意味について語りましょう。
トレイトを実装する
さて、Drawトレイトを実装する型を追加しましょう。Button型を提供します。ここも、実際にGUIライブラリを実装することは、
この本の範疇を超えているので、drawメソッドの本体は、何も有用な実装はしません。実装がどんな感じになるか想像するために、
Button構造体は、width、height、labelフィールドを持っている可能性があります。
リスト17-7に示したようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub trait Draw { fn draw(&self); } 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 // 実際にボタンを描画するコード } } }
リスト17-7: Drawトレイトを実装するあるButton構造体
Buttonのwidth、height、labelフィールドは、TextField型のように、
それらのフィールドプラスplaceholderフィールドを代わりに持つ可能性のある他のコンポーネントのフィールドとは異なるでしょう。
スクリーンに描画したい型のコンポーネントはそれぞれDrawトレイトを実装しますが、
Buttonがここでしているように、drawメソッドでは異なるコードを使用してその特定の型を描画する方法を定義しています(実際のGUIコードは、
この章の範疇を超えるのでありませんが)。例えば、Buttonには、ユーザがボタンをクリックした時に起こることに関連するメソッドを含む、
追加のimplブロックがある可能性があります。この種のメソッドは、TextFieldのような型には適用されません。
ライブラリの使用者が、width、height、optionsフィールドのあるSelectBox構造体を実装しようと決めたら、
SelectBox型にもDrawトレイトを実装します。リスト17-8のようにですね:
ファイル名: src/main.rs
extern crate gui;
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
//セレクトボックスを実際に描画するコード
}
}
リスト17-8: guiを使用し、SelectBox構造体にDrawトレイトを実装する別のクレート
ライブラリの使用者はもう、main関数を書き、Screenインスタンスを生成できます。Screenインスタンスには、
それぞれをBox<T>に放り込んでトレイトオブジェクト化してSelectBoxとButtonを追加できます。
それからScreenインスタンスに対してrunメソッドを呼び出すことができ、そうすると各コンポーネントのdrawが呼び出されます。
リスト17-9は、この実装を示しています:
ファイル名: src/main.rs
use gui::{Screen, Button};
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();
}
リスト17-9: トレイトオブジェクトを使って同じトレイトを実装する異なる型の値を格納する
ライブラリを記述した時点では、誰かがSelectBox型を追加する可能性があるなんて知りませんでしたが、
Screenの実装は、新しい型を処理し、描画することができました。何故なら、SelectBoxはDraw型、
つまり、drawメソッドを実装しているからです。
この値の具体的な型ではなく、値が応答したメッセージにのみ関係するという概念は、
動的型付け言語のダックタイピングに似た概念です: アヒルのように歩き、鳴くならば、
アヒルに違いないのです!リスト17-5のScreenのrunの実装では、runは、
各コンポーネントの実際の型がなんであるか知る必要はありません。コンポーネントが、
ButtonやSelectBoxのインスタンスであるかを確認することはなく、コンポーネントのdrawメソッドを呼び出すだけです。
componentsベクタでBox<Draw>を値の型として指定することで、Screenを、
drawメソッドを呼び出せる値を必要とするように定義できたのです。
注釈: ダックタイピングについて
ご存知かもしれませんが、ダックタイピングについて補足です。ダックタイピングとは、動的型付け言語やC++のテンプレートで使用される、 特定のフィールドやメソッドがあることを想定してコンパイルを行い、実行時に実際にあることを確かめるというプログラミング手法です。 ダック・テストという思考法に由来するそうです。
ダックタイピングの利点は、XMLやJSONなど、厳密なスキーマがないことが多い形式を扱いやすくなること、 欠点は、実行してみるまで動くかどうかわからないことでしょう。
トレイトオブジェクトとRustの型システムを使用してダックタイピングを活用したコードに似たコードを書くことの利点は、 実行時に値が特定のメソッドを実装しているか確認したり、値がメソッドを実装していない時にエラーになることを心配したりする必要は絶対になく、 とにかく呼び出せることです。コンパイラは、値が、トレイトオブジェクトが必要としているトレイトを実装していなければ、 コンパイルを通さないのです。
例えば、リスト17-10は、コンポーネントにStringのあるScreenを作成しようとした時に起こることを示しています:
ファイル名: src/main.rs
extern crate gui;
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![
Box::new(String::from("Hi")),
],
};
screen.run();
}
リスト17-10: トレイトオブジェクトのトレイトを実装しない型の使用を試みる
StringはDrawトレイトを実装していないので、このようなエラーが出ます:
error[E0277]: the trait bound `std::string::String: gui::Draw` is not satisfied
--> src/main.rs:7:13
|
7 | Box::new(String::from("Hi")),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait gui::Draw is not
implemented for `std::string::String`
|
= note: required for the cast to the object type `gui::Draw`
このエラーは、渡すことを意図していないものをScreenに渡しているので、異なる型を渡すべきか、
Screenがdrawを呼び出せるようにStringにDrawを実装するべきのどちらかであることを知らせてくれています。
トレイトオブジェクトは、ダイナミックディスパッチを行う
第10章の「ジェネリクスを使用したコードのパフォーマンス」節でジェネリクスに対してトレイト境界を使用した時に、 コンパイラが行う単相化過程の議論を思い出してください: コンパイラは、関数やメソッドのジェネリックでない実装を、 ジェネリックな型引数の箇所に使用している具体的な型に対して生成するのでした。単相化の結果吐かれるコードは、 スタティックディスパッチを行い、これは、コンパイル時にコンパイラがどのメソッドを呼び出しているかわかる時のことです。 これは、ダイナミックディスパッチとは対照的で、この時、コンパイラは、コンパイル時にどのメソッドを呼び出しているのかわかりません。 ダイナミックディスパッチの場合、コンパイラは、どのメソッドを呼び出すか実行時に弾き出すコードを生成します。
トレイトオブジェクトを使用すると、コンパイラはダイナミックディスパッチを使用しなければなりません。 コンパイラは、トレイトオブジェクトを使用しているコードで使用される可能性のある型全てを把握しないので、 どの型に実装されたどのメソッドを呼び出すかわからないのです。代わりに実行時に、トレイトオブジェクト内でポインタを使用して、 コンパイラは、どのメソッドを呼ぶか知ります。スタティックディスパッチでは行われないこの検索が起きる時には、 実行時コストがあります。また、ダイナミックディスパッチは、コンパイラがメソッドのコードをインライン化することも妨げ、 そのため、ある種の最適化が不可能になります。ですが、リスト17-5で記述し、 リスト17-9ではサポートできたコードで追加の柔軟性を確かに得られたので、考慮すべき代償です。
トレイトオブジェクトには、オブジェクト安全性が必要
トレイトオブジェクトには、オブジェクト安全なトレイトしか作成できません。 トレイトオブジェクトを安全にする特性全てを司る複雑な規則がありますが、実際には、2つの規則だけが関係があります。 トレイトは、トレイト内で定義されているメソッド全てに以下の特性があれば、オブジェクト安全になります。
- 戻り値の型が
Selfでない。 - ジェネリックな型引数がない。
Selfキーワードは、トレイトやメソッドを実装しようとしている型の別名です。トレイトオブジェクトは、
一旦、トレイトオブジェクトを使用したら、コンパイラにはそのトレイトを実装している具体的な型を知りようがないので、
オブジェクト安全でなければなりません。トレイトメソッドが具体的なSelf型を返すのに、
トレイトオブジェクトがSelfの具体的な型を忘れてしまったら、メソッドが元の具体的な型を使用できる手段はなくなってしまいます。
同じことがトレイトを使用する時に具体的な型引数で埋められるジェネリックな型引数に対しても言えます:
具体的な型がトレイトを実装する型の一部になるのです。トレイトオブジェクトの使用を通して型が忘却されたら、
そのジェネリックな型引数を埋める型がなんなのか知る術はないのです。
メソッドがオブジェクト安全でないトレイトの例は、標準ライブラリのCloneトレイトです。
Cloneトレイトのcloneメソッドのシグニチャは以下のような感じです:
#![allow(unused)] fn main() { pub trait Clone { fn clone(&self) -> Self; } }
String型はCloneトレイトを実装していて、Stringのインスタンスに対してcloneメソッドを呼び出すと、
Stringのインスタンスが返ってきます。同様に、Vec<T>のインスタンスに対してcloneを呼び出すと、
Vec<T>のインスタンスが返ってきます。cloneのシグニチャは、Selfの代わりに入る型を知る必要があります。
それが、戻り値の型になるからです。
コンパイラは、トレイトオブジェクトに関していつオブジェクト安全の規則を侵害するようなことを試みているかを示唆します。
例えば、リスト17-4でScreen構造体を実装してDrawトレイトではなく、
Cloneトレイトを実装した型を保持しようとしたとしましょう。こんな感じで:
pub struct Screen {
pub components: Vec<Box<Clone>>,
}
こんなエラーになるでしょう:
error[E0038]: the trait `std::clone::Clone` cannot be made into an object
(エラー: `std::clone::Clone`トレイトは、オブジェクトにすることはできません)
--> src/lib.rs:2:5
|
2 | pub components: Vec<Box<Clone>>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone` cannot be
made into an object
|
= note: the trait cannot require that `Self : Sized`
(注釈: このトレイトは、`Self : Sized`を満たせません)
このエラーは、このようにこのトレイトをトレイトオブジェクトとして使用することはできないことを意味しています。 オブジェクト安全性についての詳細に興味があるのなら、Rust RFC 255を参照されたし。
オブジェクト指向デザインパターンを実装する
ステートパターンは、オブジェクト指向デザインパターンの1つです。このパターンの肝は、 値が一連のステートオブジェクトで表されるなんらかの内部状態を持ち、 その内部の状態に基づいて値の振る舞いが変化するというものです。ステートオブジェクトは、 機能を共有します: Rustでは、もちろん、オブジェクトと継承ではなく、構造体とトレイトを使用します。 各ステートオブジェクトは、自身の振る舞いと別の状態に変化すべき時を司ることに責任を持ちます。 ステートオブジェクトを保持する値は、状態ごとの異なる振る舞いや、いつ状態が移行するかについては何も知りません。
ステートパターンを使用することは、プログラムの業務要件が変わる時、状態を保持する値のコードや、 値を使用するコードを変更する必要はないことを意味します。ステートオブジェクトの1つのコードを更新して、 規則を変更したり、あるいはおそらくステートオブジェクトを追加する必要しかないのです。 ステートデザインパターンの例と、そのRustでの使用方法を見ましょう。
ブログ記事のワークフローを少しずつ実装していきます。ブログの最終的な機能は以下のような感じになるでしょう:
- ブログ記事は、空の草稿から始まる。
- 草稿ができたら、査読が要求される。
- 記事が承認されたら、公開される。
- 公開されたブログ記事だけが表示する内容を返すので、未承認の記事は、誤って公開されない。
それ以外の記事に対する変更は、効果を持つべきではありません。例えば、査読を要求する前にブログ記事の草稿を承認しようとしたら、 記事は、非公開の草稿のままになるべきです。
リスト17-11は、このワークフローをコードの形で示しています: これは、
blogというライブラリクレートに実装するAPIの使用例です。まだblogクレートを実装していないので、
コンパイルはできません。
ファイル名: src/main.rs
extern crate 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());
}
リスト17-11: blogクレートに欲しい振る舞いをデモするコード
ユーザがPost::newで新しいブログ記事の草稿を作成できるようにしたいです。それから、
草稿状態の間にブログ記事にテキストを追加できるようにしたいです。承認前に記事の内容を即座に得ようとしたら、
記事はまだ草稿なので、何も起きるべきではありません。デモ目的でコードにassert_eq!を追加しました。
これに対する素晴らしい単体テストは、ブログ記事の草稿がcontentメソッドから空の文字列を返すことをアサートすることでしょうが、
この例に対してテストを書くつもりはありません。
次に、記事の査読を要求できるようにしたく、また査読を待機している間はcontentに空の文字列を返してほしいです。
記事が承認を受けたら、公開されるべきです。つまり、contentを呼んだ時に記事のテキストが返されるということです。
クレートから相互作用している唯一の型は、Postだけであることに注意してください。
この型はステートパターンを使用し、記事がなり得る種々の状態を表す3つのステートオブジェクトのうちの1つになる値を保持します。
草稿、査読待ち、公開中です。1つの状態から別の状態への変更は、Post型内部で管理されます。
Postインスタンスのライブラリ使用者が呼び出すメソッドに呼応して状態は変化しますが、
状態の変化を直接管理する必要はありません。また、ユーザは、
査読前に記事を公開するなど状態を誤ることはありません。
Postを定義し、草稿状態で新しいインスタンスを生成する
ライブラリの実装に取り掛かりましょう!なんらかの内容を保持する公開のPost構造体が必要なことはわかるので、
構造体の定義と、関連する公開のPostインスタンスを生成するnew関数から始めましょう。リスト17-12のようにですね。
また、非公開のStateトレイトも作成します。それから、Postはstateという非公開のフィールドに、
OptionでBox<State>のトレイトオブジェクトを保持します。Optionが必要な理由はすぐわかります。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { state: Option<Box<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 {} }
リスト17-12: Post構造体、新規Postインスタンスを生成するnew関数、
Stateトレイト、Draft構造体の定義
Stateトレイトは、異なる記事の状態で共有される振る舞いを定義し、Draft、PendingReview、Published状態は全て、
Stateトレイトを実装します。今は、トレイトにメソッドは何もなく、Draftが記事の初期状態にしたい状態なので、
その状態だけを定義することから始めます。
新しいPostを作る時、stateフィールドは、Boxを保持するSome値にセットします。
このBoxがDraft構造体の新しいインスタンスを指します。これにより、
新しいPostを作る度に、草稿から始まることが保証されます。Postのstateフィールドは非公開なので、
Postを他の状態で作成する方法はないのです!Post::new関数では、contentフィールドを新しい空のStringにセットしています。
記事の内容のテキストを格納する
リスト17-11は、add_textというメソッドを呼び出し、ブログ記事のテキスト内容に追加される&strを渡せるようになりたいことを示しました。
これをcontentフィールドをpubにして晒すのではなく、メソッドとして実装しています。
これは、後ほどcontentフィールドデータの読まれ方を制御するメソッドを実装できることを意味しています。
add_textメソッドは非常に素直なので、リスト17-13の実装をimpl Postブロックに追加しましょう:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { content: String, } impl Post { // --snip-- pub fn add_text(&mut self, text: &str) { self.content.push_str(text); } } }
リスト17-13: 記事のcontentにテキストを追加するadd_textメソッドを実装する
add_textメソッドは、selfへの可変参照を取ります。というのも、add_textを呼び出したPostインスタンスを変更しているからです。
それからcontentのStringに対してpush_strを呼び出し、text引数を渡して保存されたcontentに追加しています。
この振る舞いは、記事の状態によらないので、ステートパターンの一部ではありません。add_textメソッドは、
stateフィールドと全く相互作用しませんが、サポートしたい振る舞いの一部ではあります。
草稿の記事の内容は空であることを保証する
add_textを呼び出して記事に内容を追加した後でさえ、記事はまだ草稿状態なので、
それでもcontentメソッドには空の文字列スライスを返してほしいです。
リスト17-11の8行目で示したようにですね。とりあえず、この要求を実現する最も単純な方法でcontentメソッドを実装しましょう:
常に空の文字列スライスを返すことです。一旦、記事の状態を変更する能力を実装したら、公開できるように、
これを後ほど変更します。ここまで、記事は草稿状態にしかなり得ないので、記事の内容は常に空のはずです。
リスト17-14は、この仮の実装を表示しています:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { content: String, } impl Post { // --snip-- pub fn content(&self) -> &str { "" } } }
リスト17-14: Postに常に空の文字列スライスを返すcontentの仮の実装を追加する
この追加されたcontentメソッドとともに、リスト17-11の8行目までのコードは、想定通り動きます。
記事の査読を要求すると、状態が変化する
次に、記事の査読を要求する機能を追加する必要があり、これをすると、状態がDraftからPendingReviewに変わるはずです。
リスト17-15はこのコードを示しています:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { state: Option<Box<State>>, content: String, } impl Post { // --snip-- 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<State>; } struct Draft {} impl State for Draft { fn request_review(self: Box<Self>) -> Box<State> { Box::new(PendingReview {}) } } struct PendingReview {} impl State for PendingReview { fn request_review(self: Box<Self>) -> Box<State> { self } } }
リスト17-15: PostとStateトレイトにrequest_reviewメソッドを実装する
Postにselfへの可変参照を取るrequest_reviewという公開メソッドを与えます。それから、
Postの現在の状態に対して内部のrequest_reviewメソッドを呼び出し、
この2番目のrequest_reviewが現在の状態を消費し、新しい状態を返します。
Stateトレイトにrequest_reviewメソッドを追加しました; このトレイトを実装する型は全て、
これでrequest_reviewメソッドを実装する必要があります。メソッドの第1引数にself、&self、&mut selfではなく、
self: Box<Self>としていることに注意してください。この記法は、型を保持するBoxに対して呼ばれた時のみ、
このメソッドが合法になることを意味しています。この記法は、Box<Self>の所有権を奪い、古い状態を無効化するので、
Postの状態値は、新しい状態に変形できます。
古い状態を消費するために、request_reviewメソッドは、状態値の所有権を奪う必要があります。
ここでPostのstateフィールドのOptionが問題になるのです: takeメソッドを呼び出して、
stateフィールドからSome値を取り出し、その箇所にNoneを残します。なぜなら、Rustは、
構造体に未代入のフィールドを持たせてくれないからです。これにより、借用するのではなく、
Postのstate値をムーブすることができます。それから、記事のstate値をこの処理の結果にセットするのです。
self.state = self.state.request_review();のようなコードで直接state値の所有権を得るよう設定するのではなく、
一時的にNoneにstateをセットする必要があります。これにより、新しい状態に変形した後に、
Postが古いstate値を使えないことが保証されるのです。
Draftのrequest_reviewメソッドは、新しいPendingReview構造体の新しいボックスのインスタンスを返す必要があり、
これが、記事が査読待ちの時の状態を表します。PendingReview構造体もrequest_reviewメソッドを実装しますが、
何も変形はしません。むしろ、自身を返します。というのも、既にPendingReview状態にある記事の査読を要求したら、
PendingReview状態に留まるべきだからです。
ようやくステートパターンの利点が見えてき始めました: state値が何であれ、Postのrequest_reviewメソッドは同じです。
各状態は、独自の規則にのみ責任を持ちます。
Postのcontentメソッドを空の文字列スライスを返してそのままにします。
これでPostはPendingReviewとDraft状態になり得ますが、PendingReview状態でも、
同じ振る舞いが欲しいです。もうリスト17-11は11行目まで動くようになりました!
contentの振る舞いを変化させるapproveメソッドを追加する
approveメソッドは、request_reviewメソッドと類似するでしょう: 状態が承認された時に、
現在の状態があるべきと言う値にstateをセットします。リスト17-16のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { state: Option<Box<State>>, content: String, } impl Post { // --snip-- 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<State>; fn approve(self: Box<Self>) -> Box<State>; } struct Draft {} impl State for Draft { fn request_review(self: Box<Self>) -> Box<State> { Box::new(PendingReview {}) } // --snip-- fn approve(self: Box<Self>) -> Box<State> { self } } struct PendingReview {} impl State for PendingReview { fn request_review(self: Box<Self>) -> Box<State> { self } // --snip-- fn approve(self: Box<Self>) -> Box<State> { Box::new(Published {}) } } struct Published {} impl State for Published { fn request_review(self: Box<Self>) -> Box<State> { self } fn approve(self: Box<Self>) -> Box<State> { self } } }
リスト17-16: PostとStateトレイトにapproveメソッドを実装する
Stateトレイトにapproveメソッドを追加し、Published状態というStateを実装する新しい構造体を追加します。
request_reviewのように、Draftに対してapproveメソッドを呼び出したら、selfを返すので、
何も効果はありません。PendingReviewに対してapproveを呼び出すと、
Published構造体の新しいボックス化されたインスタンスを返します。Published構造体はStateトレイトを実装し、
request_reviewメソッドとapproveメソッド両方に対して、自身を返します。
そのような場合に記事は、Published状態に留まるべきだからです。
さて、Postのcontentメソッドを更新する必要が出てきました: 状態がPublishedなら、
記事のcontentフィールドの値を返したいのです; それ以外なら、空の文字列スライスを返したいです。
リスト17-17のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { trait State { fn content<'a>(&self, post: &'a Post) -> &'a str; } pub struct Post { state: Option<Box<State>>, content: String, } impl Post { // --snip-- pub fn content(&self) -> &str { self.state.as_ref().unwrap().content(&self) } // --snip-- } }
リスト17-17: Postのcontentメソッドを更新してStateのcontentメソッドに委譲する
目的は、これらの規則全てをStateを実装する構造体の内部に押し留めることなので、stateの値に対してcontentメソッドを呼び出し、
記事のインスタンス(要するに、self)を引数として渡します。そして、state値のcontentメソッドを使用したことから返ってきた値を返します。
Optionに対してas_refメソッドを呼び出します。値の所有権ではなく、Option内部の値への参照が欲しいからです。
stateはOption<Box<State>>なので、as_refを呼び出すと、Option<&Box<State>>が返ってきます。
as_refを呼ばなければ、stateを関数引数の借用した&selfからムーブできないので、エラーになるでしょう。
さらにunwrapメソッドを呼び出し、これは絶対にパニックしないことがわかっています。何故なら、
Postのメソッドが、それらのメソッドが完了した際にstateは常にSome値を含んでいることを保証するからです。
これは、コンパイラには理解不能であるものの、
None値が絶対にあり得ないとわかる第9章の「コンパイラよりも情報を握っている場合」節で語った一例です。
この時点で、&Box<State>に対してcontentを呼び出すと、参照外し型強制が&とBoxに働くので、
究極的にcontentメソッドがStateトレイトを実装する型に対して呼び出されることになります。
つまり、contentをStateトレイト定義に追加する必要があり、そこが現在の状態に応じてどの内容を返すべきかというロジックを配置する場所です。
リスト17-18のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { content: String } trait State { // --snip-- fn content<'a>(&self, post: &'a Post) -> &'a str { "" } } // --snip-- struct Published {} impl State for Published { // --snip-- fn content<'a>(&self, post: &'a Post) -> &'a str { &post.content } } }
リスト17-18: Stateトレイトにcontentメソッドを追加する
空の文字列スライスを返すデフォルト実装をcontentメソッドに追加しています。これにより、
DraftとPendingReview構造体にcontentを実装する必要はありません。Published構造体は、
contentメソッドをオーバーライドし、post.contentの値を返します。
第10章で議論したように、このメソッドにはライフタイム注釈が必要なことに注意してください。
postへの参照を引数として取り、そのpostの一部への参照を返しているので、
返却される参照のライフタイムは、post引数のライフタイムに関連します。
出来上がりました。要するに、リスト17-11はもう動くようになったのです!ブログ記事ワークフローの規則でステートパターンを実装しました。
その規則に関連するロジックは、Post中に散乱するのではなく、ステートオブジェクトに息づいています。
ステートパターンの代償
オブジェクト指向のステートパターンを実装して各状態の時に記事がなり得る異なる種類の振る舞いをカプセル化する能力が、
Rustにあることを示してきました。Postのメソッドは、種々の振る舞いについては何も知りません。
コードを体系化する仕方によれば、公開された記事が振る舞うことのある様々な方法を知るには、1箇所のみを調べればいいのです:
Published構造体のStateトレイトの実装です。
ステートパターンを使用しない対立的な実装を作ることになったら、代わりにPostのメソッドか、
あるいは記事の状態を確認し、それらの箇所(編注: Postのメソッドのことか)の振る舞いを変更するmainコードでさえ、
match式を使用したかもしれません。そうなると、複数個所を調べて記事が公開状態にあることの裏の意味全てを理解しなければならなくなります!
これは、追加した状態が増えれば、さらに上がるだけでしょう: 各match式には、別のアームが必要になるのです。
ステートパターンでは、PostのメソッドとPostを使用する箇所で、match式が必要になることはなく、
新しい状態を追加するのにも、新しい構造体を追加し、その1つの構造体にトレイトメソッドを実装するだけでいいわけです。
ステートパターンを使用した実装は、拡張して機能を増やすことが容易です。 ステートパターンを使用するコードの管理の単純さを確認するために、以下の提言を試してみてください:
- 記事の状態を
PendingReviewからDraftに戻すrejectメソッドを追加する。 - 状態が
Publishedに変化させられる前にapproveを2回呼び出す必要があるようにする。 - 記事が
Draft状態の時のみテキスト内容をユーザが追加できるようにする。 ヒント: ステートオブジェクトに内容について変わる可能性のあるものの責任を持たせつつも、Postを変更することには責任を持たせない。
ステートパターンの欠点の1つは、状態が状態間の遷移を実装しているので、状態の一部が密に結合した状態になってしまうことです。
PendingReviewとPublishedの間に、Scheduledのような別の状態を追加したら、
代わりにPendingReviewのコードをScheduledに遷移するように変更しなければならないでしょう。
状態が追加されてもPendingReviewを変更する必要がなければ、作業が減りますが、
そうすれば別のデザインパターンに切り替えることになるでしょう。
別の欠点は、ロジックの一部を重複させてしまうことです。重複を除くためには、
Stateトレイトのrequest_reviewとapproveメソッドにselfを返すデフォルト実装を試みる可能性があります;
ですが、これはオブジェクト安全性を侵害するでしょう。というのも、具体的なselfが一体なんなのかトレイトには知りようがないからです。
Stateをトレイトオブジェクトとして使用できるようにしたいので、メソッドにはオブジェクト安全になってもらう必要があるのです。
他の重複には、Postのrequest_reviewとapproveメソッドの実装が似ていることが含まれます。
両メソッドはOptionのstateの値に対する同じメソッドの実装に委譲していて、stateフィールドの新しい値を結果にセットします。
このパターンに従うPostのメソッドが多くあれば、マクロを定義して繰り返しを排除することも考慮する可能性があります(マクロについては付録Dを参照)。
オブジェクト指向言語で定義されている通り忠実にステートパターンを実装することで、
Rustの強みをできるだけ発揮していません。blogクレートに対して行える無効な状態と遷移をコンパイルエラーにできる変更に目を向けましょう。
状態と振る舞いを型としてコード化する
ステートパターンを再考して別の代償を得る方法をお見せします。状態と遷移を完全にカプセル化して、 外部のコードに知らせないようにするよりも、状態を異なる型にコード化します。結果的に、 Rustの型検査システムが、公開記事のみが許可される箇所で草稿記事の使用を試みることをコンパイルエラーを発して阻止します。
リスト17-11のmainの最初の部分を考えましょう:
ファイル名: src/main.rs
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
}
それでも、Post::newで草稿状態の新しい記事を生成することと記事の内容にテキストを追加する能力は可能にします。
しかし、空の文字列を返す草稿記事のcontentメソッドを保持する代わりに、草稿記事は、
contentメソッドを全く持たないようにします。そうすると、草稿記事の内容を得ようとしたら、
メソッドが存在しないというコンパイルエラーになるでしょう。その結果、
誤ってプロダクションコードで草稿記事の内容を表示することが不可能になります。
そのようなコードは、コンパイルさえできないからです。リスト17-19はPost構造体、DraftPost構造体、
さらにメソッドの定義を示しています:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { 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); } } }
リスト17-19: contentメソッドのあるPostとcontentメソッドのないDraftPost
PostとDraftPost構造体どちらにもブログ記事のテキストを格納する非公開のcontentフィールドがあります。
状態のコード化を構造体の型に移動したので、この構造体は最早stateフィールドを持ちません。
Postは公開された記事を表し、contentを返すcontentメソッドがあります。
それでもPost::new関数はありますが、Postのインスタンスを返すのではなく、DraftPostのインスタンスを返します。
contentは非公開であり、Postを返す関数も存在しないので、現状Postのインスタンスを生成することは不可能です。
DraftPost構造体には、以前のようにテキストをcontentに追加できるようadd_textメソッドがありますが、
DraftPostにはcontentメソッドが定義されていないことに注目してください!
従って、これでプログラムは、全ての記事が草稿記事から始まり、草稿記事は表示できる内容がないことを保証します。
この制限をかいくぐる試みは、全てコンパイルエラーに落ち着くでしょう。
遷移を異なる型への変形として実装する
では、どうやって公開された記事を得るのでしょうか?公開される前に草稿記事は査読され、
承認されなければならないという規則を強制したいです。査読待ち状態の記事は、それでも内容を表示するべきではありません。
別の構造体PendingReviewPostを追加し、DraftPostにPendingReviewPostを返すrequest_reviewメソッドを定義し、
PendingReviewPostにPostを返すapproveメソッドを定義してこれらの制限を実装しましょう。リスト17-20のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Post { content: String, } pub struct DraftPost { content: String, } impl DraftPost { // --snip-- 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, } } } }
リスト17-20: DraftPostのrequest_reviewを呼び出すことで生成されるPendingReviewPostと、
PendingReviewPostを公開されたPostに変換するapproveメソッド
request_reviewとapproveメソッドはselfの所有権を奪い、故にDraftPostとPendingReviewPostインスタンスを消費し、
それぞれPendingReviewPostと公開されたPostに変形します。このように、
DraftPostインスタンスにrequest_reviewを呼んだ後には、DraftPostインスタンスは生きながらえず、
以下同様です。PendingReviewPost構造体には、contentメソッドが定義されていないので、
DraftPost同様に、その内容を読もうとするとコンパイルエラーに落ち着きます。
contentメソッドが確かに定義された公開されたPostインスタンスを得る唯一の方法が、
PendingReviewPostに対してapproveを呼び出すことであり、PendingReviewPostを得る唯一の方法が、
DraftPostにrequest_reviewを呼び出すことなので、これでブログ記事のワークフローを型システムにコード化しました。
ですが、さらにmainにも多少小さな変更を行わなければなりません。request_reviewとapproveメソッドは、
呼ばれた構造体を変更するのではなく、新しいインスタンスを返すので、let post =というシャドーイング代入をもっと追加し、
返却されたインスタンスを保存する必要があります。また、草稿と査読待ち記事の内容を空の文字列でアサートすることも、
する必要もありません: 最早、その状態にある記事の内容を使用しようとするコードはコンパイル不可能だからです。
mainの更新されたコードは、リスト17-21に示されています:
ファイル名: src/main.rs
extern crate blog;
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());
}
リスト17-21: ブログ記事ワークフローの新しい実装を使うmainの変更
postを再代入するためにmainに行う必要のあった変更は、この実装がもう、
全くオブジェクト指向のステートパターンに沿っていないことを意味します:
状態間の変形は最早、Post実装内に完全にカプセル化されていません。
ですが、型システムとコンパイル時に起きる型チェックのおかげでもう無効な状態があり得なくなりました。
これにより、未公開の記事の内容が表示されるなどの特定のバグが、プロダクションコードに移る前に発見されることが保証されます。
blogクレートに関してこの節の冒頭で触れた追加の要求に提言される作業をそのままリスト17-20の後に試してみて、
このバージョンのコードについてどう思うか確かめてください。この設計では、
既に作業の一部が達成されている可能性があることに注意してください。
Rustは、オブジェクト指向のデザインパターンを実装する能力があるものの、状態を型システムにコード化するなどの他のパターンも、 Rustでは利用可能なことを確かめました。これらのパターンには、異なる代償があります。 あなたが、オブジェクト指向のパターンには非常に馴染み深い可能性があるものの、問題を再考してRustの機能の強みを活かすと、 コンパイル時に一部のバグを回避できるなどの利益が得られることもあります。オブジェクト指向のパターンは、 オブジェクト指向言語にはない所有権などの特定の機能によりRustでは、必ずしも最善の解決策ではないでしょう。
まとめ
この章読了後に、あなたがRustはオブジェクト指向言語であると考えるかどうかに関わらず、 もうトレイトオブジェクトを使用してRustでオブジェクト指向の機能の一部を得ることができると知っています。 ダイナミックディスパッチは、多少の実行時性能と引き換えにコードに柔軟性を齎してくれます。 この柔軟性を利用してコードのメンテナンス性に寄与するオブジェクト指向パターンを実装することができます。 Rustにはまた、オブジェクト指向言語にはない所有権などの他の機能もあります。オブジェクト指向パターンは、 必ずしもRustの強みを活かす最善の方法にはなりませんが、利用可能な選択肢の1つではあります。
次は、パターンを見ます。パターンも多くの柔軟性を可能にするRustの別の機能です。 本全体を通して僅かに見かけましたが、まだその全能力は目の当たりにしていません。さあ、行きましょう!
パターンとマッチング
パターンは、複雑であれ、単純であれ、Rustで型の構造に一致する特別な記法です。match式や他の構文と組み合わせてパターンを使用すると、
プログラムの制御フローをよりコントロールできます。パターンは、以下を組み合わせることで構成されます:
- リテラル
- 分配された配列、enum、構造体、タプル
- 変数
- ワイルドカード
- プレースホルダー
これらの要素が取り組んでいるデータの形を説明し、それから値に対してマッチを行い、 プログラムに正しい値があって特定のコードを実行し続けられるかどうかを決定します。
パターンを使用するには、なんらかの値と比較します。パターンが値に合致したら、コードで値の部分を使用します。
コイン並び替えマシンの例のような第6章でパターンを使用したmatch式を思い出してください。
値がパターンの形に当てはまったら、名前のある部品を使用できます。当てはまらなければ、
パターンに紐づいたコードは実行されません。
この章は、パターンに関連するあらゆるものの参考文献です。パターンを使用するのが合法な箇所、 論駁可能と論駁不可能なパターンの違い、 目撃する可能性のある色々な種類のパターン記法を講義します。章の終わりまでに、 パターンを使用して多くの概念をはっきり表現する方法を知るでしょう。
パターンが使用されることのある箇所全部
Rustにおいて、パターンはいろんな箇所に出現し、そうと気づかないうちにたくさん使用してきました! この節は、パターンが合法な箇所全部を議論します。
matchアーム
第6章で議論したように、パターンをmatch式のアームで使います。正式には、match式はキーワードmatch、
マッチ対象の値、パターンとそのアームのパターンに値が合致したら実行される式からなる1つ以上のマッチアームとして定義されます。
以下のように:
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
match式の必須事項の1つは、match式の値の可能性全てが考慮されなければならないという意味で網羅的である必要があることです。
全可能性をカバーしていると保証する1つの手段は、最後のアームに包括的なパターンを入れることです:
例えば、どんな値にも合致する変数名は失敗することがあり得ないので、故に残りの全ケースをカバーできます。
_という特定のパターンは何にでもマッチしますが、変数には束縛されないので、よく最後のマッチアームに使用されます。
例えば、_パターンは、指定されていないあらゆる値を無視したい時に有用です。
_パターンについて詳しくは、この章の後ほど、「パターンで値を無視する」節で講義します。
条件分岐if let式
第6章で主にif let式を1つの場合にしか合致しないmatchと同様のものを書く省略法として使用する方法を議論しました。
オプションとして、if letにはif letのパターンが合致しない時に走るコードを含む対応するelseも用意できます。
リスト18-1は、if let、else if、else if let式を混ぜてマッチさせることもできることを示しています。
そうすると、パターンと1つの値しか比較することを表現できないmatch式よりも柔軟性が高くなります。
また、一連のif let、else if、else if letアームの条件は、お互いに関連している必要はありません。
リスト18-1のコードは、背景色が何になるべきかを決定するいくつかの条件を連なって確認するところを示しています。 この例では、実際のプログラムではユーザ入力を受け付ける可能性のある変数をハードコードされた値で生成しています。
ファイル名: src/main.rs
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, {}, as the background", color); } 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"); } }
リスト18-1: if let、else if、else if let、elseを混ぜる
ユーザがお気に入りの色を指定したら、その色が背景色になります。今日が火曜日なら、背景色は緑です。 ユーザが年齢を文字列で指定し、数値として解析することができたら、背景色は、その数値の値によって紫かオレンジになります。 どの条件も適用できなければ、背景色は青になります:
この条件分岐構造により、複雑な要件をサポートさせてくれます。ここにあるハードコードされた値では、
この例はUsing purple as the background colorと出力するでしょう。
matchアームのようにif letもシャドーイングされた変数を導入できることがわかります:
if let Ok(age) = ageの行は、Ok列挙子の中の値を含むシャドーイングされた新しい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ループを走らせます。
リスト18-2の例は、ベクタをスタックとして使用するwhile letループを示し、
ベクタの値をプッシュしたのとは逆順に出力します:
#![allow(unused)] fn main() { let mut stack = Vec::new(); stack.push(1); stack.push(2); stack.push(3); while let Some(top) = stack.pop() { println!("{}", top); } }
リスト18-2: while letループを使ってstack.pop()がSomeを返す限り値を出力する
この例は、3, 2, そして1と出力します。popメソッドはベクタの最後の要素を取り出してSome(value)を返します。
ベクタが空なら、popはNoneを返します。whileループはpopがSomeを返す限り、ブロックのコードを実行し続けます。
popがNoneを返すと、ループは停止します。while letを使用してスタックから全ての要素を取り出せるのです。
forループ
第3章で、Rustコードにおいては、forループが最もありふれたループ構造だと述べましたが、
forが取るパターンについてはまだ議論していませんでした。forループにおいて、
直接キーワードforに続く値がパターンなので、for x in yでは、xがパターンになります。
リスト18-3はforループでパターンを使用してforループの一部としてタプルを分配あるいは、分解する方法をデモしています。
#![allow(unused)] fn main() { let v = vec!['a', 'b', 'c']; for (index, value) in v.iter().enumerate() { println!("{} is at index {}", value, index); } }
リスト18-3: forループでパターンを使用してタプルを分配する
リスト18-3のコードは、以下のように出力するでしょう:
a is at index 0
b is at index 1
c is at index 2
enumerateメソッドを使用してイテレータを改造し、値とその値のイテレータでの添え字をタプルに配置して生成しています。
enumerateの最初の呼び出しは、タプル(0, 'a')を生成します。この値がパターン(index, value)とマッチさせられると、
indexは0、valueは'a'になり、出力の最初の行を出力するのです。
let文
この章に先駆けて、matchとif letでパターンを使用することだけ明示的に議論してきましたが、
実はlet文を含む他の箇所でもパターンを使用してきたのです。例として、このletでの率直な変数代入を考えてください:
#![allow(unused)] fn main() { let x = 5; }
この本を通してこのようなletを何百回も使用してきて、お気付きではなかったかもしれませんが、
パターンを使用していたのです!より正式には、let文はこんな見た目をしています:
let PATTERN = EXPRESSION;
let x = 5;のような変数名がPATTERNスロットにある文で、変数名は、ただ特に単純な形態のパターンなのです。
Rustは式をパターンと比較し、見つかったあらゆる名前を代入します。故に、let x = 5;の例では、
xは「ここでマッチしたものを変数xに束縛する」ことを意味するパターンです。
名前xがパターンの全容なので、このパターンは実質的に「値が何であれ、全てを変数xに束縛しろ」を意味します。
letのパターンマッチングの観点をよりはっきり確認するためにリスト18-4を考えてください。
これはletでパターンを使用し、タプルを分配します。
#![allow(unused)] fn main() { let (x, y, z) = (1, 2, 3); }
リスト18-4: パターンを使用してタプルを分配し、3つの変数を一度に生成する
ここでタプルに対してパターンをマッチさせています。Rustは値(1, 2, 3)をパターン(x, y, z)と比較し、
値がパターンに合致すると確認するので、1をxに、2をyに、3をzに束縛します。
このタプルパターンを個別の3つの変数パターンが内部にネストされていると考えることもできます。
パターンの要素数がタプルの要素数と一致しない場合、全体の型が一致せず、コンパイルエラーになるでしょう。 例えば、リスト18-5は、3要素のタプルを2つの変数に分配しようとしているところを表示していて、動きません。
let (x, y) = (1, 2, 3);
リスト18-5: 変数がタプルの要素数と一致しないパターンを間違って構成する
このコードのコンパイルを試みると、このような型エラーに落ち着きます:
error[E0308]: mismatched types
--> src/main.rs:2:9
|
2 | let (x, y) = (1, 2, 3);
| ^^^^^^ expected a tuple with 3 elements, found one with 2 elements
| (3要素のタプルを予期したのに、2要素のタプルが見つかりました)
|
= note: expected type `({integer}, {integer}, {integer})`
found type `(_, _)`
タプルの値のうち1つ以上を無視したかったら、「パターンで値を無視する」節で見かけるように、
_か..を使用できるでしょう。パターンに変数が多すぎるというのが問題なら、変数の数がタプルの要素数と一致するように変数を減らすことで、
型を一致させることが解決策です。
関数の引数
関数の引数もパターンにできます。リスト18-6のコードは、型i32のxという引数1つを取るfooという関数を宣言していますが、
これまでに馴染み深くなっているはずです。
#![allow(unused)] fn main() { fn foo(x: i32) { // コードがここに来る // code goes here } }
リスト18-6: 関数シグニチャが引数にパターンを使用している
xの部分がパターンです!letのように、関数の引数でパターンにタプルを合致させられるでしょう。
リスト18-7では、タプルを関数に渡したのでその中の値を分離しています。
ファイル名: src/main.rs
fn print_coordinates(&(x, y): &(i32, i32)) { // 現在の位置: ({}, {}) println!("Current location: ({}, {})", x, y); } fn main() { let point = (3, 5); print_coordinates(&point); }
リスト18-7: タプルを分配する引数を伴う関数
このコードは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式は、定義により失敗する可能性を処理することを意図したものなので、
論駁可能なパターンのみを受け付けます: 条件式の機能は、成功か失敗によって異なる振る舞いをする能力にあるのです。
一般的に、論駁可能と論駁不可能なパターンの差異について心配しなくてもいいはずです; しかしながら、 エラーメッセージで見かけた際に対応できるように、論駁可能性の概念に確かに慣れておく必要があります。 そのような場合には、コードの意図した振る舞いに応じて、パターンかパターンを使用している構文を変える必要があるでしょう。
コンパイラが論駁不可能なパターンを必要とする箇所で論駁可能なパターンを使用しようとしたら、何が起きるかとその逆の例を見ましょう。
リスト18-8はlet文を示していますが、パターンにはSome(x)と指定し、論駁可能なパターンです。
ご想像通りかもしれませんが、このコードはコンパイルできません。
let Some(x) = some_option_value;
リスト18-8: letで論駁可能なパターンを使用しようとする
some_option_valueがNone値だったなら、パターンSome(x)に合致しないことになり、パターンが論駁可能であることを意味します。
ですが、let文は論駁不可能なパターンしか受け付けられません。None値に対してコードができる合法なことは何もないからです。
コンパイル時にコンパイラは、論駁不可能なパターンが必要な箇所に論駁可能なパターンを使用しようとしたと文句を言うでしょう:
error[E0005]: refutable pattern in local binding: `None` not covered
(エラー: ローカル束縛に論駁可能なパターン: `None`がカバーされていません)
-->
|
3 | let Some(x) = some_option_value;
| ^^^^^^^ pattern `None` not covered
パターンSome(x)で全ての合法な値をカバーしなかった(できませんでした!)ので、
コンパイラは当然、コンパイルエラーを生成します。
論駁不可能なパターンが必要な箇所に論駁可能なパターンがある問題を修正するには、パターンを使用するコードを変えればいいのです:
letの代わりにif letを使用できます。そして、パターンが合致しなかったら、コードは合法に継続する手段を残して、
波括弧内のコードを飛ばすだけでしょう。リスト18-9は、リスト18-8のコードの修正方法を示しています。
#![allow(unused)] fn main() { let some_option_value: Option<i32> = None; if let Some(x) = some_option_value { println!("{}", x); } }
リスト18-9: letではなく、if letと論駁可能なパターンを含むブロックを使用する
コードに逃げ道を与えました!このコードは完全に合法ですが、エラーを受け取らないで論駁不可能なパターンを使用することはできないことを意味します。
リスト18-10のように、xのような常にマッチするパターンをif letに与えたら、コンパイルできないでしょう。
if let x = 5 {
println!("{}", x);
};
リスト18-10: if letで論駁不可能なパターンを使用してみる
コンパイラは、論駁不可能なパターンとif letを使用するなんて道理が通らないと文句を言います:
error[E0162]: irrefutable if-let pattern
(エラー: 論駁不可能なif-letパターン)
--> <anon>:2:8
|
2 | if let x = 5 {
| ^ irrefutable pattern
このため、マッチアームは、論駁不可能なパターンで残りのあらゆる値に合致すべき最後のアームを除いて、
論駁可能なパターンを使用しなければなりません。コンパイラは、たった1つしかアームのないmatchで論駁不可能なパターンを使用させてくれますが、
この記法は特別有用なわけではなく、より単純なlet文に置き換えることもできるでしょう。
今やパターンを使用すべき箇所と論駁可能と論駁不可能なパターンの違いを知ったので、 パターンを生成するために使用できる全ての記法を講義しましょう。
パターン記法
本全体で、多くの種類のパターンの例を見かけてきました。この節では、パターンで合法な記法全てを集め、 それぞれを使用したくなる可能性がある理由について議論します。
リテラルにマッチする
第6章で目撃したように、パターンを直接リテラルに合致させられます。以下のコードが例を挙げています:
#![allow(unused)] fn main() { let x = 1; match x { 1 => println!("one"), // 1 2 => println!("two"), // 2 3 => println!("three"), // 3 _ => println!("anything"), // なんでも } }
このコードは、xの値が1なので、oneを出力します。この記法は、コードが特定の具体的な値を得た時に行動を起こしてほしい時に有用です。
名前付き変数にマッチする
名前付き変数はどんな値にも合致する論駁不可能なパターンであり、この本の中で何度も使用してきました。
ですが、名前付き変数をmatch式で使うと、厄介な問題があります。matchは新しいスコープを開始するので、
match式内のパターンの一部として宣言された変数は、あらゆる変数同様にmatch構文外部の同じ名前の変数を覆い隠します。
リスト18-11で、値Some(5)のxという変数と値10の変数yを宣言しています。それから値xに対してmatch式を生成します。
マッチアームのパターンと最後のprintln!を見て、このコードを実行したり、先まで読み進める前にこのコードが何を出力するか推測してみてください。
ファイル名: src/main.rs
fn main() { let x = Some(5); let y = 10; match x { // 50だったよ Some(50) => println!("Got 50"), // マッチしたよ Some(y) => println!("Matched, y = {:?}", y), // 既定のケース _ => println!("Default case, x = {:?}", x), } // 最後にはx = {}, y = {} println!("at the end: x = {:?}, y = {:?}", x, y); }
リスト18-11: シャドーイングされた変数yを導入するアームのあるmatch式
match式を実行した時に起こることを見ていきましょう。最初のマッチアームのパターンは、xの定義された値に合致しないので、
コードは継続します。
2番目のマッチアームのパターンは、Some値内部のあらゆる値に合致する新しいyという変数を導入します。
match式内の新しいスコープ内にいるので、これは新しいy変数であり、最初に値10で宣言したyではありません。
この新しいy束縛は、Some内のあらゆる値に合致し、xにあるものはこれです。故に、この新しいyは、
xの中身の値に束縛されます。その値は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を生成します。
シャドーイングされた変数を導入するのではなく、外側のxとyの値を比較するmatch式を生成するには、
代わりにマッチガード条件式を使用する必要があるでしょう。マッチガードについては、後ほど、
「マッチガードで追加の条件式」節で語ります。
複数のパターン
match式で|記法で複数のパターンに合致させることができ、これはorを意味します。例えば、以下のコードはxの値をマッチアームに合致させ、
最初のマッチアームにはor選択肢があり、xの値がそのアームのどちらかの値に合致したら、そのアームのコードが走ることを意味します:
#![allow(unused)] fn main() { let x = 1; match x { // 1か2 1 | 2 => println!("one or two"), // 3 3 => println!("three"), // なんでも _ => println!("anything"), } }
このコードは、one or twoを出力します。
..=で値の範囲に合致させる
..=記法により、限度値を含む値の範囲にマッチさせることができます。以下のコードでは、
パターンが範囲内のどれかの値に合致すると、そのアームが実行されます:
#![allow(unused)] fn main() { let x = 5; match x { // 1から5まで 1..=5 => println!("one through five"), // それ以外 _ => println!("something else"), } }
xが1、2、3、4か5なら、最初のアームが合致します。この記法は、|演算子を使用して同じ考えを表現するより便利です;
1..=5ではなく、|を使用したら、1 | 2 | 3 | 4 | 5と指定しなければならないでしょう。
範囲を指定する方が遥かに短いのです。特に1から1000までの値と合致させたいとかなら!
範囲は、数値かchar値でのみ許可されます。コンパイラがコンパイル時に範囲が空でないことを確認しているからです。
範囲が空かそうでないかコンパイラにわかる唯一の型がcharか数値なのです。
こちらは、char値の範囲を使用する例です:
#![allow(unused)] fn main() { let x = 'c'; match x { // ASCII文字前半 'a'..='j' => println!("early ASCII letter"), // ASCII文字後半 'k'..='z' => println!("late ASCII letter"), // それ以外 _ => println!("something else"), } }
コンパイラには'c'が最初のパターンの範囲にあることがわかり、early ASCII letterと出力されます。
分配して値を分解する
また、パターンを使用して構造体、enum、タプル、参照を分配し、これらの値の異なる部分を使用することもできます。 各値を見ていきましょう。
構造体を分配する
リスト18-12は、let文でパターンを使用して分解できる2つのフィールドxとyのあるPoint構造体を示しています。
ファイル名: src/main.rs
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); }
リスト18-12: 構造体のフィールドを個別の変数に分配する
このコードは、p変数のxとyフィールドの値に合致する変数aとbを生成します。この例は、
パターンの変数の名前は、構造体のフィールド名と合致する必要はないことを示しています。しかし、
変数名をフィールド名と一致させてどの変数がどのフィールド由来のものなのか覚えやすくしたくなることは一般的なことです。
変数名をフィールドに一致させることは一般的であり、let Point{ x: x, y: y } = p;と書くことは多くの重複を含むので、
構造体のフィールドと一致するパターンには省略法があります: 構造体のフィールドの名前を列挙するだけで、
パターンから生成される変数は同じ名前になるのです。リスト18-13は、リスト18-12と同じ振る舞いをするコードを表示していますが、
letパターンで生成される変数はaとbではなく、xとyです。
ファイル名: src/main.rs
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); }
リスト18-13: 構造体フィールド省略法で構造体のフィールドを分配する
このコードは、p変数のxとyフィールドに一致する変数xとyを生成します。
結果は、変数xとyがp構造体の値を含むというものです。
また、全フィールドに対して変数を生成するのではなく、リテラル値を構造体パターンの一部にして分配することもできます。 そうすることで他のフィールドは分配して変数を生成しつつ、一部のフィールドは特定の値と一致するか確認できます。
リスト18-14は、Point値を3つの場合に区別するmatch式を表示しています: x軸上の点(y = 0ならそうなる)、
y軸上の点(x = 0)、あるいはどちらでもありません。
ファイル名: src/main.rs
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; match p { // x軸上の{} Point { x, y: 0 } => println!("On the x axis at {}", x), // y軸上の{} Point { x: 0, y } => println!("On the y axis at {}", y), // どちらの軸上でもない: ({}, {}) Point { x, y } => println!("On neither axis: ({}, {})", x, y), } }
リスト18-14: 分配とリテラル値との一致を1つのパターンで
最初のアームは、yフィールドの値がリテラル0と一致するならマッチすると指定することで、x軸上にあるどんな点とも一致します。
このパターンはそれでも、このアームのコードで使用できるx変数を生成します。
同様に、2番目のアームは、xフィールドが0ならマッチすると指定することでy軸上のどんな点とも一致し、
yフィールドの値には変数yを生成します。3番目のアームは何もリテラルを指定しないので、
それ以外のあらゆるPointに合致し、xとyフィールド両方に変数を生成します。
この例で、値pは0を含むxの力で2番目のアームに一致するので、このコードはOn the y axis at 7と出力します。
enumを分配する
例えば、第6章のリスト6-5でOption<i32>を分配するなどこの本の前半でenumを分配しました。
明示的に触れなかった詳細の1つは、enumを分配するパターンは、enum内に格納されているデータが定義されている手段に対応すべきということです。
例として、リスト18-15では、リスト6-2からMessage enumを使用し、内部の値それぞれを分配するパターンを伴うmatchを書いています。
ファイル名: src/main.rs
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 => { // Quit列挙子には分配すべきデータがない println!("The Quit variant has no data to destructure.") }, Message::Move { x, y } => { println!( // x方向に{}、y方向に{}だけ動く "Move in the x direction {} and in the y direction {}", x, y ); } // テキストメッセージ: {} Message::Write(text) => println!("Text message: {}", text), Message::ChangeColor(r, g, b) => { println!( // 色を赤{}, 緑{}, 青{}に変更 "Change the color to red {}, green {}, and blue {}", r, g, b ) } } }
リスト18-15: 異なる種類の値を保持するenumの列挙子を分配する
このコードは、Change the color to red 0, green 160, blue 255と出力します。
試しにmsgの値を変更して、他のアームのコードが走るところを確認してください。
Message::Quitのようなデータのないenum列挙子については、それ以上値を分配することができません。
リテラルMessage::Quit値にマッチするだけで、変数はそのパターンに存在しません。
Message::Moveのような構造体に似たenumの列挙子については、構造体と一致させるために指定するパターンと似たパターンを使用できます。
列挙子の名前の後に波括弧を配置し、それから変数とともにフィールドを列挙するので、部品を分解してこのアームのコードで使用します。
ここでは、リスト18-13のように省略形態を使用しています。
1要素タプルを保持するMessage::Writeや、3要素タプルを保持するMessage::ChangeColorのようなタプルに似たenumの列挙子について、
パターンは、タプルと一致させるために指定するパターンと類似しています。パターンの変数の数は、
マッチ対象の列挙子の要素数と一致しなければなりません。
参照を分配する
パターンとマッチさせている値に参照が含まれる場合、値から参照を分配する必要があり、
パターンに&を指定することでそうすることができます。そうすることで参照を保持する変数を得るのではなく、
参照が指している値を保持する変数が得られます。このテクニックは、参照を走査するイテレータがあるクロージャで特に役に立ちますが、
そのクロージャで参照ではなく、値を使用したいです。
リスト18-16の例は、ベクタのPointインスタンスへの参照を走査し、xとy値に簡単に計算を行えるように、
参照と構造体を分配します。
#![allow(unused)] fn main() { struct Point { x: i32, y: i32, } let points = vec![ Point { x: 0, y: 0 }, Point { x: 1, y: 5 }, Point { x: 10, y: -3 }, ]; let sum_of_squares: i32 = points .iter() .map(|&Point { x, y }| x * x + y * y) .sum(); }
リスト18-16: 構造体への参照を構造体のフィールド値に分配する
このコードは、値135を保持する変数sum_of_squaresを返してきて、これは、x値とy値を2乗し、足し合わせ、
pointsベクタのPointそれぞれの結果を足して1つの数値にした結果です。
&Point { x, y }に&が含まれていなかったら、型不一致エラーが発生していたでしょう。
iterはそうすると、実際の値ではなく、ベクタの要素への参照を走査するからです。そのエラーはこんな見た目でしょう:
error[E0308]: mismatched types
-->
|
14 | .map(|Point { x, y }| x * x + y * y)
| ^^^^^^^^^^^^ expected &Point, found struct `Point`
|
= note: expected type `&Point`
found type `Point`
このエラーは、コンパイラがクロージャに&Pointと一致することを期待しているのに、
Pointへの参照ではなく、Point値に直接一致させようとしたことを示唆しています。
構造体とタプルを分配する
分配パターンをさらに複雑な方法で混ぜてマッチさせ、ネストすることができます。以下の例は、 構造体とタプルをタプルにネストし、全ての基本的な値を取り出している複雑な分配を表示しています:
#![allow(unused)] fn main() { struct Point { x: i32, y: i32, } let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 }); }
このコードは、複雑な型を構成する部品に分配させてくれるので、興味のある値を個別に使用できます。
パターンで分配することは、構造体の各フィールドからの値のように、複数の値をお互いに区別して使用する便利な方法です。
パターンの値を無視する
matchの最後のアームのように、パターンの値を無視して実際には何もしないけれども、
残りの全ての値の可能性を考慮する包括的なものを得ることは、時として有用であると認識しましたね。
値全体やパターンの一部の値を無視する方法はいくつかあります: _パターンを使用すること(もう見かけました)、
他のパターン内で_パターンを使用すること、アンダースコアで始まる名前を使用すること、..を使用して値の残りの部分を無視することです。
これらのパターンそれぞれを使用する方法と理由を探究しましょう。
_で値全体を無視する
どんな値にも一致するけれども、値を束縛しないワイルドカードパターンとしてアンダースコア、_を使用してきました。
アンダースコア、_パターンは特にmatch式の最後のアームとして役に立ちますが、
関数の引数も含めてあらゆるパターンで使えます。リスト18-17に示したようにですね。
ファイル名: src/main.rs
fn foo(_: i32, y: i32) { // このコードは、y引数を使うだけです: {} println!("This code only uses the y parameter: {}", y); } fn main() { foo(3, 4); }
リスト18-17: 関数シグニチャで_を使用する
このコードは、最初の引数として渡された値3を完全に無視し、This code only uses the y parameter: 4と出力します。
特定の関数の引数が最早必要ないほとんどの場合、未使用の引数が含まれないようにシグニチャを変更するでしょう。 関数の引数を無視することが特に有用なケースもあり、例えば、トレイトを実装する際、 特定の型シグニチャが必要だけれども、自分の実装の関数本体では引数の1つが必要ない時などです。 そうすれば、代わりに名前を使った場合のようには、未使用関数引数についてコンパイラが警告することはないでしょう。
ネストされた_で値の一部を無視する
また、他のパターンの内部で_を使用して、値の一部だけを無視することもでき、例えば、
値の一部だけを確認したいけれども、走らせたい対応するコードでは他の部分を使用することがない時などです。
リスト18-18は、設定の値を管理する責任を負ったコードを示しています。業務要件は、
ユーザが既存の設定の変更を上書きすることはできないべきだけれども、設定を解除し、
現在設定がされていなければ設定に値を与えられるというものです。
#![allow(unused)] 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); }
リスト18-18: Some内の値を使用する必要がない時にSome列挙子と合致するパターンでアンダースコアを使用する
このコードは、Can't overwrite an existing customized value、そしてsetting is Some(5)と出力するでしょう。
最初のマッチアームで、どちらのSome列挙子内部の値にも合致させたり、使用する必要はありませんが、
setting_valueとnew_setting_valueがSome列挙子の場合を確かに確認する必要があります。
その場合、何故setting_valueを変更しないかを出力し、変更しません。
2番目のアームの_パターンで表現される他のあらゆる場合(setting_valueとnew_setting_valueどちらかがNoneなら)には、
new_setting_valueにsetting_valueになってほしいです。
また、1つのパターンの複数箇所でアンダースコアを使用して特定の値を無視することもできます。 リスト18-19は、5要素のタプルで2番目と4番目の値を無視する例です。
#![allow(unused)] fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, _, third, _, fifth) => { // 何らかの数値: {}, {}, {} println!("Some numbers: {}, {}, {}", first, third, fifth) }, } }
リスト18-19: タプルの複数の部分を無視する
このコードは、Some numbers: 2, 8, 32と出力し、値4と16は無視されます。
名前を_で始めて未使用の変数を無視する
変数を作っているのにどこでも使用していなければ、バグかもしれないのでコンパイラは通常、警告を発します。 しかし時として、まだ使用しない変数を作るのが有用なこともあります。プロトタイプを開発していたり、 プロジェクトを始めた直後だったりなどです。このような場面では、変数名をアンダースコアで始めることで、 コンパイラに未使用変数について警告しないよう指示することができます。リスト18-20で2つの未使用変数を生成していますが、 このコードを実行すると、そのうちの1つにしか警告が出ないはずです。
ファイル名: src/main.rs
fn main() { let _x = 5; let y = 10; }
リスト18-20: アンダースコアで変数名を始めて未使用変数警告が出るのを回避する
ここで、変数yを使用していないことに対して警告が出ていますが、アンダースコアが接頭辞になっている変数には、
使用していないという警告が出ていません。
_だけを使うのとアンダースコアで始まる名前を使うことには微妙な違いがあることに注意してください。
_x記法はそれでも、値を変数に束縛する一方で、_は全く束縛しません。この差異が問題になる場合を示すために、
リスト18-21はエラーを提示するでしょう。
// こんにちは!
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
// 文字列が見つかりました
println!("found a string");
}
println!("{:?}", s);
リスト18-21: それでも、アンダースコアで始まる未使用の変数は値を束縛し、値の所有権を奪う可能性がある
それでもs値は_sにムーブされ、再度sを使用できなくするので、エラーを受け取るでしょう。ですが、
アンダースコアを単独で使用すれば、値を束縛することは全くありません。
sが_にムーブされないので、リスト18-22はエラーなくコンパイルできます。
#![allow(unused)] fn main() { let s = Some(String::from("Hello!")); if let Some(_) = s { println!("found a string"); } println!("{:?}", s); }
リスト18-22: アンダースコアを使用すると、値を束縛しない
このコードは、sを何にも束縛しないので、ただ単に上手く動きます。つまり、ムーブされないのです。
..で値の残りの部分を無視する
多くの部分がある値では、..記法を使用していくつかの部分だけを使用して残りを無視し、
無視する値それぞれにアンダースコアを列挙する必要性を回避できます。..パターンは、
パターンの残りで明示的にマッチさせていない値のどんな部分も無視します。リスト18-23では、
3次元空間で座標を保持するPoint構造体があります。match式でx座標のみ処理し、
yとzフィールドの値は無視したいです。
#![allow(unused)] 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), } }
リスト18-23: ..でx以外のPointのフィールド全てを無視する
x値を列挙し、それから..パターンを含んでいるだけです。これは、y: _やz: _と列挙しなければいけないのに比べて、
手っ取り早いです。特に1つや2つのフィールドのみが関連する場面で多くのフィールドがある構造体に取り掛かっている時には。
..記法は、必要な数だけ値に展開されます。リスト18-24は、タプルで..を使用する方法を表示しています。
ファイル名: src/main.rs
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, .., last) => { println!("Some numbers: {}, {}", first, last); }, } }
リスト18-24: タプルの最初と最後の値にだけ合致し、他の値を無視する
このコードにおいて、最初と最後の値はfirstとlastに合致します。..は、
途中のもの全部に合致し、無視します。
しかしながら、..を使うのは明確でなければなりません。どの値がマッチしてどの値が無視されるべきかが不明瞭なら、
コンパイラはエラーを出します。リスト18-25は、..を曖昧に使用する例なので、コンパイルできません。
ファイル名: src/main.rs
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {}", second)
},
}
}
リスト18-25: ..を曖昧に使用しようとする試み
この例をコンパイルすると、こんなエラーが出ます:
error: `..` can only be used once per tuple or tuple struct pattern
(エラー: `..`は、タプルやタプル構造体パターン1つにつき、1回しか使用できません)
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| ^^
コンパイラが、secondの値に合致する前にタプルの幾つの値を無視し、それからそれによってさらに幾つの値を無視するかを決めることは不可能です。
このコードは、2を無視し、secondに4を束縛し、それから8、16、32を無視したり、
2と4を無視してsecondに8を束縛し、それから16と32を無視するなどを意味することもあるでしょう。
変数名のsecondは、コンパイラにとってなんの特別な意味もなく、このように2箇所で..を使うのは曖昧なので、
コンパイルエラーになります。
refとref mutでパターンに参照を生成する
refを使用して値の所有権がパターンの変数にムーブされないように、参照を生成することに目を向けましょう。
通常、パターンにマッチさせると、パターンで導入された変数は値に束縛されます。Rustの所有権規則は、
その値がmatchなどパターンを使用しているあらゆる場所にムーブされることを意味します。
リスト18-26は、変数があるパターンとそれからmatchの後に値全体をprintln!文で後ほど使用するmatchの例を示しています。
このコードはコンパイルに失敗します。robot_name値の一部の所有権が、
最初のmatchアームのパターンのname変数に移るからです。
let robot_name = Some(String::from("Bors"));
match robot_name {
// 名前が見つかりました: {}
Some(name) => println!("Found a name: {}", name),
None => (),
}
// robot_nameは: {:?}
println!("robot_name is: {:?}", robot_name);
リスト18-26: matchアームパターンで変数を生成すると、値の所有権が奪われる
robot_nameの一部の所有権がnameにムーブされたので、robot_nameに最早所有権がないために、
matchの後にprintln!で最早robot_nameを使用することは叶いません。
このコードを修正するために、Some(name)パターンに所有権を奪わせるのではなく、
robot_nameのその部分を借用させたいです。パターンの外なら、値を借用する手段は、
&で参照を生成することだと既にご認識でしょうから、解決策はSome(name)をSome(&name)に変えることだとお考えかもしれませんね。
しかしながら、「分配して値を分解する」節で見かけたように、パターンにおける&記法は参照を生成せず、
値の既存の参照にマッチします。パターンにおいて&には既にその意味があるので、
&を使用してパターンで参照を生成することはできません。
その代わりに、パターンで参照を生成するには、リスト18-27のように、新しい変数の前にrefキーワードを使用します。
#![allow(unused)] fn main() { let robot_name = Some(String::from("Bors")); match robot_name { Some(ref name) => println!("Found a name: {}", name), None => (), } println!("robot_name is: {:?}", robot_name); }
リスト18-27: パターンの変数が値の所有権を奪わないように参照を生成する
robot_nameのSome列挙子の値がmatchにムーブされないので、この例はコンパイルできます;
matchはムーブするのではなく、robot_nameのデータへの参照を取っただけなのです。
パターンで合致した値を可変化できるように可変参照を生成するには、&mutの代わりにref mutを使用します。
理由は今度も、パターンにおいて、前者は既存の可変参照にマッチするためにあり、新しい参照を生成しないからです。
リスト18-28は、可変参照を生成するパターンの例です。
#![allow(unused)] fn main() { let mut robot_name = Some(String::from("Bors")); match robot_name { // 別の名前 Some(ref mut name) => *name = String::from("Another name"), None => (), } println!("robot_name is: {:?}", robot_name); }
リスト18-28: ref mutを使用して、パターンの一部として値への可変参照を生成する
この例はコンパイルが通り、robot_name is: Some("Another name")と出力するでしょう。
nameは可変参照なので、値を可変化するためにマッチアーム内で*演算子を使用して参照外しする必要があります。
マッチガードで追加の条件式
マッチガードは、matchアームのパターンの後に指定されるパターンマッチングとともに、
そのアームが選択されるのにマッチしなければならない追加のif条件です。マッチガードは、
1つのパターン単独でできるよりも複雑な考えを表現するのに役に立ちます。
この条件は、パターンで生成された変数を使用できます。リスト18-29は、
最初のアームにパターンSome(x)とif x < 5というマッチガードもあるmatchを示しています。
#![allow(unused)] fn main() { let num = Some(4); match num { // 5未満です: {} Some(x) if x < 5 => println!("less than five: {}", x), Some(x) => println!("{}", x), None => (), } }
リスト18-29: パターンにマッチガードを追記する
この例は、less than five: 4と出力します。numが最初のアームのパターンと比較されると、
Some(4)はSome(x)に一致するので、マッチします。そして、マッチガードがxの値が5未満か確認し、
そうなっているので、最初のアームが選択されます。
代わりにnumがSome(10)だったなら、最初のアームのマッチガードは偽になったでしょう。
10は5未満ではないからです。Rustはそうしたら2番目のアームに移動し、マッチするでしょう。
2番目のアームにはマッチガードがなく、それ故にあらゆるSome列挙子に一致するからです。
パターン内でif x < 5という条件を表現する方法はありませんので、マッチガードにより、
この論理を表現する能力が得られるのです。
リスト18-11において、マッチガードを使用すれば、パターンがシャドーイングする問題を解決できると述べました。
matchの外側の変数を使用するのではなく、match式のパターン内部では新しい変数が作られることを思い出してください。
その新しい変数は、外側の変数の値と比較することができないことを意味しました。リスト18-30は、
マッチガードを使ってこの問題を修正する方法を表示しています。
ファイル名: src/main.rs
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 = {:?}, y = {:?}", x, y); }
リスト18-30: マッチガードを使用して外側の変数と等しいか確認する
このコードは今度は、Default case, x = Some(5)と出力するでしょう。2番目のマッチアームのパターンは、
外側のyを覆い隠してしまう新しい変数yを導入せず、マッチガード内で外側のyを使用できることを意味します。
外側のyを覆い隠してしまうSome(y)としてパターンを指定するのではなく、Some(n)を指定しています。
これにより、何も覆い隠さない新しい変数nが生成されます。matchの外側にはn変数は存在しないからです。
マッチガードのif n == yはパターンではなく、故に新しい変数を導入しません。このyは、
新しいシャドーイングされたyではなく、外側のyであり、nとyを比較することで、
外側のyと同じ値を探すことができます。
また、マッチガードでor演算子の|を使用して複数のパターンを指定することもできます;
マッチガードの条件は全てのパターンに適用されます。リスト18-31は、
|を使用するパターンとマッチガードを組み合わせる優先度を示しています。この例で重要な部分は、
if yは6にしか適用されないように見えるのに、if yマッチガードが4、5、そして6に適用されることです。
#![allow(unused)] fn main() { let x = 4; let y = false; match x { // はい 4 | 5 | 6 if y => println!("yes"), // いいえ _ => println!("no"), } }
リスト18-31: 複数のパターンとマッチガードを組み合わせる
マッチの条件は、xの値が4、5、6に等しくかつyがtrueの場合だけにアームがマッチすると宣言しています。
このコードが走ると、最初のアームのパターンはxが4なので、合致しますが、マッチガードif yは偽なので、
最初のアームは選ばれません。コードは2番目のアームに移動して、これがマッチし、このプログラムはnoと出力します。
理由は、if条件が最後の値の6だけでなく、パターン全体4 | 5 | 6に適用されるからです。
言い換えると、パターンと関わるマッチガードの優先度は、以下のように振る舞います:
(4 | 5 | 6) if y => ...
以下のようにではありません:
4 | 5 | (6 if y) => ...
コードを実行後には、優先度の動作は明らかになります: マッチガードが|演算子で指定される値のリストの最後の値にしか適用されないなら、
アームはマッチし、プログラムはyesと出力したでしょう。
@束縛
at演算子(@)により、値を保持する変数を生成するのと同時にその値がパターンに一致するかを調べることができます。
リスト18-32は、Message::Helloのidフィールドが範囲3..=7にあるかを確かめたいという例です。
しかし、アームに紐づいたコードで使用できるように変数id_variableに値を束縛もしたいです。この変数をフィールドと同じ、
idと名付けることもできますが、この例では異なる名前にします。
#![allow(unused)] fn main() { enum Message { Hello { id: i32 }, } let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id_variable @ 3..=7 } => { // 範囲内のidが見つかりました: {} println!("Found an id in range: {}", id_variable) }, Message::Hello { id: 10..=12 } => { // 別の範囲内のidが見つかりました println!("Found an id in another range") }, Message::Hello { id } => { // それ以外のidが見つかりました println!("Found some other id: {}", id) }, } }
@を使用してテストしつつ、パターンの値に束縛する
この例は、Found an id in range: 5と出力します。範囲3..=7の前にid_variable @と指定することで、
値が範囲パターンに一致することを確認しつつ、範囲にマッチしたどんな値も捕捉しています。
パターンで範囲しか指定していない2番目のアームでは、アームに紐づいたコードにidフィールドの実際の値を含む変数はありません。
idフィールドの値は10、11、12だった可能性があるでしょうが、そのパターンに来るコードは、
どれなのかわかりません。パターンのコードはidフィールドの値を使用することは叶いません。
idの値を変数に保存していないからです。
範囲なしに変数を指定している最後のアームでは、確かにアームのコードで使用可能な値がidという変数にあります。
理由は、構造体フィールド省略記法を使ったからです。しかし、このアームでidフィールドの値に対して、
最初の2つのアームのようには、確認を行っていません: どんな値でも、このパターンに一致するでしょう。
@を使用することで、値を検査しつつ、1つのパターン内で変数に保存させてくれるのです。
まとめ
Rustのパターンは、異なる種類のデータを区別するのに役立つという点でとても有用です。match式で使用されると、
コンパイラはパターンが全ての可能性を網羅しているか保証し、そうでなければプログラムはコンパイルできません。
let文や関数の引数のパターンは、その構文をより有用にし、値を分配して小さな部品にすると同時に変数に代入できるようにしてくれます。
単純だったり複雑だったりするパターンを生成してニーズに合わせることができます。
次の本書の末尾から2番目の章では、Rustの多彩な機能の高度な視点に目を向けます。
高度な機能
今までに、Rustプログラミング言語の最もよく使われる部分を学んできました。第20章でもう1つ別のプロジェクトを行う前に、 時折遭遇する言語の側面をいくつか見ましょう。この章は、Rustを使用する際に知らないことに遭遇した時に参考にすることができます。 この章で使用することを学ぶ機能は、かなり限定的な場面でしか役に立ちません。あまり頻繁には手を伸ばすことがない可能性はありますが、 Rustが提供しなければならない機能全ての概要を確かに把握してもらいたいのです。
この章で講義するのは:
- Unsafe Rust: Rustの保証の一部を抜けてその保証を手動で保持する責任を負う方法
- 高度なトレイト: 関連型、デフォルト型引数、フルパス記法、スーパートレイト、トレイトに関連するニュータイプパターン
- 高度な型: ニュータイプパターンについてもっと、型エイリアス、never型、動的サイズ決定型
- 高度な関数とクロージャ: 関数ポインタとクロージャの返却
- マクロ: コンパイル時に、より多くのコードを定義するコードを定義する方法
皆さんのための何かがあるRustの機能の盛大な儀式です!さあ、飛び込みましょう!
Unsafe Rust
ここまでに議論してきたコードは全て、Rustのメモリ安全保証がコンパイル時に強制されていました。しかしながら、 Rustには、これらのメモリ安全保証を強制しない第2の言語が中に隠されています: それはunsafe Rustと呼ばれ、 普通のRustのように動きますが、おまけの強大な力を与えてくれます。
静的解析は原理的に保守的なので、unsafe Rustが存在します。コードが保証を保持しているかコンパイラが決定しようとする際、 なんらかの不正なプログラムを受け入れるよりも合法なプログラムを拒否したほうがいいのです。コードは大丈夫かもしれないけれど、 コンパイラにわかる範囲ではダメなのです!このような場合、unsafeコードを使用してコンパイラに「信じて!何をしているかわかってるよ」と教えられます。 欠点は、自らのリスクで使用することです: unsafeコードを誤って使用したら、 nullポインタ参照外しなどのメモリ非安全に起因する問題が起こることもあるのです。
Rustにunsafeな分身がある別の理由は、根本にあるコンピュータのハードウェアが本質的にunsafeだからです。 Rustがunsafeな処理を行わせてくれなかったら、特定の仕事を行えないでしょう。Rustは、低レベルなシステムプログラミングを許可する必要があります。 直接OSと相互作用したり、独自のOSを書くことさえもそうです。低レベルなシステムプログラミングに取り組むことは、 言語の目標の1つなのです。unsafe Rustでできることとその方法を探究しましょう。
unsafeの強大な力(superpower)
unsafe Rustに切り替えるには、unsafeキーワードを使用し、それからunsafeコードを保持する新しいブロックを開始してください。
safe Rustでは行えない4つの行動をunsafe Rustでは行え、これはunsafe superpowersと呼ばれます。
そのsuperpowerには、以下の能力が含まれています:
- 生ポインタを参照外しすること
- unsafeな関数やメソッドを呼ぶこと
- 可変で静的な変数にアクセスしたり変更すること
- unsafeなトレイトを実装すること
unsafeは、借用チェッカーや他のRustの安全性チェックを無効にしないことを理解するのは重要なことです:
unsafeコードで参照を使用しても、チェックはされます。unsafeキーワードにより、これら4つの機能にアクセスできるようになり、
その場合、コンパイラによってこれらのメモリ安全性は確認されないのです。unsafeブロック内でも、ある程度の安全性は得られます。
また、unsafeは、そのブロックが必ずしも危険だったり、絶対メモリ安全上の問題を抱えていることを意味するものではありません:
その意図は、プログラマとしてunsafeブロック内のコードがメモリに合法的にアクセスすることを保証することです。
人間は失敗をするもので、間違いも起きますが、これら4つのunsafeな処理をunsafeで注釈されたブロックに入れる必要があることで、
メモリ安全性に関するどんなエラーもunsafeブロック内にあるに違いないと知ります。unsafeブロックは小さくしてください;
メモリのバグを調査するときに感謝することになるでしょう。
unsafeなコードをできるだけ分離するために、unsafeなコードを安全な抽象の中に閉じ込め、安全なAPIを提供するのが最善です。
これについては、後ほどunsafeな関数とメソッドを調査する際に議論します。標準ライブラリの一部は、
検査されたunsafeコードの安全な抽象として実装されています。安全な抽象にunsafeなコードを包むことで、
unsafeが、あなたやあなたのユーザがunsafeコードで実装された機能を使いたがる可能性のある箇所全部に漏れ出ることを防ぎます。
安全な抽象を使用することは、安全だからです。
4つのunsafeなsuperpowerを順に見ていきましょう。unsafeなコードへの安全なインターフェイスを提供する一部の抽象化にも目を向けます。
生ポインタを参照外しする
第4章の「ダングリング参照」節で、コンパイラは、参照が常に有効であることを保証することに触れました。
unsafe Rustには参照に類似した生ポインタと呼ばれる2つの新しい型があります。参照同様、
生ポインタも不変や可変になり得て、それぞれ*const Tと*mut Tと表記されます。このアスタリスクは、参照外し演算子ではありません;
型名の一部です。生ポインタの文脈では、不変は、参照外し後に直接ポインタに代入できないことを意味します。
参照やスマートポインタと異なり、生ポインタは:
- 同じ場所への不変と可変なポインタや複数の可変なポインタが存在することで借用規則を無視できる
- 有効なメモリを指しているとは保証されない
- nullの可能性がある
- 自動的な片付けは実装されていない
これらの保証をコンパイラに強制させることから抜けることで、保証された安全性を諦めてパフォーマンスを向上させたり、 Rustの保証が適用されない他の言語やハードウェアとのインターフェイスの能力を得ることができます。
リスト19-1は、参照から不変と可変な生ポインタを生成する方法を示しています。
#![allow(unused)] fn main() { let mut num = 5; let r1 = &num as *const i32; let r2 = &mut num as *mut i32; }
リスト19-1: 参照から生ポインタを生成する
このコードにはunsafeキーワードを含めていないことに注意してください。safeコードで生ポインタを生成できます;
もうすぐわかるように、unsafeブロックの外では、生ポインタを参照外しできないだけなのです。
asを使って不変と可変な参照を対応する生ポインタの型にキャストして生ポインタを生成しました。
有効であることが保証される参照から直接生ポインタを生成したので、これらの特定の生ポインタは有効であることがわかりますが、
その前提をあらゆる生ポインタに敷くことはできません。
次に、有効であることが確信できない生ポインタを生成します。リスト19-2は、メモリの任意の箇所を指す生ポインタの生成法を示しています。 任意のメモリを使用しようとすることは未定義です: そのアドレスにデータがある可能性もあるし、ない可能性もあり、 コンパイラがコードを最適化してメモリアクセスがなくなる可能性もあるし、プログラムがセグメンテーションフォールトでエラーになる可能性もあります。 通常、このようなコードを書くいい理由はありませんが、可能ではあります。
#![allow(unused)] fn main() { let address = 0x012345usize; let r = address as *const i32; }
リスト19-2: 任意のメモリアドレスへの生ポインタを生成する
safeコードで生ポインタを生成できるけれども、生ポインタを参照外しして指しているデータを読むことはできないことを思い出してください。
リスト19-3では、unsafeブロックが必要になる参照外し演算子の*を生ポインタに使っています。
#![allow(unused)] fn main() { let mut num = 5; let r1 = &num as *const i32; let r2 = &mut num as *mut i32; unsafe { println!("r1 is: {}", *r1); println!("r2 is: {}", *r2); } }
リスト19-3: unsafeブロック内で生ポインタを参照外しする
ポインタの生成は害を及ぼしません; 問題が起こり得るのはポインタが指している値にアクセスしようとするときのみで、この際に無効な値を扱うことになる可能性があります。
また、リスト19-1とリスト19-3では、
numが格納されている同じメモリ上の場所を両方とも指す*const i32と*mut i32の生ポインタを生成したことに注目してください。
代わりにnumへの不変と可変な参照を生成しようとしたら、コードはコンパイルできなかったでしょう。
Rustの所有権規則により、不変参照と可変参照を同時に存在させられないからです。生ポインタなら、
同じ場所への可変なポインタと不変なポインタを生成でき、可変なポインタを通してデータを変更し、データ競合を引き起こす可能性があります。
気を付けてください!
これらの危険がありながら、一体何故生ポインタを使うのでしょうか?主なユースケースの1つは、次の節「unsafeな関数やメソッドを呼ぶ」で見るように、 Cコードとのインターフェイスです。別のユースケースは、借用チェッカーには理解できない安全な抽象を構成する時です。 unsafeな関数を導入し、それからunsafeコードを使用する安全な抽象の例に目を向けます。
unsafeな関数やメソッドを呼ぶ
unsafeブロックが必要になる2番目の処理は、unsafe関数の呼び出しです。unsafeな関数やメソッドも見た目は、
普通の関数やメソッドと全く同じですが、残りの定義の前に追加のunsafeがあります。この文脈でのunsafeキーワードは、
この関数を呼ぶ際に保持しておく必要のある要求が関数にあることを示唆します。コンパイラには、
この要求を満たしているか保証できないからです。unsafeブロックでunsafeな関数を呼び出すことで、
この関数のドキュメンテーションを読み、関数の契約を守っているという責任を取ると宣言します。
こちらは、本体で何もしないdangerousというunsafeな関数です:
#![allow(unused)] fn main() { unsafe fn dangerous() {} unsafe { dangerous(); } }
個別のunsafeブロックでdangerous関数を呼ばなければなりません。unsafeブロックなしでdangerousを呼ぼうとすれば、
エラーになるでしょう:
error[E0133]: call to unsafe function requires unsafe function or block
(エラー: unsafe関数の呼び出しには、unsafeな関数かブロックが必要です)
-->
|
4 | dangerous();
| ^^^^^^^^^^^ call to unsafe function
dangerousへの呼び出しの周りにunsafeブロックを挿入することで、コンパイラに関数のドキュメンテーションを読み、
適切に使用する方法を理解したことをアサートし、関数の契約を満たしていると実証しました。
unsafe関数の本体は、実効的にunsafeブロックになるので、unsafe関数内でunsafeな別の処理を行うのに、
別のunsafeブロックは必要ないのです。
unsafeコードに安全な抽象を行う
関数がunsafeなコードを含んでいるだけで関数全体をunsafeでマークする必要があることにはなりません。
事実、安全な関数でunsafeなコードをラップすることは一般的な抽象化です。例として、
なんらかのunsafeコードが必要になる標準ライブラリの関数split_at_mutを学び、その実装方法を探究しましょう。
この安全なメソッドは、可変なスライスに定義されています: スライスを1つ取り、引数で与えられた添え字でスライスを分割して2つにします。
リスト19-4は、split_at_mutの使用法を示しています。
#![allow(unused)] 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]); }
リスト19-4: 安全なsplit_at_mut関数を使用する
この関数をsafe Rustだけを使用して実装することはできません。試みは、リスト19-5のようになる可能性がありますが、コンパイルできません。
簡単のため、split_at_mutをメソッドではなく関数として実装し、ジェネリックな型Tではなく、i32のスライス用に実装します。
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
assert!(mid <= len);
(&mut slice[..mid],
&mut slice[mid..])
}
リスト19-5: safe Rustだけを使用したsplit_at_mutの未遂の実装
この関数はまず、スライスの全体の長さを得ます。それから引数で与えられた添え字が長さ以下であるかを確認してスライス内にあることをアサートします。 このアサートは、スライスを分割する添え字よりも大きい添え字を渡したら、その添え字を使用しようとする前に関数がパニックすることを意味します。
そして、2つの可変なスライスをタプルで返します: 1つは元のスライスの最初からmid添え字まで、
もう一方は、midからスライスの終わりまでです。
リスト19-5のコードのコンパイルを試みると、エラーになるでしょう。
error[E0499]: cannot borrow `*slice` as mutable more than once at a time
(エラー: 一度に2回以上、`*slice`を可変で借用できません)
-->
|
6 | (&mut slice[..mid],
| ----- first mutable borrow occurs here
7 | &mut slice[mid..])
| ^^^^^ second mutable borrow occurs here
8 | }
| - first borrow ends here
Rustの借用チェッカーには、スライスの異なる部分を借用していることが理解できないのです; 同じスライスから2回借用していることだけ知っています。2つのスライスが被らないので、 スライスの異なる部分を借用することは、根本的に大丈夫なのですが、コンパイラはこれを知れるほど賢くありません。 プログラマにはコードが大丈夫とわかるのに、コンパイラにはわからないのなら、unsafeコードに手を伸ばすタイミングです。
リスト19-6はunsafeブロック、生ポインタ、unsafe関数への呼び出しをしてsplit_at_mutの実装が動くようにする方法を示しています。
#![allow(unused)] fn main() { use std::slice; fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = slice.len(); let ptr = slice.as_mut_ptr(); assert!(mid <= len); unsafe { (slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.offset(mid as isize), len - mid)) } } }
リスト19-6: split_at_mut関数の実装でunsafeコードを使用する
第4章の「スライス型」節から、スライスはなんらかのデータへのポインタとスライスの長さであることを思い出してください。
lenメソッドを使用してスライスの長さを得て、as_mut_ptrメソッドを使用してスライスの生ポインタにアクセスしています。
この場合、i32値の可変スライスがあるので、as_mut_ptrは型*mut i32の生ポインタを返し、これを変数ptrに格納しました。
mid添え字がスライス内にあるかというアサートを残しています。そして、unsafeコードに到達します:
slice::from_raw_parts_mut関数は、生ポインタと長さを取り、スライスを生成します。この関数を使って、
ptrから始まり、midの長さのスライスを生成しています。それからptrにmidを引数としてoffsetメソッドを呼び出し、
midで始まる生ポインタを得て、そのポインタとmidの後の残りの要素数を長さとして使用してスライスを生成しています。
関数slice::from_raw_parts_mutは、unsafeです。何故なら、生ポインタを取り、このポインタが有効であることを信用しなければならないからです。
生ポインタのoffsetメソッドもunsafeです。オフセット位置もまた有効なポインタであることを信用しなければならないからです。
故に、slice::from_raw_parts_mutとoffsetを呼べるように、その呼び出しの周りにunsafeブロックを置かなければならなかったのです。
コードを眺めてmidがlen以下でなければならないとするアサートを追加することで、
unsafeブロック内で使用されている生ポインタが全てスライス内のデータへの有効なポインタであることがわかります。
これは、受け入れられ、適切なunsafeの使用法です。
できあがったsplit_at_mut関数をunsafeでマークする必要はなく、この関数をsafe Rustから呼び出せることに注意してください。
unsafeコードを安全に使用する関数の実装で、unsafeコードへの安全な抽象化を行いました。
この関数がアクセスするデータからの有効なポインタだけを生成するからです。
対照的に、リスト19-7のslice::from_raw_parts_mutの使用は、スライスが使用されるとクラッシュする可能性が高いでしょう。
このコードは任意のメモリアドレスを取り、10,000要素の長さのスライスを生成します:
#![allow(unused)] fn main() { use std::slice; let address = 0x012345usize; let r = address as *mut i32; let slice = unsafe { slice::from_raw_parts_mut(r, 10000) }; }
リスト19-7: 任意のメモリアドレスからスライスを生成する
この任意の場所のメモリは所有していなく、このコードが生成するスライスに有効なi32値が含まれる保証もありません。
sliceを有効なスライスであるかのように使用しようとすると、未定義動作に陥ります。
extern関数を使用して、外部のコードを呼び出す
時として、自分のRustコードが他の言語で書かれたコードと相互作用する必要が出てくる可能性があります。このために、
Rustにはexternというキーワードがあり、これは、
FFI(Foreign Function Interface: 外部関数インターフェイス)の生成と使用を容易にします。
FFIは、あるプログラミング言語に関数を定義させ、異なる(外部の)プログラミング言語にそれらの関数を呼び出すことを可能にする方法です
リスト19-8は、Cの標準ライブラリからabs関数を統合するセットアップ方法をデモしています。
externブロック内で宣言された関数は、常にRustコードから呼ぶにはunsafeになります。理由は、
他の言語では、Rustの規則や保証が強制されず、コンパイラもチェックできないので、
安全性を保証する責任はプログラマに降りかかるのです。
ファイル名: src/main.rs
extern "C" { fn abs(input: i32) -> i32; } fn main() { unsafe { // -3の絶対値は、Cによると{} println!("Absolute value of -3 according to C: {}", abs(-3)); } }
リスト19-8: 他の言語で定義されたextern関数を宣言し、呼び出す
extern "C"ブロック内で他の言語から呼び出した関数の名前とシグニチャを列挙します。"C"の部分は、
外部関数がどのABI(application binary interface: アプリケーション・バイナリ・インターフェイス)を使用しているか定義します:
ABIは関数の呼び出し方法をアセンブリレベルで定義します。"C"ABIは最も一般的でCプログラミング言語のABIに従っています。
他の言語からRustの関数を呼び出す
また、
externを使用して他の言語にRustの関数を呼ばせるインターフェイスを生成することもできます。externブロックの代わりに、externキーワードを追加し、fnキーワードの直前に使用するABIを指定します。 さらに、#[no_mangle]注釈を追加してRustコンパイラに関数名をマングルしないように指示する必要もあります。 マングルとは、コンパイラが関数に与えた名前を他のコンパイル過程の情報をより多く含むけれども、人間に読みにくい異なる名前にすることです。 全ての言語のコンパイラは、少々異なる方法でマングルを行うので、Rustの関数が他の言語で名前付けできるように、 Rustコンパイラの名前マングルをオフにしなければならないのです。以下の例では、共有ライブラリにコンパイルし、Cからリンクした後に
call_from_c関数をCコードからアクセスできるようにしています:#![allow(unused)] fn main() { #[no_mangle] pub extern "C" fn call_from_c() { // CからRust関数を呼び出したばかり! println!("Just called a Rust function from C!"); } }この
externの使用法では、unsafeは必要ありません。
可変で静的な変数にアクセスしたり、変更する
今までずっと、グローバル変数について語りませんでした。グローバル変数をRustは確かにサポートしていますが、 Rustの所有権規則で問題になることもあります。2つのスレッドが同じ可変なグローバル変数にアクセスしていたら、 データ競合を起こすこともあります。
Rustでは、グローバル変数は、static(静的)変数と呼ばれます。リスト19-9は、 値として文字列スライスのある静的変数の宣言例と使用を示しています。
ファイル名: src/main.rs
static HELLO_WORLD: &str = "Hello, world!"; fn main() { // 名前は: {} println!("name is: {}", HELLO_WORLD); }
リスト19-9: 不変で静的な変数を定義し、使用する
静的変数は、定数に似ています。定数については、第3章の「変数と定数の違い」節で議論しました。
静的変数の名前は慣習でSCREAMING_SNAKE_CASE(直訳: 叫ぶスネークケース)になり、変数の型を注釈しなければなりません。
この例では&'static strです。静的変数は、'staticライフタイムの参照のみ格納でき、
これは、Rustコンパイラがライフタイムを推量できることを意味します; 明示的に注釈する必要はありません。
不変で静的な変数にアクセスすることは安全です。
定数と不変で静的な変数は、類似して見える可能性がありますが、微妙な差異は、 静的変数の値は固定されたメモリアドレスになることです。値を使用すると、常に同じデータにアクセスします。 一方、定数は使用される度にデータを複製させることができます。
定数と静的変数の別の違いは、静的変数は可変にもなることです。可変で静的な変数にアクセスし変更することは、unsafeです。
リスト19-10は、COUNTERという可変で静的な変数を宣言し、アクセスし、変更する方法を表示しています。
ファイル名: src/main.rs
static mut COUNTER: u32 = 0; fn add_to_count(inc: u32) { unsafe { COUNTER += inc; } } fn main() { add_to_count(3); unsafe { println!("COUNTER: {}", COUNTER); } }
リスト19-10: 可変で静的な変数を読んだり、書き込むのはunsafeである
普通の変数同様、mutキーワードを使用して可変性を指定します。COUNTERを読み書きするコードはどれも、unsafeブロックになければなりません。
シングルスレッドなので、このコードは想定通り、コンパイルでき、COUNTER: 3と出力します。
複数のスレッドにCOUNTERにアクセスさせると、データ競合になる可能性が高いでしょう。
グローバルにアクセス可能な可変なデータがあると、データ競合がないことを保証するのは難しくなり、そのため、 Rustは可変で静的な変数をunsafeと考えるのです。可能なら、コンパイラが、データが異なるスレッドからアクセスされることが安全に行われているかを確認するように、 第16章で議論した並行性テクニックとスレッド安全なスマートポインタを使用するのが望ましいです。
unsafeなトレイトを実装する
unsafeでのみ動く最後の行動は、unsafeなトレイトを実装することです。少なくとも、1つのメソッドにコンパイラが確かめられないなんらかの不変条件があると、
トレイトはunsafeになります。traitの前にunsafeキーワードを追加し、トレイトの実装もunsafeでマークすることで、
トレイトがunsafeであると宣言できます。リスト19-11のようにですね。
#![allow(unused)] fn main() { unsafe trait Foo { // methods go here // メソッドがここに来る } unsafe impl Foo for i32 { // method implementations go here // メソッドの実装がここに来る } }
リスト19-11: unsafeなトレイトを定義して実装する
unsafe implを使用することで、コンパイラが確かめられない不変条件を守ることを約束しています。
例として、第16章の「SyncとSendトレイトで拡張可能な並行性」節で議論したSyncとSendマーカートレイトを思い出してください:
型が完全にSendとSync型だけで構成されていたら、コンパイラはこれらのトレイトを自動的に実装します。
生ポインタなどのSendやSyncでない型を含む型を実装し、その型をSendやSyncでマークしたいなら、
unsafeを使用しなければなりません。コンパイラは、型がスレッド間を安全に送信できたり、
複数のスレッドから安全にアクセスできるという保証を保持しているか確かめられません; 故に、そのチェックを手動で行い、
unsafeでそのように示唆する必要があります。
いつunsafeコードを使用するべきか
unsafeを使って議論したばかりの4つの行動(強大な力)のうちの1つを行うのは間違っていたり、認められさえもしないものではありません。
ですが、unsafeコードを正しくするのは、より巧妙なことでしょう。コンパイラがメモリ安全性を保持する手助けをできないからです。
unsafeコードを使用する理由があるなら、そうすることができ、明示的にunsafe注釈をすることで問題が起きたら、
その原因を追求するのが容易になります。
高度なトレイト
最初にトレイトについて講義したのは、第10章の「トレイト: 共通の振る舞いを定義する」節でしたが、 ライフタイム同様、より高度な詳細は議論しませんでした。今や、Rustに詳しくなったので、核心に迫れるでしょう。
関連型でトレイト定義においてプレースホルダーの型を指定する
関連型は、トレイトのメソッド定義がシグニチャでプレースホルダーの型を使用できるように、トレイトと型のプレースホルダーを結び付けます。 トレイトを実装するものがこの特定の実装で型の位置に使用される具体的な型を指定します。そうすることで、 なんらかの型を使用するトレイトをトレイトを実装するまでその型が一体なんであるかを知る必要なく定義できます。
この章のほとんどの高度な機能は、稀にしか必要にならないと解説しました。関連型はその中間にあります: 本の他の部分で説明される機能よりは使用されるのが稀ですが、この章で議論される他の多くの機能よりは頻繁に使用されます。
関連型があるトレイトの一例は、標準ライブラリが提供するIteratorトレイトです。その関連型はItemと名付けられ、
Iteratorトレイトを実装している型が走査している値の型の代役を務めます。第13章の「Iteratorトレイトとnextメソッド」節で、
Iteratorトレイトの定義は、リスト19-20に示したようなものであることに触れました。
#![allow(unused)] fn main() { pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } }
リスト19-20: 関連型ItemがあるIteratorトレイトの定義
型Itemはプレースホルダー型でnextメソッドの定義は、型Option<Self::Item>の値を返すことを示しています。
Iteratorトレイトを実装するものは、Itemの具体的な型を指定し、nextメソッドは、
その具体的な型の値を含むOptionを返します。
関連型は、ジェネリクスにより扱う型を指定せずに関数を定義できるという点でジェネリクスに似た概念のように思える可能性があります。 では、何故関連型を使用するのでしょうか?
2つの概念の違いを第13章からCounter構造体にIteratorトレイトを実装する例で調査しましょう。
リスト13-21で、Item型はu32だと指定しました:
ファイル名: src/lib.rs
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
この記法は、ジェネリクスと比較可能に思えます。では、何故単純にリスト19-21のように、
Iteratorトレイトをジェネリクスで定義しないのでしょうか?
#![allow(unused)] fn main() { pub trait Iterator<T> { fn next(&mut self) -> Option<T>; } }
リスト19-21: ジェネリクスを使用した架空のIteratorトレイトの定義
差異は、リスト19-21のようにジェネリクスを使用すると、各実装で型を注釈しなければならないことです;
Iterator<String> for Counterや他のどんな型にも実装することができるので、
CounterのIteratorの実装が複数できるでしょう。換言すれば、トレイトにジェネリックな引数があると、
毎回ジェネリックな型引数の具体的な型を変更してある型に対して複数回実装できるということです。
Counterに対してnextメソッドを使用する際に、どのIteratorの実装を使用したいか型注釈をつけなければならないでしょう。
関連型なら、同じ型に対してトレイトを複数回実装できないので、型を注釈する必要はありません。
関連型を使用する定義があるリスト19-20では、Itemの型は1回しか選択できませんでした。
1つしかimpl Iterator for Counterがないからです。Counterにnextを呼び出す度に、
u32値のイテレータが欲しいと指定しなくてもよいわけです。
デフォルトのジェネリック型引数と演算子オーバーロード
ジェネリックな型引数を使用する際、ジェネリックな型に対して既定の具体的な型を指定できます。これにより、
既定の型が動くのなら、トレイトを実装する側が具体的な型を指定する必要を排除します。ジェネリックな型に既定の型を指定する記法は、
ジェネリックな型を宣言する際に<PlaceholderType=ConcreteType>です。
このテクニックが有用になる場面の好例が、演算子オーバーロードです。演算子オーバーロードとは、
特定の状況で演算子(+など)の振る舞いをカスタマイズすることです。
Rustでは、独自の演算子を作ったり、任意の演算子をオーバーロードすることはできません。しかし、
演算子に紐づいたトレイトを実装することでstd::opsに列挙された処理と対応するトレイトをオーバーロードできます。
例えば、リスト19-22で+演算子をオーバーロードして2つのPointインスタンスを足し合わせています。
Point構造体にAddトレイトを実装することでこれを行なっています。
ファイル名: src/main.rs
use std::ops::Add; #[derive(Debug, 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 }); }
リスト19-22: Addトレイトを実装してPointインスタンス用に+演算子をオーバーロードする
addメソッドは2つのPointインスタンスのx値と2つのPointインスタンスのy値を足します。
Addトレイトには、addメソッドから返却される型を決定するOutputという関連型があります。
このコードの既定のジェネリック型は、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トレイトの実装例に目を向けましょう。
異なる単位で値を保持する構造体、MillimetersとMeters(それぞれミリメートルとメートル)が2つあります。
ミリメートルの値をメートルの値に足し、Addの実装に変換を正しくしてほしいです。
AddをRHSにMetersのあるMillimetersに実装することができます。リスト19-23のように:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { 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)) } } }
リスト19-23: MillimetersにAddトレイトを実装して、MetersにMillimetersを足す
MillimetersをMetersに足すため、Selfという既定を使う代わりにimpl Add<Meters>を指定して、
RHS型引数の値をセットしています。
主に2通りの方法でデフォルト型引数を使用します:
- 既存のコードを破壊せずに型を拡張する
- ほとんどのユーザは必要としない特定の場合でカスタマイズを可能にする
標準ライブラリのAddトレイトは、2番目の目的の例です: 通常、2つの似た型を足しますが、
Addトレイトはそれ以上にカスタマイズする能力を提供します。Addトレイト定義でデフォルト型引数を使用することは、
ほとんどの場合、追加の引数を指定しなくてもよいことを意味します。つまり、トレイトを使いやすくして、
ちょっとだけ実装の定型コードが必要なくなるのです。
最初の目的は2番目に似ていますが、逆です: 既存のトレイトに型引数を追加したいなら、既定を与えて、 既存の実装コードを破壊せずにトレイトの機能を拡張できるのです。
明確化のためのフルパス記法: 同じ名前のメソッドを呼ぶ
Rustにおいて、別のトレイトのメソッドと同じ名前のメソッドがトレイトにあったり、両方のトレイトを1つの型に実装することを妨げるものは何もありません。 トレイトのメソッドと同じ名前のメソッドを直接型に実装することも可能です。
同じ名前のメソッドを呼ぶ際、コンパイラにどれを使用したいのか教える必要があるでしょう。両方ともflyというメソッドがある2つのトレイト、
PilotとWizard(訳注: パイロットと魔法使い)を定義したリスト19-24のコードを考えてください。
それから両方のトレイトを既にflyというメソッドが実装されている型Human(訳注: 人間)に実装します。
各flyメソッドは異なることをします。
ファイル名: src/main.rs
#![allow(unused)] fn main() { 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*"); } } }
リスト19-24: 2つのトレイトにflyがあるように定義され、Humanに実装されつつ、
flyメソッドはHumanに直接にも実装されている
Humanのインスタンスに対してflyを呼び出すと、コンパイラは型に直接実装されたメソッドを標準で呼び出します。
リスト19-25のようにですね:
ファイル名: src/main.rs
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(); }
リスト19-25: Humanのインスタンスに対してflyを呼び出す
このコードを実行すると、*waving arms furiously*と出力され、コンパイラがHumanに直接実装されたflyメソッドを呼んでいることを示しています。
Pilotトレイトか、Wizardトレイトのflyメソッドを呼ぶためには、
より明示的な記法を使用して、どのflyメソッドを意図しているか指定する必要があります。
リスト19-26は、この記法をデモしています。
ファイル名: src/main.rs
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(); }
リスト19-26: どのトレイトのflyメソッドを呼び出したいか指定する
メソッド名の前にトレイト名を指定すると、コンパイラにどのflyの実装を呼び出したいか明確化できます。
また、Human::fly(&person)と書くこともでき、リスト19-26で使用したperson.fly()と等価ですが、
こちらの方は明確化する必要がないなら、ちょっと記述量が増えます。
このコードを実行すると、こんな出力がされます:
This is your captain speaking.
Up!
*waving arms furiously*
flyメソッドはself引数を取るので、1つのトレイトを両方実装する型が2つあれば、
コンパイラには、selfの型に基づいてどのトレイトの実装を使うべきかわかるでしょう。
しかしながら、トレイトの一部になる関連関数にはself引数がありません。同じスコープの2つの型がそのトレイトを実装する場合、
フルパス記法(fully qualified syntax)を使用しない限り、どの型を意図しているかコンパイラは推論できません。例えば、
リスト19-27のAnimalトレイトには、関連関数baby_name、構造体DogのAnimalの実装、
Dogに直接定義された関連関数baby_nameがあります。
ファイル名: src/main.rs
trait Animal { fn baby_name() -> String; } struct Dog; impl Dog { fn baby_name() -> String { // スポット(Wikipediaによると、飼い主の事故死後もその人の帰りを待つ忠犬の名前の模様) 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()); }
リスト19-27: 関連関数のあるトレイトとそのトレイトも実装し、同じ名前の関連関数がある型
このコードは、全ての子犬をスポットと名付けたいアニマル・シェルター(訳注: 身寄りのないペットを保護する保健所みたいなところ)用で、
Dogに定義されたbaby_name関連関数で実装されています。Dog型は、トレイトAnimalも実装し、
このトレイトは全ての動物が持つ特徴を記述します。赤ちゃん犬は子犬と呼ばれ、
それがDogのAnimalトレイトの実装のAnimalトレイトと紐づいたbase_name関数で表現されています。
mainで、Dog::baby_name関数を呼び出し、直接Dogに定義された関連関数を呼び出しています。
このコードは以下のような出力をします:
A baby dog is called a Spot
この出力は、欲しかったものではありません。Dogに実装したAnimalトレイトの一部のbaby_name関数を呼び出したいので、
コードはA baby dog is called a puppyと出力します。リスト19-26で使用したトレイト名を指定するテクニックは、
ここでは役に立ちません; mainをリスト19-28のようなコードに変更したら、コンパイルエラーになるでしょう。
ファイル名: src/main.rs
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
リスト19-28: Animalトレイトのbaby_name関数を呼び出そうとするも、コンパイラにはどの実装を使うべきかわからない
Animal::baby_nameはメソッドではなく関連関数であり、故にself引数がないので、どのAnimal::baby_nameが欲しいのか、
コンパイラには推論できません。こんなコンパイルエラーが出るでしょう:
error[E0283]: type annotations required: cannot resolve `_: Animal`
(エラー: 型注釈が必要です: `_: Animal`を解決できません)
--> src/main.rs:20:43
|
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^
|
= note: required by `Animal::baby_name`
(注釈: `Animal::baby_name`に必要です)
Dogに対してAnimal実装を使用したいと明確化し、コンパイラに指示するには、フルパス記法を使う必要があります。
リスト19-29は、フルパス記法を使用する方法をデモしています。
ファイル名: src/main.rs
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()); }
リスト19-29: フルパス記法を使ってDogに実装されているように、
Animalトレイトからのbaby_name関数を呼び出したいと指定する
コンパイラに山カッコ内で型注釈を提供し、これは、この関数呼び出しではDog型をAnimalとして扱いたいと宣言することで、
Dogに実装されたように、Animalトレイトのbaby_nameメソッドを呼び出したいと示唆しています。
もうこのコードは、望み通りの出力をします:
A baby dog is called a puppy
一般的に、フルパス記法は、以下のように定義されています:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
関連関数では、receiverがないでしょう: 他の引数のリストがあるだけでしょう。関数やメソッドを呼び出す箇所全部で、
フルパス記法を使用することもできるでしょうが、プログラムの他の情報からコンパイラが推論できるこの記法のどの部分も省略することが許容されています。
同じ名前を使用する実装が複数あり、どの実装を呼び出したいかコンパイラが特定するのに助けが必要な場合だけにこのより冗長な記法を使用する必要があるのです。
スーパートレイトを使用して別のトレイト内で、あるトレイトの機能を必要とする
時として、あるトレイトに別のトレイトの機能を使用させる必要がある可能性があります。この場合、 依存するトレイトも実装されることを信用する必要があります。信用するトレイトは、実装しているトレイトのスーパートレイトです。
例えば、アスタリスクをフレームにする値を出力するoutline_printメソッドがあるOutlinePrintトレイトを作りたくなったとしましょう。
つまり、Displayを実装し、(x, y)という結果になるPoint構造体が与えられて、
xが1、yが3のPointインスタンスに対してoutline_printを呼び出すと、以下のような出力をするはずです:
**********
* *
* (1, 3) *
* *
**********
outline_printの実装では、Displayトレイトの機能を使用したいです。故に、Displayも実装する型に対してだけOutlinePrintが動くと指定し、
OutlinePrintが必要とする機能を提供する必要があるわけです。トレイト定義でOutlinePrint: Displayと指定することで、
そうすることができます。このテクニックは、トレイトにトレイト境界を追加することに似ています。
リスト19-30は、OutlinePrintトレイトの実装を示しています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { 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)); } } }
リスト19-30: Displayからの機能を必要とするOutlinePrintトレイトを実装する
OutlinePrintはDisplayトレイトを必要とすると指定したので、Displayを実装するどんな型にも自動的に実装されるto_string関数を使えます。
トレイト名の後にコロンとDisplayトレイトを追加せずにto_stringを使おうとしたら、
現在のスコープで型&Selfにto_stringというメソッドは存在しないというエラーが出るでしょう。
Displayを実装しない型、Point構造体などにOutlinePrintを実装しようとしたら、何が起きるか確認しましょう:
ファイル名: src/main.rs
#![allow(unused)] fn main() { trait OutlinePrint {} struct Point { x: i32, y: i32, } impl OutlinePrint for Point {} }
Displayが必要だけれども、実装されていないというエラーが出ます:
error[E0277]: the trait bound `Point: std::fmt::Display` is not satisfied
--> src/main.rs:20:6
|
20 | impl OutlinePrint for Point {}
| ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter;
try using `:?` instead if you are using a format string
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
これを修正するために、PointにDisplayを実装し、OutlinePrintが必要とする制限を満たします。
こんな感じで:
ファイル名: src/main.rs
#![allow(unused)] fn main() { struct Point { x: i32, y: i32, } use std::fmt; impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } }
そうすれば、PointにOutlinePrintトレイトを実装してもコンパイルは成功し、
Pointインスタンスに対してoutline_printを呼び出し、アスタリスクのふちの中に表示することができます。
ニュータイプパターンを使用して外部の型に外部のトレイトを実装する
第10章の「型にトレイトを実装する」節で、トレイトか型がクレートにローカルな限り、型にトレイトを実装できると述べるオーファンルールについて触れました。 ニュータイプパターンを使用してこの制限を回避することができ、タプル構造体に新しい型を作成することになります。 (タプル構造体については、第5章の「異なる型を生成する名前付きフィールドのないタプル構造体を使用する」節で講義しました。) タプル構造体は1つのフィールドを持ち、トレイトを実装したい型の薄いラッパになるでしょう。そして、 ラッパの型はクレートにローカルなので、トレイトをラッパに実装できます。ニュータイプという用語は、 Haskellプログラミング言語に端を発しています。このパターンを使用するのに実行時のパフォーマンスを犠牲にすることはなく、 ラッパ型はコンパイル時に省かれます。
例として、Vec<T>にDisplayを実装したいとしましょう。DisplayトレイトもVec<T>型もクレートの外で定義されているので、
直接それを行うことはオーファンルールにより妨げられます。Vec<T>のインスタンスを保持するWrapper構造体を作成できます;
そして、WrapperにDisplayを実装し、Vec<T>値を使用できます。リスト19-31のように。
ファイル名: src/main.rs
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); }
リスト19-31: Vec<String>の周りにWrapperを作成してDisplayを実装する
Displayの実装は、self.0で中身のVec<T>にアクセスしています。Wrapperはタプル構造体で、
Vec<T>がタプルの添え字0の要素だからです。それから、Wrapperに対してDisplay型の機能を使用できます。
このテクニックを使用する欠点は、Wrapperが新しい型なので、保持している値のメソッドがないことです。
self.0に委譲して、WrapperをVec<T>と全く同様に扱えるように、Wrapperに直接Vec<T>の全てのメソッドを実装しなければならないでしょう。
内部の型が持つ全てのメソッドを新しい型に持たせたいなら、
Derefトレイト(第15章の「Derefトレイトでスマートポインタを普通の参照のように扱う」節で議論しました)をWrapperに実装して、
内部の型を返すことは解決策の1つでしょう。内部の型のメソッド全部をWrapper型に持たせたくない(例えば、Wrapper型の機能を制限するなど)なら、
本当に欲しいメソッドだけを手動で実装しなければならないでしょう。
もう、トレイトに関してニュータイプパターンが使用される方法を知りました; トレイトが関連しなくても、 有用なパターンでもあります。焦点を変更して、Rustの型システムと相互作用する一部の高度な方法を見ましょう。
高度な型
Rustの型システムには、この本で触れたけれども、まだ議論していない機能があります。ニュータイプが何故型として有用なのかを調査するため、
一般化してニュータイプを議論することから始めます。そして、型エイリアスに移ります。ニュータイプに類似しているけれども、
多少異なる意味を持つ機能です。また、!型と動的サイズ決定型も議論します。
注釈: 次の節は、前節「外部の型に外部のトレイトを実装するニュータイプパターン」を読了済みであることを前提にしています。
型安全性と抽象化を求めてニュータイプパターンを使用する
ここまでに議論した以上の作業についてもニュータイプパターンは有用で、静的に絶対に値を混同しないことを強制したり、
値の単位を示すことを含みます。ニュータイプを使用して単位を示す例をリスト19-23で見かけました:
MillimetersとMeters構造体は、u32値をニュータイプにラップしていたことを思い出してください。
型Millimetersを引数にする関数を書いたら、誤ってその関数を型Metersや普通のu32で呼び出そうとするプログラムはコンパイルできないでしょう。
型の実装の詳細を抽象化する際にニュータイプパターンを使用するでしょう: 例えば、新しい型を直接使用して、 利用可能な機能を制限したら、非公開の内部の型のAPIとは異なる公開APIを新しい型は露出できます。
ニュータイプはまた、内部の実装を隠匿することもできます。例を挙げれば、People型を提供して、
人のIDと名前を紐づけて格納するHashMap<i32, String>をラップすることができるでしょう。
Peopleを使用するコードは、名前の文字列をPeopleコレクションに追加するメソッドなど、
提供している公開APIとだけ相互作用するでしょう; そのコードは、内部でi32IDを名前に代入していることを知る必要はないでしょう。
ニュータイプパターンは、カプセル化を実現して実装の詳細を隠匿する軽い方法であり、
実装の詳細を隠匿することは、第17章の「カプセル化は実装詳細を隠蔽する」節で議論しましたね。
型エイリアスで型同義語を生成する
ニュータイプパターンに付随して、Rustでは、既存の型に別の名前を与える型エイリアス(type alias: 型別名)を宣言する能力が提供されています。
このために、typeキーワードを使用します。例えば、以下のようにi32に対してKilometersというエイリアスを作れます。
#![allow(unused)] fn main() { type Kilometers = i32; }
これで、別名のKilometersはi32と同義語になりました; リスト19-23で生成したMillimetersとMetersとは異なり、
Kilometersは個別の新しい型ではありません。型Kilometersの値は、型i32の値と同等に扱われます。
#![allow(unused)] fn main() { type Kilometers = i32; let x: i32 = 5; let y: Kilometers = 5; println!("x + y = {}", x + y); }
Kilometersとi32が同じ型なので、両方の型の値を足し合わせたり、Kilometersの値をi32引数を取る関数に渡せたりします。
ですが、この方策を使用すると、先ほど議論したニュータイプパターンで得られる型チェックの利便性は得られません。
型同義語の主なユースケースは、繰り返しを減らすことです。例えば、こんな感じの長い型があるかもしれません:
Box<Fn() + Send + 'static>
この長ったらしい型を関数シグニチャや型注釈としてコードのあちこちで記述するのは、面倒で間違いも起きやすいです。 リスト19-32のそのようなコードで溢れかえったプロジェクトがあることを想像してください。
#![allow(unused)] fn main() { let f: Box<Fn() + Send + 'static> = Box::new(|| println!("hi")); fn takes_long_type(f: Box<Fn() + Send + 'static>) { // --snip-- } fn returns_long_type() -> Box<Fn() + Send + 'static> { // --snip-- Box::new(|| ()) } }
リスト19-32: 長い型を多くの場所で使用する
型エイリアスは、繰り返しを減らすことでこのコードをより管理しやすくしてくれます。リスト19-33で、
冗長な型にThunk(注釈: 塊)を導入し、その型の使用全部をより短い別名のThunkで置き換えることができます。
#![allow(unused)] fn main() { type Thunk = Box<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(|| ()) } }
リスト19-33: 型エイリアスのThunkを導入して繰り返しを減らす
このコードの方が遥かに読み書きしやすいです!型エイリアスに意味のある名前を選択すると、 意図を伝えるのにも役に立つことがあります(thunkは後ほど評価されるコードのための単語なので、 格納されるクロージャーには適切な名前です)。
型エイリアスは、繰り返しを減らすためにResult<T, E>型ともよく使用されます。標準ライブラリのstd::ioモジュールを考えてください。
I/O処理はしばしば、Result<T, E>を返して処理がうまく動かなかった時を扱います。このライブラリには、
全ての可能性のあるI/Oエラーを表すstd::io::Error構造体があります。std::ioの関数の多くは、
Writeトレイトの以下の関数のようにEがstd::io::ErrorのResult<T, E>を返すでしょう:
#![allow(unused)] fn main() { use std::io::Error; use std::fmt; 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にはこんな類のエイリアス宣言があります:
type Result<T> = Result<T, std::io::Error>;
この宣言はstd::ioモジュール内にあるので、フルパスエイリアスのstd::io::Result<T>を使用できます。
つまり、Eがstd::io::Errorで埋められたResult<T, E>です。その結果、Writeトレイトの関数シグニチャは、
以下のような見た目になります:
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: Arguments) -> Result<()>;
}
型エイリアスは、2通りの方法で役に立っています: コードを書きやすくすることとstd::ioを通して首尾一貫したインターフェイスを与えてくれることです。
別名なので、ただのResult<T, E>であり、要するにResult<T, E>に対して動くメソッドはなんでも使えるし、
?演算子のような特殊な記法も使えます。
never型は絶対に返らない
Rustには、!という名前の特別な型があります。それは型理論の専門用語では Empty型 と呼ばれ値なしを表します。私たちは、
関数が値を返すことが決して (never) ない時に戻り値の型を記す場所に使われるので、never type(訳注: 日本語にはできないので、never型と呼ぶしかないか)と呼ぶのが好きです。
こちらが例です:
fn bar() -> ! {
// --snip--
}
このコードは、「関数barはneverを返す」と解読します。neverを返す関数は、発散する関数(diverging function)と呼ばれます。
型!の値は生成できないので、barからリターンする(呼び出し元に制御を戻す)ことは決してできません。
ですが、値を絶対に生成できない型をどう使用するのでしょうか?リスト2-5のコードを思い出してください; リスト19-34に一部を再掲します。
#![allow(unused)] fn main() { let guess = "3"; loop { let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; break; } }
リスト19-34: continueになるアームがあるmatch
この時点では、このコードの詳細の一部を飛ばしました。第6章の「match制御フロー演算子」節で、
matchアームは全て同じ型を返さなければならないと議論しました。従って、例えば以下のコードは動きません:
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "hello",
}
このコードのguessは整数かつ文字列にならなければならないでしょうが、Rustでは、guessは1つの型にしかならないことを要求されます。
では、continueは何を返すのでしょうか?どうやってリスト19-34で1つのアームからはu32を返し、別のアームでは、
continueで終わっていたのでしょうか?
もうお気付きかもしれませんが、continueは!値です。つまり、コンパイラがguessの型を計算する時、
両方のmatchアームを見て、前者はu32の値、後者は!値となります。!は絶対に値を持ち得ないので、
コンパイラは、guessの型はu32と決定するのです。
この振る舞いを解説する公式の方法は、型!の式は、他のどんな型にも型強制され得るということです。
このmatchアームをcontinueで終えることができます。何故なら、continueは値を返さないからです;
その代わりに制御をループの冒頭に戻すので、Errの場合、guessには絶対に値を代入しないのです。
never型は、panic!マクロとも有用です。Option<T>値に対して呼び出して、値かパニックを生成したunwrap関数を覚えていますか?
こちらがその定義です:
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
このコードにおいて、リスト19-34のmatchと同じことが起きています: コンパイラは、valの型はTで、
panic!の型は!なので、match式全体の結果はTと確認します。panic!は値を生成しないので、
このコードは動きます。つまり、プログラムを終了するのです。Noneの場合、unwrapから値は返さないので、
このコードは合法なのです。
型が!の最後の式は、loopです:
// 永遠に
print!("forever ");
loop {
// さらに永遠に
print!("and ever ");
}
ここで、ループは終わりませんので、!が式の値です。ところが、breakを含んでいたら、これは真実にはならないでしょう。
breakに到達した際にループが終了してしまうからです。
動的サイズ決定型とSizedトレイト
コンパイラが特定の型の値1つにどれくらいのスペースのメモリを確保するのかなどの特定の詳細を知る必要があるために、 Rustの型システムには混乱を招きやすい細かな仕様があります: 動的サイズ決定型の概念です。時としてDSTやサイズなし型とも称され、 これらの型により、実行時にしかサイズを知ることのできない値を使用するコードを書かせてくれます。
strと呼ばれる動的サイズ決定型の詳細を深掘りしましょう。本を通して使用してきましたね。
そうです。&strではなく、strは単独でDSTなのです。実行時までは文字列の長さを知ることができず、
これは、型strの変数を生成したり、型strを引数に取ることはできないことを意味します。
動かない以下のコードを考えてください:
// こんにちは
let s1: str = "Hello there!";
// 調子はどう?
let s2: str = "How's it going?";
コンパイラは、特定の型のどんな値に対しても確保するメモリ量を知る必要があり、ある型の値は全て同じ量のメモリを使用しなければなりません。
Rustでこのコードを書くことが許容されたら、これら2つのstr値は、同じ量のスペースを消費する必要があったでしょう。
ですが、長さが異なります: s1は、12バイトのストレージが必要で、s2は15バイトです。このため、
動的サイズ決定型を保持する変数を生成することはできないのです。
では、どうすればいいのでしょうか?この場合、もう答えはご存知です: s1とs2の型をstrではなく、
&strにすればいいのです。第4章の「文字列スライス」節でスライスデータ構造は、
開始地点とスライスの長さを格納していると述べたことを思い出してください。
従って、&Tは、Tがどこにあるかのメモリアドレスを格納する単独の値だけれども、&strは2つの値なのです:
strのアドレスとその長さです。そのため、コンパイル時に&strのサイズを知ることができます:
usizeの長さの2倍です。要するに、参照している文字列の長さによらず、常に&strのサイズがわかります。
通常、このようにしてRustでは動的サイズ決定型が使用されます: 動的情報のサイズを格納する追加のちょっとしたメタデータがあるのです。
動的サイズ決定型の黄金規則は、常に動的サイズ決定型の値をなんらかの種類のポインタの背後に配置しなければならないということです。
strを全ての種類のポインタと組み合わせられます: 例を挙げれば、Box<str>やRc<str>などです。
実際、これまでに見かけましたが、異なる動的サイズ決定型でした: トレイトです。全てのトレイトは、
トレイト名を使用して参照できる動的サイズ決定型です。第17章の「トレイトオブジェクトで異なる型の値を許容する」節で、
トレイトをトレイトオブジェクトとして使用するには、&TraitやBox<Trait>(Rc<Trait>も動くでしょう)など、
ポインタの背後に配置しなければならないことに触れました。
DSTを扱うために、RustにはSizedトレイトと呼ばれる特定のトレイトがあり、型のサイズがコンパイル時にわかるかどうかを決定します。
このトレイトは、コンパイル時にサイズの判明する全てのものに自動的に実装されます。加えて、
コンパイラは暗黙的に全てのジェネリックな関数にSizedの境界を追加します。つまり、こんな感じのジェネリック関数定義は:
fn generic<T>(t: T) {
// --snip--
}
実際にはこう書いたかのように扱われます:
fn generic<T: Sized>(t: T) {
// --snip--
}
既定では、ジェネリック関数はコンパイル時に判明するサイズがある型に対してのみ動きます。 ですが、以下の特別な記法を用いてこの制限を緩めることができます:
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
?Sizedのトレイト境界は、Sizedのトレイト境界の逆になります: これを「TはSizedかもしれないし、違うかもしれない」と解読するでしょう。
この記法は、Sizedにのみ利用可能で、他のトレイトにはありません。
また、t引数の型をTから&Tに切り替えたことにも注目してください。型はSizedでない可能性があるので、
なんらかのポインタの背後に使用する必要があるのです。今回は、参照を選択しました。
次は、関数とクロージャについて語ります!
高度な関数とクロージャ
最後に関数とクロージャに関連する高度な機能の一部を探究し、これには関数ポインタとクロージャの返却が含まれます。
関数ポインタ
クロージャを関数に渡す方法について語りました; 普通の関数を関数に渡すこともできるのです!
新しいクロージャを定義するのではなく、既に定義した関数を渡したい時にこのテクニックは有用です。
これを関数ポインタで行うと、関数を引数として他の関数に渡して使用できます。関数は、型fn(小文字のfです)に型強制されます。
Fnクロージャトレイトと混同すべきではありません。fn型は、関数ポインタと呼ばれます。
引数が関数ポインタであると指定する記法は、クロージャのものと似ています。リスト19-35のように。
ファイル名: src/main.rs
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); }
リスト19-35: fn型を使用して引数として関数ポインタを受け入れる
このコードは、The answer is: 12と出力します。do_twiceの引数fは、型i32の1つの引数を取り、
i32を返すfnと指定しています。それから、do_twiceの本体でfを呼び出すことができます。
mainでは、関数名のadd_oneを最初の引数としてdo_twiceに渡せます。
クロージャと異なり、fnはトレイトではなく型なので、トレイト境界としてFnトレイトの1つでジェネリックな型引数を宣言するのではなく、
直接fnを引数の型として指定します。
関数ポインタは、クロージャトレイト3つ全て(Fn、FnMut、FnOnce)を実装するので、常に関数ポインタを引数として、
クロージャを期待する関数に渡すことができます。関数が関数とクロージャどちらも受け入れられるように、
ジェネリックな型とクロージャトレイトの1つを使用して関数を書くのが最善です。
クロージャではなくfnだけを受け入れたくなる箇所の一例は、クロージャのない外部コードとのインターフェイスです:
C関数は引数として関数を受け入れられますが、Cにはクロージャがありません。
インラインでクロージャが定義されるか、名前付きの関数を使用できるであろう箇所の例として、mapの使用に目を向けましょう。
map関数を使用して数字のベクタを文字列のベクタに変換するには、このようにクロージャを使用できるでしょう:
#![allow(unused)] 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に引数として関数を名指しできるでしょう:
#![allow(unused)] 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を実装するあらゆる型に実装しています。
このスタイルを好む方もいますし、クロージャを使うのを好む方もいます。どちらも結果的に同じコードにコンパイルされるので、 どちらでも、自分にとって明確な方を使用してください。
クロージャを返却する
クロージャはトレイトによって表現されます。つまり、クロージャを直接は返却できないのです。
トレイトを返却したい可能性のあるほとんどの場合、代わりにトレイトを実装する具体的な型を関数の戻り値として使用できます。
ですが、クロージャではそれはできません。返却可能な具体的な型がないからです; 例えば、
関数ポインタのfnを戻り値の型として使うことは許容されていません。
以下のコードは、クロージャを直接返そうとしていますが、コンパイルできません:
fn returns_closure() -> Fn(i32) -> i32 {
|x| x + 1
}
コンパイルエラーは以下の通りです:
error[E0277]: the trait bound `std::ops::Fn(i32) -> i32 + 'static:
std::marker::Sized` is not satisfied
-->
|
1 | fn returns_closure() -> Fn(i32) -> i32 {
| ^^^^^^^^^^^^^^ `std::ops::Fn(i32) -> i32 + 'static`
does not have a constant size known at compile-time
|
= help: the trait `std::marker::Sized` is not implemented for
`std::ops::Fn(i32) -> i32 + 'static`
= note: the return type of a function must have a statically known size
エラーは、再度Sizedトレイトを参照しています!コンパイラには、クロージャを格納するのに必要なスペースがどれくらいかわからないのです。
この問題の解決策は先ほど見かけました。トレイトオブジェクトを使えます:
#![allow(unused)] fn main() { fn returns_closure() -> Box<Fn(i32) -> i32> { Box::new(|x| x + 1) } }
このコードは、問題なくコンパイルできます。トレイトオブジェクトについて詳しくは、 第17章の「トレイトオブジェクトで異なる型の値を許容する」節を参照してください。
次は、マクロを見てみましょう!
マクロ
本全体を通じてprintln!のようなマクロを使用してきましたが、マクロがなんなのかや、
どう動いているのかということは完全には探究していませんでした。
Rustにおいて、マクロという用語はある機能の集合のことを指します:macro_rules!を使った 宣言的 (declarative) マクロと、3種類の 手続き的 (procedural) マクロ:
- 構造体とenumに
derive属性を使ったときに追加されるコードを指定する、カスタムの#[derive]マクロ - 任意の要素に使えるカスタムの属性を定義する、属性風のマクロ
- 関数のように見えるが、引数として指定されたトークンに対して作用する関数風のマクロ
です。
それぞれについて一つずつ話していきますが、その前にまず、どうして関数がすでにあるのにマクロなんてものが必要なのか見てみましょう。
マクロと関数の違い
基本的に、マクロは、他のコードを記述するコードを書く術であり、これはメタプログラミングとして知られています。
付録Cで、derive属性を議論し、これは、色々なトレイトの実装を生成してくれるのでした。
また、本を通してprintln!やvec!マクロを使用してきました。これらのマクロは全て、展開され、
手で書いたよりも多くのコードを生成します。
メタプログラミングは、書いて管理しなければならないコード量を減らすのに有用で、これは、関数の役目の一つでもあります。 ですが、マクロには関数にはない追加の力があります。
関数シグニチャは、関数の引数の数と型を宣言しなければなりません。一方、マクロは可変長の引数を取れます:
println!("hello")のように1引数で呼んだり、println!("hello {}", name)のように2引数で呼んだりできるのです。
また、マクロは、コンパイラがコードの意味を解釈する前に展開されるので、例えば、
与えられた型にトレイトを実装できます。関数ではできません。何故なら、関数は実行時に呼ばれ、
トレイトはコンパイル時に実装される必要があるからです。
関数ではなくマクロを実装する欠点は、Rustコードを記述するRustコードを書いているので、 関数定義よりもマクロ定義は複雑になることです。この間接性のために、マクロ定義は一般的に、 関数定義よりも、読みにくく、わかりにくく、管理しづらいです。
マクロと関数にはもう一つ、重要な違いがあります: ファイル内で呼び出す前にマクロは定義したりスコープに導入しなければなりませんが、 一方で関数はどこにでも定義でき、どこでも呼び出せます。
一般的なメタプログラミングのためにmacro_rules!で宣言的なマクロ
Rustにおいて、最もよく使用される形態のマクロは、宣言的マクロです。これらは時として、
例によるマクロ、macro_rules!マクロ、あるいはただ単にマクロとも称されます。
核となるのは、宣言的マクロは、Rustのmatch式に似た何かを書けるということです。第6章で議論したように、
match式は、式を取り、式の結果の値をパターンと比較し、それからマッチしたパターンに紐づいたコードを実行する制御構造です。
マクロも、あるコードと紐付けられたパターンと値を比較します。ここで、値とは
マクロに渡されたリテラルのRustのソースコードそのもののこと。パターンがそのソースコードの構造と比較されます。
各パターンに紐づいたコードは、それがマッチしたときに、マクロに渡されたコードを置き換えます。これは全て、コンパイル時に起きます。
マクロを定義するには、macro_rules!構文を使用します。vec!マクロが定義されている方法を見て、
macro_rules!を使用する方法を探究しましょう。vec!マクロを使用して特定の値で新しいベクタを生成する方法は、
第8章で講義しました。例えば、以下のマクロは、3つの整数を持つ新しいベクタを生成します:
#![allow(unused)] fn main() { let v: Vec<u32> = vec![1, 2, 3]; }
また、vec!マクロを使用して2整数のベクタや、5つの文字列スライスのベクタなども生成できます。
同じことを関数を使って行うことはできません。予め、値の数や型がわかっていないからです。
リスト19-28で些か簡略化されたvec!マクロの定義を見かけましょう。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[macro_export] macro_rules! vec { ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec } }; } }
リスト19-28: vec!マクロ定義の簡略化されたバージョン
標準ライブラリの
vec!マクロの実際の定義は、予め正確なメモリ量を確保するコードを含みます。 その最適化コードは、ここでは簡略化のために含みません。
#[macro_export]注釈は、マクロを定義しているクレートがスコープに持ち込まれたなら、無条件でこのマクロが利用可能になるべきということを示しています。
この注釈がなければ、このマクロはスコープに導入されることができません。
それから、macro_rules!でマクロ定義と定義しているマクロの名前をビックリマークなしで始めています。
名前はこの場合vecであり、マクロ定義の本体を意味する波括弧が続いています。
vec!本体の構造は、match式の構造に類似しています。ここではパターン( $( $x:expr ),* )の1つのアーム、
=>とこのパターンに紐づくコードのブロックが続きます。パターンが合致すれば、紐づいたコードのブロックが発されます。
これがこのマクロの唯一のパターンであることを踏まえると、合致する合法的な方法は一つしかありません;
それ以外は、全部エラーになるでしょう。より複雑なマクロには、2つ以上のアームがあるでしょう。
マクロ定義で合法なパターン記法は、第18章で講義したパターン記法とは異なります。というのも、 マクロのパターンは値ではなく、Rustコードの構造に対してマッチされるからです。リスト19-28のパターンの部品がどんな意味か見ていきましょう; マクロパターン記法全ては参考文献をご覧ください。
まず、1組のカッコがパターン全体を囲んでいます。次にドル記号($)、そして1組のカッコが続き、
このかっこは、置き換えるコードで使用するためにかっこ内でパターンにマッチする値をキャプチャします。
$()の内部には、$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
}
任意の型のあらゆる数の引数を取り、指定した要素を含むベクタを生成するコードを生成できるマクロを定義しました。
macro_rules!には、いくつかの奇妙なコーナーケースがあります。
将来、Rustには別種の宣言的マクロが登場する予定です。これは、同じように働くけれども、それらのコーナーケースのうちいくらかを修正します。
そのアップデート以降、macro_rules!は事実上非推奨 (deprecated) となる予定です。
この事実と、ほとんどのRustプログラマーはマクロを書くよりも使うことが多いということを考えて、macro_rules!についてはこれ以上語らないことにします。
もしマクロの書き方についてもっと知りたければ、オンラインのドキュメントや、“The Little Book of Rust Macros”のようなその他のリソースを参照してください。
属性からコードを生成する手続き的マクロ
2つ目のマクロの形は、手続き的マクロと呼ばれ、より関数のように働きます(そして一種の手続きです)。 宣言的マクロがパターンマッチングを行い、マッチしたコードを他のコードで置き換えていたのとは違い、 手続き的マクロは、コードを入力として受け取り、そのコードに対して作用し、出力としてコードを生成します。
3種の手続き的マクロ (カスタムのderiveマクロ, 属性風マクロ、関数風マクロ)はみな同じような挙動をします。
手続き的マクロを作る際は、その定義はそれ専用の特殊なクレート内に置かれる必要があります。
これは複雑な技術的理由によるものであり、将来的には解消したいです。
手続き的マクロを使うとListing 19-29のコードのようになります。some_attributeがそのマクロを使うためのプレースホールダーです。
ファイル名: src/lib.rs
use proc_macro;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
Listing 19-29: 手続き的マクロの使用例
手続き的マクロを定義する関数はTokenStreamを入力として受け取り、TokenStreamを出力として生成します。
TokenStream型はRustに内蔵されているproc_macroクレートで定義されており、トークンの列を表します。
ここがマクロの一番重要なところなのですが、マクロが作用するソースコードは、入力のTokenStreamへと変換され、マクロが生成するコードが出力のTokenStreamなのです。
この関数には属性もつけられていますが、これはどの種類の手続き的マクロを作っているのかを指定します。
同じクレート内に複数の種類の手続き的マクロを持つことも可能です。
様々な種類の手続き的マクロを見てみましょう。カスタムのderiveマクロから始めて、そのあと他の種類との小さな相違点を説明します。
カスタムのderive マクロの書き方
hello_macroという名前のクレートを作成してみましょう。
このクレートは、hello_macroという関連関数が1つあるHelloMacroというトレイトを定義します。
クレートの使用者に使用者の型にHelloMacroトレイトを実装することを強制するのではなく、
使用者が型を#[derive(HelloMacro)]で注釈してhello_macro関数の既定の実装を得られるように、
手続き的マクロを提供します。既定の実装は、Hello, Macro! My name is TypeName!(訳注: こんにちは、マクロ!僕の名前はTypeNameだよ!)と出力し、
ここでTypeNameはこのトレイトが定義されている型の名前です。言い換えると、他のプログラマに我々のクレートを使用して、
リスト19-30のようなコードを書けるようにするクレートを記述します。
ファイル名: src/main.rs
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
リスト19-30: 我々の手続き的マクロを使用した時にクレートの使用者が書けるようになるコード
このコードは完成したら、Hello, Macro! My name is Pancakes!(Pancakes: ホットケーキ)と出力します。最初の手順は、
新しいライブラリクレートを作成することです。このように:
$ cargo new hello_macro --lib
次にHelloMacroトレイトと関連関数を定義します:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub trait HelloMacro { fn hello_macro(); } }
トレイトと関数があります。この時点でクレートの使用者は、以下のように、 このトレイトを実装して所望の機能を達成できるでしょう。
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にはリフレクションの能力がないので、型の名前を実行時に検索することができないのです。
コンパイル時にコード生成するマクロが必要です。
注釈: リフレクションとは、実行時に型名や関数の中身などを取得する機能のことです。 言語によって提供されていたりいなかったりしますが、実行時にメタデータがないと取得できないので、 RustやC++のようなアセンブリコードに翻訳され、パフォーマンスを要求される高級言語では、提供されないのが一般的と思われます。
次の手順は、手続き的マクロを定義することです。これを執筆している時点では、手続き的マクロは、
独自のクレートに存在する必要があります。最終的には、この制限は持ち上げられる可能性があります。
クレートとマクロクレートを構成する慣習は以下の通りです: 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ファイルに追加してください:
ファイル名: hello_macro_derive/Cargo.toml
[lib]
proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"
手続き的マクロの定義を開始するために、hello_macro_deriveクレートのsrc/lib.rsファイルにリスト19-31のコードを配置してください。
impl_hello_macro関数の定義を追加するまでこのコードはコンパイルできないことに注意してください。
ファイル名: hello_macro_derive/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// 操作可能な構文木としてのRustコードの表現を構築する
// 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)
}
リスト19-31: Rustコードを処理するためにほとんどの手続き的マクロクレートに必要になるコード
TokenStreamをパースする役割を持つhello_macro_derive関数と、構文木を変換する役割を持つimpl_hello_macro関数にコードを分割したことに注目してください:これにより手続き的マクロを書くのがより簡単になります。
外側の関数(今回だと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コードの完全なパーサを書くのは、単純な作業ではないのです。
hello_macro_derive関数は、ライブラリの使用者が型に#[derive(HelloMacro)]を指定した時に呼び出されます。
それが可能な理由は、ここでhello_macro_derive関数をproc_macro_deriveで注釈し、トレイト名に一致するHelloMacroを指定したからです;
これは、ほとんどの手続き的マクロが倣う慣習です。
この関数はまず、TokenStreamからのinputをデータ構造に変換し、解釈したり操作したりできるようにします。
ここでsynが登場します。
synのparse関数はTokenStreamを受け取り、パースされたRustのコードを表現するDeriveInput構造体を返します。
Listing 19-32はstruct Pancakes;という文字列をパースすることで得られるDeriveInput構造体の関係ある部分を表しています。
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
Listing 19-32: このマクロを使った属性を持つListing 19-30のコードをパースしたときに得られるDeriveInputインスタンス
この構造体のフィールドは、構文解析したRustコードがPancakesというident(識別子、つまり名前)のユニット構造体であることを示しています。
この構造体にはRustコードのあらゆる部分を記述するフィールドがもっと多くあります;
DeriveInputのsynドキュメンテーションで詳細を確認してください。
まもなくimpl_hello_macro関数を定義し、そこにインクルードしたい新しいRustコードを構築します。
でもその前に、私達のderiveマクロのための出力もまたTokenStreamであることに注目してください。
返されたTokenStreamをクレートの使用者が書いたコードに追加しているので、クレートをコンパイルすると、
我々が修正したTokenStreamで提供している追加の機能を得られます。
ここで、unwrapを呼び出すことで、syn::parse関数が失敗したときにhello_macro_derive関数をパニックさせていることにお気付きかもしれません。
エラー時にパニックするのは、手続き的マクロコードでは必要なことです。何故なら、
proc_macro_derive関数は、手続き的マクロのAPIに従うために、Resultではなく
TokenStreamを返さなければならないからです。この例については、unwrapを使用して簡略化することを選択しました;
プロダクションコードでは、panic!かexpectを使用して何が間違っていたのかより具体的なエラーメッセージを提供すべきです。
今や、TokenStreamからの注釈されたRustコードをDeriveInputインスタンスに変換するコードができたので、
Listing 19-33のように、注釈された型にHelloMacroトレイトを実装するコードを生成しましょう:
ファイル名: hello_macro_derive/src/lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;
use quote::quote;
use syn;
#[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 gen = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
gen.into()
}
Listing 19-33: パースされたRustコードを用いてHelloMacroトレイトを実装する
ast.identを使って、注釈された型の名前(識別子)を含むIdent構造体インスタンスを得ています。
Listing 19-32の構造体を見ると、impl_hello_macro関数をListing 19-30のコードに実行したときに私達の得るidentは、フィールドidentの値として"Pancakes"を持つだろうとわかります。
従って、Listing 19-33における変数nameは構造体Identのインスタンスをもちます。このインスタンスは、printされた時は文字列"Pancakes"、即ちListing 19-30の構造体の名前となります。
quote!マクロを使うことで、私達が返したいRustコードを定義することができます。
ただ、コンパイラが期待しているものはquote!マクロの実行結果とはちょっと違うものです。なので、TokenStreamに変換してやる必要があります。
マクロの出力する直接表現を受け取り、必要とされているTokenStream型の値を返すintoメソッドを呼ぶことでこれを行います。
このマクロはまた、非常にかっこいいテンプレート機構も提供してくれます; #nameと書くと、quote!は
それをnameという変数の値と置き換えます。普通のマクロが動作するのと似た繰り返しさえ行えます。
本格的に入門したいなら、quoteクレートのdocをご確認ください。
手続き的マクロには使用者が注釈した型に対してHelloMacroトレイトの実装を生成してほしいですが、
これは#nameを使用することで得られます。トレイトの実装には1つの関数hello_macroがあり、
この本体に提供したい機能が含まれています: Hello, Macro! My name is、そして、注釈した型の名前を出力する機能です。
ここで使用したstringify!マクロは、言語に組み込まれています。1 + 2などのようなRustの式を取り、
コンパイル時に"1 + 2"のような文字列リテラルにその式を変換します。
これは、format!やprintln!のような、式を評価し、そしてその結果をStringに変換するマクロとは異なります。
#name入力が文字通り出力されるべき式という可能性もあるので、stringify!を使用しています。
stringify!を使用すると、コンパイル時に#nameを文字列リテラルに変換することで、メモリ確保しなくても済みます。
この時点で、cargo buildはhello_macroとhello_macro_deriveの両方で成功するはずです。
これらのクレートをリスト19-30のコードにフックして、手続き的マクロが動くところを確認しましょう!
cargo new pancakesであなたのプロジェクトのディレクトリ(訳注:これまでに作った2つのクレート内ではないということ)に新しいバイナリプロジェクトを作成してください。
hello_macroとhello_macro_deriveを依存としてpancakesクレートのCargo.tomlに追加する必要があります。
自分のバージョンのhello_macroとhello_macro_deriveをcrates.io に公開しているなら、
普通の依存になるでしょう; そうでなければ、以下のようにpath依存として指定すればよいです:
[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
リスト19-30のコードをsrc/main.rsに配置し、cargo runを実行してください: Hello, Macro! My name is Pancakesと出力するはずです。
手続き的マクロのHelloMacroトレイトの実装は、pancakesクレートが実装する必要なく、包含されました;
#[derive(HelloMacro)]がトレイトの実装を追加したのです。
続いて、他の種類の手続き的マクロがカスタムのderiveマクロとどのように異なっているか見てみましょう。
属性風マクロ
属性風マクロはカスタムのderiveマクロと似ていますが、derive属性のためのコードを生成するのではなく、新しい属性を作ることができます。
また、属性風マクロはよりフレキシブルでもあります:deriveは構造体とenumにしか使えませんでしたが、属性は関数のような他の要素に対しても使えるのです。
属性風マクロを使った例を以下に示しています:webアプリケーションフレームワークを使っているときに、routeという関数につける属性名があるとします:
#[route(GET, "/")]
fn index() {
この#[route]属性はそのフレームワークによって手続き的マクロとして定義されたものなのでしょう。
マクロを定義する関数のシグネチャは以下のようになっているでしょう:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
ここで、2つTokenStream型の引数がありますね。
1つ目は属性の中身:GET, "/"に対応しており、2つ目は属性が付けられた要素の中身に対応しています。今回だとfn index() {}と関数の本体の残りですね。
それ以外において、属性風マクロはカスタムのderiveマクロと同じ動きをします:
クレートタイプとしてproc-macroを使ってクレートを作り、あなたのほしいコードを生成してくれる関数を実装すればよいです!
関数風マクロ
関数風マクロは、関数呼び出しのように見えるマクロを定義します。
これらは、macro_rules!マクロのように、関数よりフレキシブルです。
たとえば、これらは任意の数の引数を取ることができます。
しかし、一般的なメタプログラミングのためにmacro_rules!で宣言的なマクロで話したように、macro_rules!マクロはmatch風の構文を使ってのみ定義できたのでした。
関数風マクロは引数としてTokenStreamをとり、そのTokenStreamを定義に従って操作します。操作には、他の2つの手続き的マクロと同じように、Rustコードが使われます。
例えば、sql!マクロという関数風マクロで、以下のように呼び出されるものを考えてみましょう:
let sql = sql!(SELECT * FROM posts WHERE id=1);
このマクロは、中に入れられたSQL文をパースし、それが構文的に正しいことを確かめます。これはmacro_rules!マクロが可能とするよりも遥かに複雑な処理です。
sql!マクロは以下のように定義することができるでしょう:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
この定義はカスタムのderiveマクロのシグネチャと似ています:カッコの中のトークンを受け取り、生成したいコードを返すのです。
まとめ
ふう! あなたがいま手にしたRustの機能はあまり頻繁に使うものではありませんが、非常に特殊な状況ではその存在を思い出すことになるでしょう。 たくさんの難しいトピックを紹介しましたが、これは、もしあなたがエラー時の推奨メッセージや他の人のコードでそれらに遭遇した時、その概念と文法を理解できるようになっていてほしいからです。 この章を、解決策にたどり着くためのリファレンスとして活用してください。
次は、この本で話してきたすべてのことを実際に使って、もう一つプロジェクトをやってみましょう!
最後のプロジェクト: マルチスレッドのWebサーバを構築する
長い旅でしたが、本の末端に到達しました。この章では、共にもう一つプロジェクトを構築して最後の方の章で講義した概念の一部をデモしつつ、 それより前の方で学習した内容を思い出してもらいます。
最後のプロジェクトでは、helloと話すWebサーバを作り、Webブラウザでは、図20-1のような見た目になります。

図20-1: 最後の共有されたプロジェクト
こちらがWebサーバを構築するプランです:
- TCPとHTTPについて少し学ぶ。
- ソケットでTCP接続をリッスンする。
- 少量のHTTPリクエストを構文解析する。
- 適切なHTTPレスポンスを生成する。
- スレッドプールでサーバのスループットを強化する。
ですが、取り掛かる前に、ある小さな事実に触れなければなりません: わたしたちがこれから行うやり方は、RustでWebサーバを構築する最善の方法ではないだろうということです。 これから構築するよりもより完全なWebサーバとスレッドプールの実装を提供する製品利用可能な多くのクレートが、 https://crates.io/ で利用可能なのです。
しかしながら、この章での意図は、学習を手助けすることであり、簡単なやり方を選ぶことではありません。 Rustはシステムプログラミング言語なので、取りかかる抽象度を選ぶことができ、 他の言語で可能だったり実践的だったりするよりも低レベルまで行くことができます。一般的な考えと将来使う可能性のあるクレートの背後にある技術を学べるように、 手動で基本的なHTTPサーバとスレッドプールを書きます。
シングルスレッドのWebサーバを構築する
シングルスレッドのWebサーバを動かすところから始めます。始める前に、Webサーバ構築に関係するプロトコルをさっと一覧しましょう。 これらのプロトコルの詳細は、この本の範疇を超えていますが、さっと眺めることで必要な情報が得られるでしょう。
主に2つのプロトコルがWebサーバに関係し、Hypertext Transfer Protocol (HTTP)(注釈: ハイパーテキスト転送プロトコル)と、
Transmission Control Protocol (TCP)(注釈: 伝送制御プロトコル)です。
両者のプロトコルは、リクエスト・レスポンスプロトコルであり、つまり、クライアントがリクエスト(要求)を初期化し、
サーバはリクエストをリッスンし、クライアントにレスポンス(応答)を提供するということです。
それらのリクエストとレスポンスの中身は、プロトコルで規定されています。
TCPは、情報がとあるサーバから別のサーバへどう到達するかの詳細を記述するものの、その情報がなんなのかは指定しない、 より低レベルのプロトコルです。HTTPはリクエストとレスポンスの中身を定義することでTCPの上に成り立っています。 技術的にはHTTPを他のプロトコルとともに使用することができますが、大抵の場合、HTTPはTCPの上にデータを送信します。 TCPとHTTPのリクエストとレスポンスの生のバイトを取り扱います。
TCP接続をリッスンする
WebサーバはTCP接続をリッスンするので、そこが最初に取り掛かる部分になります。標準ライブラリは、
std::netというこれを行うモジュールを用意しています。通常通り、新しいプロジェクトを作りましょう:
$ cargo new hello --bin
Created binary (application) `hello` project
$ cd hello
さて、リスト20-1のコードをsrc/main.rsに入力して始めてください。このコードは、
TCPストリームを受信するため127.0.0.1:7878というアドレスをリッスンします。
入力ストリームを得ると、Connection established!と出力します。
ファイル名: src/main.rs
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!"); } }
リスト20-1: 入力ストリームをリッスンし、ストリームを受け付けた時にメッセージを出力する
TcpListenerにより、アドレス127.0.0.1:7878でTCP接続をリッスンできます。アドレス内で、
コロンの前の区域は、自分のコンピュータを表すIPアドレスで(これはどんなコンピュータでも同じで、
特に著者のコンピュータを表すわけではありません)、7878はポートです。このポートを選択した理由は2つあります:
HTTPは通常このポートで受け付けられることと、7878は電話で“rust”と入力されるからです。
この筋書きでのbind関数は、新しいTcpListenerインスタンスを返すという点でnew関数のような働きをします。
この関数がbindと呼ばれている理由は、ネットワークにおいて、リッスンすべきポートに接続することは、
「ポートに束縛する」(binding to a port)こととして知られているからです。
bind関数はResult<T, E>を返し、束縛が失敗することもあることを示しています。例えば、
ポート80に接続するには管理者権限が必要なので(管理者以外はポート1024以上しかリッスンできません)管理者にならずにポート80に接続を試みたら、
束縛はうまくいかないでしょう。また、別の例として自分のプログラムを2つ同時に立ち上げて2つのプログラムが同じポートをリッスンしたら、
束縛は機能しないでしょう。学習目的のためだけに基本的なサーバを記述しているので、この種のエラーを扱う心配はしません;
その代わり、unwrapを使用してエラーが発生したら、プログラムを停止します。
TcpListenerのincomingメソッドは、一連のストリームを与えるイテレータを返します(具体的には、型TcpStreamのストリーム)。
単独のストリームがクライアント・サーバ間の開かれた接続を表します。接続(connection)は、
クライアントがサーバに接続し、サーバがレスポンスを生成し、サーバが接続を閉じるというリクエストとレスポンス全体の過程の名前です。
そのため、TcpStreamは自身を読み取って、クライアントが送信したことを確認し、それからレスポンスをストリームに記述させてくれます。
総括すると、このforループは各接続を順番に処理し、我々が扱えるように一連のストリームを生成します。
とりあえず、ストリームの扱いは、unwrapを呼び出してストリームにエラーがあった場合にプログラムを停止することから構成されています;
エラーがなければ、プログラムはメッセージを出力します。次のリストで成功した時にさらに多くの機能を追加します。
クライアントがサーバに接続する際にincomingメソッドからエラーを受け取る可能性がある理由は、
実際には接続を走査していないからです。代わりに接続の試行を走査しています。接続は多くの理由で失敗する可能性があり、
そのうちの多くは、OS特有です。例を挙げれば、多くのOSには、サポートできる同時に開いた接続数に上限があります;
開かれた接続の一部が閉じられるまでその数字を超えた接続の試行はエラーになります。
このコードを試しに実行してみましょう!端末でcargo runを呼び出し、それからWebブラウザで127.0.0.1:7878をロードしてください。
ブラウザは、「接続がリセットされました」などのエラーメッセージを表示するはずです。サーバが現状、何もデータを返してこないからです。
ですが、端末に目を向ければ、ブラウザがサーバに接続した際にいくつかメッセージが出力されるのを目の当たりにするはずです。
Running `target/debug/hello`
Connection established!
Connection established!
Connection established!
時々、1回のブラウザリクエストで複数のメッセージが出力されるのを目の当たりにするでしょう; その理由は、ブラウザがページだけでなく、 ブラウザのタブに出現するfavicon.icoアイコンなどの他のリソースにもリクエストを行なっているということかもしれません。
サーバが何もデータを送り返してこないので、ブラウザがサーバに何度も接続を試みているということである可能性もあるでしょう。
streamがスコープを抜け、ループの最後でドロップされると、接続はdrop実装の一部として閉じられます。
ブラウザは、再試行することで閉じられた接続を扱うことがあります。問題が一時的なものである可能性があるからです。
重要な要素は、TCP接続へのハンドルを得ることに成功したということです!
特定のバージョンのコードを走らせ終わった時にctrl-cを押して、
プログラムを止めることを忘れないでください。そして、一連のコード変更を行った後にcargo runを再起動し、
最新のコードを実行していることを確かめてください。
リクエストを読み取る
ブラウザからリクエストを読み取る機能を実装しましょう!まず接続を得、それから接続に対して何らかの行動を行う責任を分離するために、
接続を処理する新しい関数を開始します。この新しいhandle_connection関数において、TCPストリームからデータを読み取り、
ブラウザからデータが送られていることを確認できるように端末に出力します。コードをリスト20-2のように変更してください。
ファイル名: src/main.rs
use std::io::prelude::*; use std::net::TcpStream; 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(); handle_connection(stream); } } fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; stream.read(&mut buffer).unwrap(); println!("Request: {}", String::from_utf8_lossy(&buffer[..])); }
リスト20-2: TcpStreamから読み取り、データを出力する
std::io::preludeをスコープに導入して、ストリームから読み書きさせてくれる特定のトレイトにアクセスできるようにしています。
main関数内のforループで、接続を確立したというメッセージを出力する代わりに、今では、
新しいhandle_connection関数を呼び出し、streamを渡しています。
handle_connection関数において、stream引数を可変にしました。理由は、
TcpStreamインスタンスが内部で返すデータを追いかけているからです。要求した以上のデータを読み取り、
次回データを要求した時のためにそのデータを保存する可能性があります。故に、内部の状態が変化する可能性があるので、
mutにする必要があるのです; 普通、「読み取り」に可変化は必要ないと考えてしまいますが、この場合、mutキーワードが必要です。
次に、実際にストリームから読み取る必要があります。これを2つの手順で行います: まず、
スタックに読み取ったデータを保持するbufferを宣言します。バッファーのサイズは1024バイトにしました。
これは、基本的なリクエストには十分な大きさでこの章の目的には必要十分です。任意のサイズのリクエストを扱いたければ、
バッファーの管理はもっと複雑にする必要があります; 今は、単純に保っておきます。このバッファーをstream.readに渡し、
これがTcpStreamからバイトを読み取ってバッファーに置きます。
2番目にバッファーのバイトを文字列に変換し、その文字列を出力します。String::from_utf8_lossy関数は、
&[u8]を取り、Stringを生成します。名前の“lossy”の箇所は、無効なUTF-8シーケンスを目の当たりにした際のこの関数の振る舞いを示唆しています:
無効なシーケンスを�、U+FFFD REPLACEMENT CHARACTERで置き換えます。
リクエストデータによって埋められなかったバッファーの部分(訳注 バッファーとして1024バイトの領域を用意しているが、リクエストデータは1024バイト存在しないことがほとんどなので変数 buffer の後ろ部分が埋められないまま放置されることを意図していると思われる) は、置換文字が表示される場合があります。
このコードを試しましょう!プログラムを開始してWebブラウザで再度リクエストを送ってください。ブラウザではそれでも、 エラーページが得られるでしょうが、端末のプログラムの出力はこんな感じになっていることに注目してください:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished dev [unoptimized + debuginfo] target(s) in 0.42 secs
Running `target/debug/hello`
Request: GET / HTTP/1.1
Host: 127.0.0.1:7878
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:52.0) Gecko/20100101
Firefox/52.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Upgrade-Insecure-Requests: 1
������������������������������������
ブラウザによって、少し異なる出力になる可能性があります。今やリクエストデータを出力しているので、
Request: GETの後のパスを見ることで1回のブラウザリクエストから複数の接続が得られる理由が確認できます。
繰り返される接続が全て / を要求しているなら、ブラウザは、我々のプログラムからレスポンスが得られないので、
繰り返し / をフェッチしようとしていることがわかります。
このリクエストデータを噛み砕いて、ブラウザが我々のプログラムに何を要求しているかを理解しましょう。
HTTPリクエストを詳しく見る
HTTPはテキストベースのプロトコルで、1つの要求はこのようなフォーマットに則っています:
Method Request-URI HTTP-Version CRLF
headers CRLF
message-body
1行目は、クライアントが要求しているものがなんなのかについての情報を保持するリクエスト行です。
リクエスト行の最初の部分は使用されている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により、リクエスト行がリクエストデータの残りと区別されています。CRLFを出力すると、
\r\nではなく、新しい行が開始されることに注意してください。
ここまでプログラムを実行して受け取ったリクエスト行のデータをみると、GETがメソッド、/ が要求URI、
HTTP/1.1がバージョンであることが確認できます。
リクエスト行の後に、Host:以下から始まる残りの行は、ヘッダです。GETリクエストには、本体(訳注:message-bodyのこと)がありません。
試しに他のブラウザからリクエストを送ったり、127.0.0.1:7878/testなどの異なるアドレスを要求してみて、どうリクエストデータが変わるか確認してください。
さて、ブラウザが要求しているものがわかったので、何かデータを返しましょう!
レスポンスを記述する
さて、クライアントのリクエストに対する返答としてデータの送信を実装します。レスポンスは、以下のようなフォーマットです:
HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body
最初の行は、レスポンスで使用されるHTTPバージョン、リクエストの結果を要約する数値ステータス・コード、そしてステータス・コードのテキスト記述を提供する理由句を含む ステータス行 です。 CRLFシーケンスの後には、任意のヘッダ、別のCRLFシーケンス、そしてレスポンスの本体が続きます。
こちらがHTTPバージョン1.1を使用し、ステータスコードが200で、OKフレーズ、ヘッダと本体なしの例のレスポンスです:
HTTP/1.1 200 OK\r\n\r\n
ステータスコード200は、一般的な成功のレスポンスです。テキストは、矮小な成功のHTTPレスポンスです。
これを成功したリクエストへの返答としてストリームに書き込みましょう!handle_connection関数から、
リクエストデータを出力していたprintln!を除去し、リスト20-3のコードと置き換えてください。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::io::prelude::*; use std::net::TcpStream; fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; stream.read(&mut buffer).unwrap(); let response = "HTTP/1.1 200 OK\r\n\r\n"; stream.write(response.as_bytes()).unwrap(); stream.flush().unwrap(); } }
リスト20-3: ストリームに矮小な成功のHTTPレスポンスを書き込む
新しい最初の行に成功したメッセージのデータを保持するresponse変数を定義しています。そして、
responseに対してas_bytesを呼び出し、文字列データをバイトに変換します。streamのwriteメソッドは、
&[u8]を取り、接続に直接そのバイトを送信します。
write処理は失敗することもあるので、以前のようにエラーの結果にはunwrapを使用します。
今回も、実際のアプリでは、エラー処理をここに追加するでしょう。最後にflushは待機し、
バイトが全て接続に書き込まれるまでプログラムが継続するのを防ぎます; TcpStreamは内部にバッファーを保持して、
元となるOSへの呼び出しを最小化します。
これらの変更とともに、コードを実行し、リクエストをしましょう。最早、端末にどんなデータも出力していないので、 Cargoからの出力以外には何も出力はありません。Webブラウザで127.0.0.1:7878をロードすると、 エラーではなく空のページが得られるはずです。HTTPリクエストとレスポンスを手で実装したばかりなのです!
本物のHTMLを返す
空のページ以上のものを返す機能を実装しましょう。新しいファイルhello.htmlをsrcディレクトリではなく、 プロジェクトのルートディレクトリに作成してください。お好きなようにHTMLを書いてください; リスト20-4は、一つの可能性を示しています。
ファイル名: hello.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<!--
やあ!
-->
<h1>Hello!</h1>
<!--
Rustからやあ
-->
<p>Hi from Rust</p>
</body>
</html>
リスト20-4: レスポンスで返すサンプルのHTMLファイル
これは、ヘッドとテキストのある最低限のHTML5ドキュメントです。リクエストを受け付けた際にこれをサーバから返すには、
リスト20-5のようにhandle_connectionを変更してHTMLファイルを読み込み、本体としてレスポンスに追加して送ります。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::io::prelude::*; use std::net::TcpStream; use std::fs::File; // --snip-- fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; stream.read(&mut buffer).unwrap(); let mut file = File::open("hello.html").unwrap(); let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents); stream.write(response.as_bytes()).unwrap(); stream.flush().unwrap(); } }
リスト20-5: レスポンスの本体としてhello.htmlの中身を送る
先頭に行を追加して標準ライブラリのFileをスコープに導入しました。ファイルを開き、中身を読み込むコードは、
馴染みがあるはずです; リスト12-4でI/Oプロジェクト用にファイルの中身を読み込んだ時に第12章で使用しましたね。
次にformat!でファイルの中身を成功したレスポンスの本体として追記しています。
このコードをcargo runで走らせ、127.0.0.1:7878をブラウザでロードしてください;
HTMLが描画されるのが確認できるはずです!
現時点では、buffer内のリクエストデータは無視し、無条件でHTMLファイルの中身を送り返しているだけです。
これはつまり、ブラウザで127.0.0.1:7878/something-elseをリクエストしても、
この同じHTMLレスポンスが得られるということです。我々のサーバはかなり限定的で、多くのWebサーバとは異なっています。
リクエストに基づいてレスポンスをカスタマイズし、/ への合法なリクエストに対してのみHTMLファイルを送り返したいです。
リクエストにバリデーションをかけ、選択的にレスポンスを返す
現状、このWebサーバはクライアントが何を要求しても、このファイルのHTMLを返します。HTMLファイルを返却する前にブラウザが / をリクエストしているか確認し、
ブラウザが他のものを要求していたらエラーを返す機能を追加しましょう。このために、
handle_connectionをリスト20-6のように変更する必要があります。この新しいコードは、
/ への要求がどんな見た目になるのか我々が知っていることに対して受け取ったリクエストの中身を検査し、ifとelseブロックを追加して、
リクエストを異なる形で扱います。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::io::prelude::*; use std::net::TcpStream; use std::fs::File; // --snip-- fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; stream.read(&mut buffer).unwrap(); let get = b"GET / HTTP/1.1\r\n"; if buffer.starts_with(get) { let mut file = File::open("hello.html").unwrap(); let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); let response = format!("HTTP/1.1 200 OK\r\n\r\n{}", contents); stream.write(response.as_bytes()).unwrap(); stream.flush().unwrap(); } else { // 何か他の要求 // some other request } } }
リスト20-6: リクエストをマッチさせ、/ へのリクエストを他のリクエストとは異なる形で扱う
まず、/ リクエストに対応するデータをget変数にハードコードしています。生のバイトをバッファーに読み込んでいるので、
b""バイト文字列記法を中身のデータの先頭に追記することで、getをバイト文字列に変換しています。
そして、bufferがgetのバイトから始まっているか確認します。もしそうなら、/ への合法なリクエストを受け取ったことを意味し、
これが、HTMLファイルの中身を返すifブロックで扱う成功した場合になります。
bufferがgetのバイトで始まらないのなら、何か他のリクエストを受け取ったことになります。
この後すぐ、elseブロックに他のリクエストに対応するコードを追加します。
さあ、このコードを走らせて127.0.0.1:7878を要求してください; hello.htmlのHTMLが得られるはずです。 127.0.0.1:7878/something-elseなどの他のリクエストを行うと、リスト20-1や20-2のコードを走らせた時に見かけた接続エラーになるでしょう。
では、elseブロックにリスト20-7のコードを追記して、ステータスコード404のレスポンスを返しましょう。
これは、リクエストの中身が見つからなかったことを通知します。エンドユーザへのレスポンスを示し、ページをブラウザに描画するよう、
何かHTMLも返します。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::io::prelude::*; use std::net::TcpStream; use std::fs::File; fn handle_connection(mut stream: TcpStream) { if true { // --snip-- } else { let status_line = "HTTP/1.1 404 NOT FOUND\r\n\r\n"; let mut file = File::open("404.html").unwrap(); let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); let response = format!("{}{}", status_line, contents); stream.write(response.as_bytes()).unwrap(); stream.flush().unwrap(); } } }
リスト20-7: / 以外の何かが要求されたら、ステータスコード404とエラーページで応答する
ここでは、レスポンスにはステータスコード404と理由フレーズNOT FOUNDのステータス行があります。
それでもヘッダは返さず、レスポンスの本体は、ファイル404.htmlのHTMLになります。エラーページのために、
hello.htmlの隣に404.htmlファイルを作成する必要があります; 今回も、ご自由にお好きなHTMLにしたり、
リスト20-8の例のHTMLを使用したりしてください。
ファイル名: 404.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>
リスト20-8: あらゆる404レスポンスでページが送り返す中身のサンプル
これらの変更とともに、もう一度サーバを実行してください。127.0.0.1:7878を要求すると、 hello.htmlの中身が返り、127.0.0.1:7878/fooなどの他のリクエストには404.htmlからのエラーHTMLが返るはずです。
リファクタリングの触り
現在、ifとelseブロックには多くの繰り返しがあります: どちらもファイルを読み、ファイルの中身をストリームに書き込んでいます。
唯一の違いは、ステータス行とファイル名だけです。それらの差異を、ステータス行とファイル名の値を変数に代入する個別のifとelse行に引っ張り出して、
コードをより簡潔にしましょう; そうしたら、それらの変数を無条件にコードで使用し、ファイルを読んでレスポンスを書き込めます。
リスト20-9は、大きなifとelseブロックを置き換えた後の結果のコードを示しています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::io::prelude::*; use std::net::TcpStream; use std::fs::File; // --snip-- fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; stream.read(&mut buffer).unwrap(); let get = b"GET / HTTP/1.1\r\n"; // --snip-- let (status_line, filename) = if buffer.starts_with(get) { ("HTTP/1.1 200 OK\r\n\r\n", "hello.html") } else { ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html") }; let mut file = File::open(filename).unwrap(); let mut contents = String::new(); file.read_to_string(&mut contents).unwrap(); let response = format!("{}{}", status_line, contents); stream.write(response.as_bytes()).unwrap(); stream.flush().unwrap(); } }
リスト20-9: 2つの場合で異なるコードだけを含むように、ifとelseブロックをリファクタリングする
これで、ifとelseブロックは、タプルにステータス行とファイル名の適切な値を返すだけになりました;
それから、分配を使用してこれら2つの値を第18章で議論したように、let文のパターンでstatus_lineとfilenameに代入しています。
前は重複していたコードは、今ではifとelseブロックの外に出て、status_lineとfilename変数を使用しています。
これにより、2つの場合の違いがわかりやすくなり、ファイル読み取りとレスポンス記述の動作法を変更したくなった際に、
1箇所だけコードを更新すればいいようになったことを意味します。リスト20-9のコードの振る舞いは、
リスト20-8と同じです。
素晴らしい!もう、およそ40行のRustコードで、あるリクエストには中身のあるページで応答し、 他のあらゆるリクエストには404レスポンスで応答する単純なWebサーバができました。
現状、このサーバは、シングルスレッドで実行されます。つまり、1回に1つのリクエストしか捌けないということです。 何か遅いリクエストをシミュレーションすることで、それが問題になる可能性を調査しましょう。 それから1度にサーバが複数のリクエストを扱えるように修正します。
シングルスレッドサーバをマルチスレッド化する
現状、サーバはリクエストを順番に処理します。つまり、最初の接続が処理し終わるまで、2番目の接続は処理しないということです。 サーバが受け付けるリクエストの量が増えるほど、この連続的な実行は、最適ではなくなるでしょう。 サーバが処理するのに長い時間がかかるリクエストを受け付けたら、新しいリクエストは迅速に処理できても、 続くリクエストは長いリクエストが完了するまで待たなければならなくなるでしょう。これを修正する必要がありますが、 まずは、実際に問題が起こっているところを見ます。
現在のサーバの実装で遅いリクエストをシミュレーションする
処理が遅いリクエストが現在のサーバ実装に対して行われる他のリクエストにどう影響するかに目を向けます。 リスト20-10は、応答する前に5秒サーバをスリープさせる遅いレスポンスをシミュレーションした /sleepへのリクエストを扱う実装です。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; use std::io::prelude::*; use std::net::TcpStream; use std::fs::File; // --snip-- fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; stream.read(&mut buffer).unwrap(); // --snip-- let get = b"GET / HTTP/1.1\r\n"; let sleep = b"GET /sleep HTTP/1.1\r\n"; let (status_line, filename) = if buffer.starts_with(get) { ("HTTP/1.1 200 OK\r\n\r\n", "hello.html") } else if buffer.starts_with(sleep) { thread::sleep(Duration::from_secs(5)); ("HTTP/1.1 200 OK\r\n\r\n", "hello.html") } else { ("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html") }; // --snip-- } }
リスト20-10: /sleepを認識して5秒間スリープすることで遅いリクエストをシミュレーションする
このコードはちょっと汚いですが、シミュレーション目的には十分です。2番目のリクエストsleepを作成し、
そのデータをサーバは認識します。ifブロックの後にelse ifを追加し、/sleepへのリクエストを確認しています。
そのリクエストが受け付けられると、サーバは成功のHTMLページを描画する前に5秒間スリープします。
我々のサーバがどれだけ基礎的か見て取れます: 本物のライブラリは、もっと冗長でない方法で複数のリクエストの認識を扱うでしょう!
cargo runでサーバを開始してください。それから2つブラウザのウインドウを開いてください: 1つは、
http://localhost:7878/ 用、そしてもう1つはhttp://localhost:7878/sleep 用です。
以前のように / URIを数回入力したら、素早く応答するでしょう。しかし、/sleepを入力し、それから / をロードしたら、
sleepがロードする前にきっかり5秒スリープし終わるまで、/ は待機するのを目撃するでしょう。
より多くのリクエストが遅いリクエストの背後に回ってしまうのを回避するようWebサーバが動く方法を変える方法は複数あります; これから実装するのは、スレッドプールです。
スレッドプールでスループットを向上させる
スレッドプールは、待機し、タスクを処理する準備のできた一塊りの大量に生成されたスレッドです。 プログラムが新しいタスクを受け取ったら、プールのスレッドのどれかをタスクにあてがい、 そのスレッドがそのタスクを処理します。 プールの残りのスレッドは、最初のスレッドが処理中にやってくる他のあらゆるタスクを扱うために利用可能です。 最初のスレッドがタスクの処理を完了したら、アイドル状態のスレッドプールに戻り、新しいタスクを処理する準備ができます。 スレッドプールにより、並行で接続を処理でき、サーバのスループットを向上させます。
プール内のスレッド数は、小さい数字に制限し、DoS(Denial of Service; サービスの拒否)攻撃から保護します; リクエストが来た度に新しいスレッドをプログラムに生成させたら、 1000万リクエストをサーバに行う誰かが、サーバのリソースを使い尽くし、リクエストの処理を停止に追い込むことで、 大混乱を招くことができてしまうでしょう。
無制限にスレッドを大量生産するのではなく、プールに固定された数のスレッドを待機させます。リクエストが来る度に、
処理するためにプールに送られます。プールは、やって来るリクエストのキューを管理します。
プールの各スレッドがこのキューからリクエストを取り出し、リクエストを処理し、そして、別のリクエストをキューに要求します。
この設計により、Nリクエストを並行して処理でき、ここでNはスレッド数です。各スレッドが実行に時間のかかるリクエストに応答していたら、
続くリクエストはそれでも、キュー内で待機させられてしまうこともありますが、その地点に到達する前に扱える時間のかかるリクエスト数を増加させました。
このテクニックは、Webサーバのスループットを向上させる多くの方法の1つに過ぎません。探究する可能性のある他の選択肢は、 fork/joinモデルと、シングルスレッドの非同期I/Oモデルです。この話題にご興味があれば、他の解決策についてもっと読み、 Rustで実装を試みることができます; Rustのような低レベル言語であれば、これらの選択肢全部が可能なのです。
スレッドプールを実装し始める前に、プールを使うのはどんな感じになるはずなのかについて語りましょう。コードの設計を試みる際、 クライアントのインターフェイスをまず書くことは、設計を導く手助けになることがあります。呼び出したいように構成されるよう、 コードのAPIを記述してください; そして、機能を実装してから公開APIの設計をするのではなく、その構造内で機能を実装してください。
第12章のプロジェクトでTDDを使用したように、ここではCompiler Driven Development(コンパイラ駆動開発)を使用します。 欲しい関数を呼び出すコードを書き、それからコンパイラの出すエラーを見てコードが動くように次に何を変更すべきかを決定します。
各リクエストに対してスレッドを立ち上げられる場合のコードの構造
まず、全接続に対して新しいスレッドを確かに生成した場合にコードがどんな見た目になるかを探究しましょう。
先ほど述べたように、無制限にスレッドを大量生産する可能性があるという問題のため、これは最終的な計画ではありませんが、
開始点です。リスト20-11は、新しいスレッドを立ち上げてforループ内で各ストリームを扱うためにmainに行う変更を示しています。
ファイル名: src/main.rs
use std::thread; use std::io::prelude::*; use std::net::TcpListener; use std::net::TcpStream; 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) {}
リスト20-11: 各ストリームに対して新しいスレッドを立ち上げる
第16章で学んだように、thread::spawnは新しいスレッドを生成し、それからクロージャ内のコードを新しいスレッドで実行します。
このコードを実行してブラウザで /sleepをロードし、それからもう2つのブラウザのタブで / をロードしたら、
確かに / へのリクエストは、/sleepが完了するのを待機しなくても済むことがわかるでしょう。
ですが、前述したように、無制限にスレッドを生成することになるので、これは最終的にシステムを参らせてしまうでしょう。
有限数のスレッド用に似たインターフェイスを作成する
スレッドからスレッドプールへの変更にAPIを使用するコードへの大きな変更が必要ないように、
スレッドプールには似た、馴染み深い方法で動作してほしいです。リスト20-12は、
thread::spawnの代わりに使用したいThreadPool構造体の架空のインターフェイスを表示しています。
ファイル名: src/main.rs
use std::thread; use std::io::prelude::*; use std::net::TcpListener; use std::net::TcpStream; struct ThreadPool; impl ThreadPool { fn new(size: u32) -> ThreadPool { ThreadPool } fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static {} } 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) {}
リスト20-12: ThreadPoolの理想的なインターフェイス
ThreadPool::newを使用して設定可能なスレッド数で新しいスレッドプールを作成し、今回の場合は4です。
それからforループ内で、pool.executeは、プールが各ストリームに対して実行すべきクロージャを受け取るという点で、
thread::spawnと似たインターフェイスです。pool.executeを実装する必要があるので、
これはクロージャを取り、実行するためにプール内のスレッドに与えます。このコードはまだコンパイルできませんが、
コンパイラがどう修正したらいいかガイドできるように試してみます。
コンパイラ駆動開発でThreadPool構造体を構築する
リスト20-12の変更をsrc/main.rsに行い、それから開発を駆動するためにcargo checkからのコンパイラエラーを活用しましょう。
こちらが得られる最初のエラーです:
$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
error[E0433]: failed to resolve. Use of undeclared type or module `ThreadPool`
(エラー: 解決に失敗しました。未定義の型またはモジュール`ThreadPool`を使用しています)
--> src\main.rs:10:16
|
10 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^^^^^^ Use of undeclared type or module
`ThreadPool`
error: aborting due to previous error
よろしい!このエラーはThreadPool型かモジュールが必要なことを教えてくれているので、今構築します。
ThreadPoolの実装は、Webサーバが行う仕事の種類とは独立しています。従って、helloクレートをバイナリクレートからライブラリクレートに切り替え、
ThreadPoolの実装を保持させましょう。ライブラリクレートに変更後、
個別のスレッドプールライブラリをWebリクエストを提供するためだけではなく、スレッドプールでしたいあらゆる作業にも使用できます。
以下を含むsrc/lib.rsを生成してください。これは、現状存在できる最も単純なThreadPoolの定義です:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct ThreadPool; }
それから新しいディレクトリ、src/binを作成し、src/main.rsに根付くバイナリクレートをsrc/bin/main.rsに移動してください。
そうすると、ライブラリクレートがhelloディレクトリ内で主要クレートになります; それでも、
cargo runでsrc/bin/main.rsのバイナリを実行することはできます。main.rsファイルを移動後、
編集してライブラリクレートを持ち込み、以下のコードをsrc/bin/main.rsの先頭に追記してThreadPoolをスコープに導入してください:
ファイル名: src/bin/main.rs
extern crate hello;
use hello::ThreadPool;
このコードはまだ動きませんが、再度それを確認して扱う必要のある次のエラーを手に入れましょう:
$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
error[E0599]: no function or associated item named `new` found for type
`hello::ThreadPool` in the current scope
(エラー: 現在のスコープで型`hello::ThreadPool`の関数または関連アイテムに`new`というものが見つかりません)
--> src/bin/main.rs:13:16
|
13 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^^^^^^ function or associated item not found in
`hello::ThreadPool`
このエラーは、次に、ThreadPoolに対してnewという関連関数を作成する必要があることを示唆しています。
また、newには4を引数として受け入れる引数1つがあり、ThreadPoolインスタンスを返すべきということも知っています。
それらの特徴を持つ最も単純なnew関数を実装しましょう:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct ThreadPool; impl ThreadPool { pub fn new(size: usize) -> ThreadPool { ThreadPool } } }
size引数の型として、usizeを選択しました。何故なら、マイナスのスレッド数は、何も筋が通らないことを知っているからです。
また、この4をスレッドのコレクションの要素数として使用し、第3章の「整数型」節で議論したように、これはusizeのあるべき姿であることも知っています。
コードを再度確認しましょう:
$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
warning: unused variable: `size`
(警告: 未使用の変数: `size`)
--> src/lib.rs:4:16
|
4 | pub fn new(size: usize) -> ThreadPool {
| ^^^^
|
= note: #[warn(unused_variables)] on by default
= note: to avoid this warning, consider using `_size` instead
error[E0599]: no method named `execute` found for type `hello::ThreadPool` in the current scope
--> src/bin/main.rs:18:14
|
18 | pool.execute(|| {
| ^^^^^^^
今度は、警告とエラーが出ました。一時的に警告は無視して、ThreadPoolにexecuteメソッドがないためにエラーが発生しました。
「有限数のスレッド用に似たインターフェイスを作成する」節で我々のスレッドプールは、
thread::spawnと似たインターフェイスにするべきと決定したことを思い出してください。
さらに、execute関数を実装するので、与えられたクロージャを取り、実行するようにプールの待機中のスレッドに渡します。
ThreadPoolにexecuteメソッドをクロージャを引数として受け取るように定義します。
第13章の「ジェネリック引数とFnトレイトを使用してクロージャを保存する」節から、
3つの異なるトレイトでクロージャを引数として取ることができることを思い出してください: Fn、FnMut、FnOnceです。
ここでは、どの種類のクロージャを使用するか決定する必要があります。最終的には、
標準ライブラリのthread::spawn実装に似たことをすることがわかっているので、
thread::spawnのシグニチャで引数にどんな境界があるか見ることができます。ドキュメンテーションは、以下のものを示しています:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static
F型引数がここで関心のあるものです; T型引数は戻り値と関係があり、関心はありません。spawnは、
Fのトレイト境界としてFnOnceを使用していることが確認できます。これはおそらく、我々が欲しているものでもあるでしょう。
というのも、最終的にはexecuteで得た引数をspawnに渡すからです。さらにFnOnceは使用したいトレイトであると自信を持つことができます。
リクエストを実行するスレッドは、そのリクエストのクロージャを1回だけ実行し、これはFnOnceのOnceに合致するからです。
F型引数にはまた、トレイト境界のSendとライフタイム境界の'staticもあり、この状況では有用です:
あるスレッドから別のスレッドにクロージャを移動するのにSendが必要で、スレッドの実行にどれくらいかかるかわからないので、
'staticも必要です。ThreadPoolにこれらの境界のジェネリックな型Fの引数を取るexecuteメソッドを生成しましょう:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct ThreadPool; impl ThreadPool { // --snip-- pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static { } } }
それでも、FnOnceの後に()を使用しています。このFnOnceは引数を取らず、値も返さないクロージャを表すからです。
関数定義同様に、戻り値の型はシグニチャから省略できますが、引数がなくても、カッコは必要です。
またもや、これがexecuteメソッドの最も単純な実装です: 何もしませんが、
コードがコンパイルできるようにしようとしているだけです。再確認しましょう:
$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
warning: unused variable: `size`
--> src/lib.rs:4:16
|
4 | pub fn new(size: usize) -> ThreadPool {
| ^^^^
|
= note: #[warn(unused_variables)] on by default
= note: to avoid this warning, consider using `_size` instead
warning: unused variable: `f`
--> src/lib.rs:8:30
|
8 | pub fn execute<F>(&self, f: F)
| ^
|
= note: to avoid this warning, consider using `_f` instead
これで警告を受け取るだけになり、コンパイルできるようになりました!しかし、cargo runを試して、
ブラウザでリクエストを行うと、章の冒頭で見かけたエラーがブラウザに現れることに注意してください。
ライブラリは、まだ実際にexecuteに渡されたクロージャを呼び出していないのです!
注釈: HaskellやRustなどの厳密なコンパイラがある言語についての格言として「コードがコンパイルできたら、 動作する」というものをお聴きになったことがある可能性があります。ですが、この格言は普遍的に当てはまるものではありません。 このプロジェクトはコンパイルできますが、全く何もしません!本物の完璧なプロジェクトを構築しようとしているのなら、 ここが単体テストを書き始めて、コードがコンパイルでき、かつ欲しい振る舞いを保持していることを確認するのに良い機会でしょう。
newでスレッド数を検査する
newとexecuteの引数で何もしていないので、警告が出続けます。欲しい振る舞いでこれらの関数の本体を実装しましょう。
まずはじめに、newを考えましょう。先刻、size引数に非負整数型を選択しました。負のスレッド数のプールは、
全く道理が通らないからです。しかしながら、0スレッドのプールも全く意味がわかりませんが、0も完全に合法なusizeです。
ThreadPoolインスタンスを返す前にsizeが0よりも大きいことを確認するコードを追加し、リスト20-13に示したように、
assert!マクロを使用することで0を受け取った時にプログラムをパニックさせます。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct ThreadPool; impl ThreadPool { /// 新しいThreadPoolを生成する。 /// /// sizeがプールのスレッド数です。 /// /// # パニック /// /// sizeが0なら、`new`関数はパニックします。 /// /// 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-- } }
リスト20-13: ThreadPool::newを実装してsizeが0ならパニックする
doc commentでThreadPoolにドキュメンテーションを追加しました。第14章で議論したように、
関数がパニックすることもある場面を声高に叫ぶセクションを追加することで、
いいドキュメンテーションの実践に倣っていることに注意してください。
試しにcargo doc --openを実行し、ThreadPool構造体をクリックして、newの生成されるドキュメンテーションがどんな見た目か確かめてください!
ここでしたようにassert!マクロを追加する代わりに、リスト12-9のI/OプロジェクトのConfig::newのように、
newにResultを返させることもできるでしょう。しかし、今回の場合、スレッドなしでスレッドプールを作成しようとするのは、
回復不能なエラーであるべきと決定しました。野心を感じるのなら、以下のシグニチャのnewも書いてみて、両者を比較してみてください:
pub fn new(size: usize) -> Result<ThreadPool, PoolCreationError> {
スレッドを格納するスペースを生成する
今や、プールに格納する合法なスレッド数を知る方法ができたので、ThreadPool構造体を返す前にスレッドを作成して格納できます。
ですが、どのようにスレッドを「格納」するのでしょうか?もう一度、thread::spawnシグニチャを眺めてみましょう:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T + Send + 'static,
T: Send + 'static
spawn関数は、JoinHandle<T>を返し、ここでTは、クロージャが返す型です。試しに同じようにJoinHandleを使ってみて、
どうなるか見てみましょう。我々の場合、スレッドプールに渡すクロージャは接続を扱い、何も返さないので、
Tはユニット型()になるでしょう。
リスト20-14のコードはコンパイルできますが、まだスレッドは何も生成しません。ThreadPoolの定義を変更して、
thread::JoinHandle<()>インスタンスのベクタを保持し、sizeキャパシティのベクタを初期化し、
スレッドを生成する何らかのコードを実行するforループを設定し、それらを含むThreadPoolインスタンスを返します。
ファイル名: src/lib.rs
use std::thread;
pub struct ThreadPool {
threads: Vec<thread::JoinHandle<()>>,
}
impl ThreadPool {
// --snip--
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--
}
リスト20-14: ThreadPoolにスレッドを保持するベクタを生成する
ライブラリクレート内でstd::threadをスコープに導入しました。ThreadPoolのベクタの要素の型として、
thread::JoinHandleを使用しているからです。
一旦、合法なサイズを受け取ったら、ThreadPoolはsize個の要素を保持できる新しいベクタを生成します。
この本ではまだ、with_capacity関数を使用したことがありませんが、これはVec::newと同じ作業をしつつ、
重要な違いがあります: ベクタに予めスペースを確保しておくのです。ベクタにsize個の要素を格納する必要があることはわかっているので、
このメモリ確保を前もってしておくと、Vec::newよりも少しだけ効率的になります。Vec::newは、
要素が挿入されるにつれて、自身のサイズを変更します。
再びcargo checkを実行すると、もういくつか警告が出るものの、成功するはずです。
ThreadPoolからスレッドにコードを送信する責任を負うWorker構造体
リスト20-14のforループにスレッドの生成に関するコメントを残しました。ここでは、実際にスレッドを生成する方法に目を向けます。
標準ライブラリはスレッドを生成する手段としてthread::spawnを提供し、thread::spawnは、
生成されるとすぐにスレッドが実行すべき何らかのコードを得ることを予期します。ところが、我々の場合、
スレッドを生成して、後ほど送信するコードを待機してほしいです。標準ライブラリのスレッドの実装は、
それをするいかなる方法も含んでいません; それを手動で実装しなければなりません。
この新しい振る舞いを管理するスレッドとThreadPool間に新しいデータ構造を導入することでこの振る舞いを実装します。
このデータ構造をWorkerと呼び、プール実装では一般的な用語です。レストランのキッチンで働く人々を思い浮かべてください:
労働者は、お客さんからオーダーが来るまで待機し、それからそれらのオーダーを取り、満たすことに責任を負います。
スレッドプールにJoinHandle<()>インスタンスのベクタを格納する代わりに、Worker構造体のインスタンスを格納します。
各Workerが単独のJoinHandle<()>インスタンスを格納します。そして、Workerに実行するコードのクロージャを取り、
既に走っているスレッドに実行してもらうために送信するメソッドを実装します。ログを取ったり、デバッグする際にプールの異なるワーカーを区別できるように、
各ワーカーにidも付与します。
ThreadPoolを生成する際に発生することに以下の変更を加えましょう。このようにWorkerをセットアップした後に、
スレッドにクロージャを送信するコードを実装します:
idとJoinHandle<()>を保持するWorker構造体を定義する。ThreadPoolを変更し、Workerインスタンスのベクタを保持する。id番号を取り、idと空のクロージャで大量生産されるスレッドを保持するWorkerインスタンスを返すWorker::new関数を定義する。ThreadPool::newでforループカウンタを使用してidを生成し、そのidで新しいWorkerを生成し、ベクタにワーカーを格納する。
挑戦に積極的ならば、リスト20-15のコードを見る前にご自身でこれらの変更を実装してみてください。
いいですか?こちらが先ほどの変更を行う1つの方法を行ったリスト20-15です。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::thread; pub struct ThreadPool { workers: Vec<Worker>, } impl ThreadPool { // --snip-- 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-- } struct Worker { id: usize, thread: thread::JoinHandle<()>, } impl Worker { fn new(id: usize) -> Worker { let thread = thread::spawn(|| {}); Worker { id, thread, } } } }
リスト20-15: ThreadPoolを変更してスレッドを直接保持するのではなく、Workerインスタンスを保持する
ThreadPoolのフィールド名をthreadsからworkersに変更しました。JoinHandle<()>インスタンスではなく、
Workerインスタンスを保持するようになったからです。forループのカウンタをWorker::newへの引数として使用し、
それぞれの新しいWorkerをworkersというベクタに格納します。
外部のコード(src/bin/main.rsのサーバなど)は、ThreadPool内でWorker構造体を使用していることに関する実装の詳細を知る必要はないので、
Worker構造体とそのnew関数は非公開にしています。Worker::new関数は与えたidを使用し、
空のクロージャを使って新しいスレッドを立ち上げることで生成されるJoinHandle<()>インスタンスを格納します。
このコードはコンパイルでき、ThreadPool::newへの引数として指定した数のWorkerインスタンスを格納します。
ですがそれでも、executeで得るクロージャを処理してはいません。次は、それをする方法に目を向けましょう。
チャンネル経由でスレッドにリクエストを送信する
さて、thread::spawnに与えられたクロージャが全く何もしないという問題に取り組みましょう。現在、
executeメソッドで実行したいクロージャを得ています。ですが、ThreadPoolの生成中、Workerそれぞれを生成する際に、
実行するクロージャをthread::spawnに与える必要があります。
作ったばかりのWorker構造体にThreadPoolが保持するキューから実行するコードをフェッチして、
そのコードをスレッドが実行できるように送信してほしいです。
第16章でこのユースケースにぴったりであろうチャンネル(2スレッド間コミュニケーションをとる単純な方法)について学びました。
チャンネルをキューの仕事として機能させ、executeはThreadPoolからWorkerインスタンスに仕事を送り、
これが仕事をスレッドに送信します。こちらが計画です:
ThreadPoolはチャンネルを生成し、チャンネルの送信側に就く。Workerそれぞれは、チャンネルの受信側に就く。- チャンネルに送信したいクロージャを保持する新しい
Job構造体を生成する。 executeメソッドは、実行したい仕事をチャンネルの送信側に送信する。- スレッド内で、
Workerはチャンネルの受信側をループし、受け取ったあらゆる仕事のクロージャを実行する。
ThreadPool::new内でチャンネルを生成し、ThreadPoolインスタンスに送信側を保持することから始めましょう。リスト20-16のようにですね。
今の所、Job構造体は何も保持しませんが、チャンネルに送信する種類の要素になります。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::thread; // --snip-- use std::sync::mpsc; pub struct ThreadPool { workers: Vec<Worker>, sender: mpsc::Sender<Job>, } struct Job; impl ThreadPool { // --snip-- 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-- } struct Worker { id: usize, thread: thread::JoinHandle<()>, } impl Worker { fn new(id: usize) -> Worker { let thread = thread::spawn(|| {}); Worker { id, thread, } } } }
リスト20-18: ThreadPoolを変更してJobインスタンスを送信するチャンネルの送信側を格納する
ThreadPool::new内で新しいチャンネルを生成し、プールに送信側を保持させています。これはコンパイルに成功しますが、
まだ警告があります。
スレッドプールがワーカーを生成する際に各ワーカーにチャンネルの受信側を試しに渡してみましょう。
受信側はワーカーが大量生産するスレッド内で使用したいことがわかっているので、クロージャ内でreceiver引数を参照します。
リスト20-17のコードはまだ完璧にはコンパイルできません。
ファイル名: src/lib.rs
impl ThreadPool {
// --snip--
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--
}
// --snip--
impl Worker {
fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
let thread = thread::spawn(|| {
receiver;
});
Worker {
id,
thread,
}
}
}
リスト20-17: チャンネルの受信側をワーカーに渡す
多少些細で単純な変更を行いました: チャンネルの受信側をWorker::newに渡し、それからクロージャの内側で使用しています。
このコードのチェックを試みると、このようなエラーが出ます:
$ cargo check
Compiling hello v0.1.0 (file:///projects/hello)
error[E0382]: use of moved value: `receiver`
--> src/lib.rs:27:42
|
27 | workers.push(Worker::new(id, receiver));
| ^^^^^^^^ value moved here in
previous iteration of loop
|
= note: move occurs because `receiver` has type
`std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
このコードは、receiverを複数のWorkerインスタンスに渡そうとしています。第16章を思い出すように、これは動作しません:
Rustが提供するチャンネル実装は、複数の生成者、単独の消費者です。要するに、
チャンネルの消費側をクローンするだけでこのコードを修正することはできません。たとえできたとしても、
使用したいテクニックではありません; 代わりに、全ワーカー間で単独のreceiverを共有することで、
スレッド間に仕事を分配したいです。
さらに、チャンネルキューから仕事を取り出すことは、receiverを可変化することに関連するので、
スレッドには、receiverを共有して変更する安全な方法が必要です; さもなくば、
競合状態に陥る可能性があります(第16章で講義しました)。
第16章で議論したスレッド安全なスマートポインタを思い出してください: 複数のスレッドで所有権を共有しつつ、
スレッドに値を可変化させるためには、Arc<Mutex<T>>を使用する必要があります。Arc型は、
複数のワーカーに受信者を所有させ、Mutexにより、1度に受信者から1つの仕事をたった1つのワーカーが受け取ることを保証します。
リスト20-18は、行う必要のある変更を示しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::thread; use std::sync::mpsc; use std::sync::Arc; use std::sync::Mutex; // --snip-- pub struct ThreadPool { workers: Vec<Worker>, sender: mpsc::Sender<Job>, } struct Job; impl ThreadPool { // --snip-- 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-- } 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, } } } }
リスト20-18: ArcとMutexを使用してワーカー間でチャンネルの受信側を共有する
ThreadPool::newで、チャンネルの受信側をArcとMutexに置いています。新しいワーカーそれぞれに対して、
Arcをクローンして参照カウントを跳ね上げているので、ワーカーは受信側の所有権を共有することができます。
これらの変更でコードはコンパイルできます!ゴールはもうすぐそこです!
executeメソッドを実装する
最後にThreadPoolにexecuteメソッドを実装しましょう。
Jobも構造体からexecuteが受け取るクロージャの型を保持するトレイトオブジェクトの型エイリアスに変更します。
第19章の「型エイリアスで型同義語を生成する」節で議論したように、型エイリアスにより長い型を短くできます。
リスト20-19をご覧ください。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { // --snip-- pub struct ThreadPool { workers: Vec<Worker>, sender: mpsc::Sender<Job>, } use std::sync::mpsc; struct Worker {} type Job = Box<FnOnce() + Send + 'static>; impl ThreadPool { // --snip-- pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static { let job = Box::new(f); self.sender.send(job).unwrap(); } } // --snip-- }
リスト20-19: 各クロージャを保持するBoxに対してJob型エイリアスを生成し、それからチャンネルに仕事を送信する
executeで得たクロージャを使用して新しいJobインスタンスを生成した後、その仕事をチャンネルの送信側に送信しています。
送信が失敗した時のためにsendに対してunwrapを呼び出しています。これは例えば、全スレッドの実行を停止させるなど、
受信側が新しいメッセージを受け取るのをやめてしまったときなどに起こる可能性があります。現時点では、
スレッドの実行を止めることはできません: スレッドは、プールが存在する限り実行し続けます。
unwrapを使用している理由は、失敗する場合が起こらないとわかっているからですが、コンパイラにはわかりません。
ですが、まだやり終えたわけではありませんよ!ワーカー内でthread::spawnに渡されているクロージャは、
それでもチャンネルの受信側を参照しているだけです。その代わりに、クロージャには永遠にループし、
チャンネルの受信側に仕事を要求し、仕事を得たらその仕事を実行してもらう必要があります。
リスト20-20に示した変更をWorker::newに行いましょう。
ファイル名: src/lib.rs
// --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 {} got a job; executing.", id);
(*job)();
}
});
Worker {
id,
thread,
}
}
}
リスト20-20: ワーカーのスレッドで仕事を受け取り、実行する
ここで、まずreceiverに対してlockを呼び出してミューテックスを獲得し、それからunwrapを呼び出して、
エラーの際にはパニックします。ロックの獲得は、ミューテックスが毒された状態なら失敗する可能性があり、
これは、他のどれかのスレッドがロックを保持している間に、解放するのではなく、パニックした場合に起き得ます。
この場面では、unwrapを呼び出してこのスレッドをパニックさせるのは、取るべき正当な行動です。
このunwrapをあなたにとって意味のあるエラーメッセージを伴うexpectに変更することは、ご自由に行なってください。
ミューテックスのロックを獲得できたら、recvを呼び出してチャンネルからJobを受け取ります。
最後のunwrapもここであらゆるエラーを超えていき、これはチャンネルの送信側を保持するスレッドが閉じた場合に発生する可能性があり、
受信側が閉じた場合にsendメソッドがErrを返すのと似ています。
recvの呼び出しはブロックするので、まだ仕事がなければ、現在のスレッドは、仕事が利用可能になるまで待機します。
Mutex<T>により、ただ1つのWorkerスレッドのみが一度に仕事の要求を試みることを保証します。
理論的には、このコードはコンパイルできるはずです。残念ながら、Rustコンパイラはまだ完全ではなく、 このようなエラーが出ます:
error[E0161]: cannot move a value of type std::ops::FnOnce() +
std::marker::Send: the size of std::ops::FnOnce() + std::marker::Send cannot be
statically determined
(エラー: std::ops::FnOnce() + std::marker::Sendの値をムーブできません:
std::ops::FnOnce() + std::marker::Sendのサイズを静的に決定できません)
--> src/lib.rs:63:17
|
63 | (*job)();
| ^^^^^^
問題が非常に謎めいているので、エラーも非常に謎めいています。Box<T>に格納されたFnOnceクロージャを呼び出すためには(Job型エイリアスがそう)、
呼び出す際にクロージャがselfの所有権を奪うので、
クロージャは自身をBox<T>からムーブする必要があります。一般的に、RustはBox<T>から値をムーブすることを許可しません。
コンパイラには、Box<T>の内側の値がどれほどの大きさなのか見当がつかないからです:
第15章でBox<T>に格納して既知のサイズの値を得たい未知のサイズの何かがあるためにBox<T>を正確に使用したことを思い出してください。
リスト17-15で見かけたように、記法self: Box<Self>を使用するメソッドを書くことができ、
これにより、メソッドはBox<T>に格納されたSelf値の所有権を奪うことができます。
それがまさしくここで行いたいことですが、残念ながらコンパイラはさせてくれません:
クロージャが呼び出された際に振る舞いを実装するRustの一部は、self: Box<Self>を使用して実装されていないのです。
故に、コンパイラはまだこの場面においてself: Box<Self>を使用してクロージャの所有権を奪い、
クロージャをBox<T>からムーブできることを理解していないのです。
Rustはまだコンパイラの改善途上にあり、リスト20-20のコードは、 将来的にうまく動くようになるべきです。まさしくあなたのような方がこれや他の問題を修正しています!この本を完了したら、 是非ともあなたにも参加していただきたいです。
ですがとりあえず、手頃なトリックを使ってこの問題を回避しましょう。この場合、self: Box<Self>で、
Box<T>の内部の値の所有権を奪うことができることをコンパイラに明示的に教えてあげます;
そして、一旦クロージャの所有権を得たら、呼び出せます。これには、
シグニチャにself: Box<Self>を使用するcall_boxというメソッドのある新しいトレイトFnBoxを定義すること、
FnOnce()を実装する任意の型に対してFnBoxを定義すること、型エイリアスを新しいトレイトを使用するように変更すること、
Workerをcall_boxメソッドを使用するように変更することが関連します。これらの変更は、
リスト20-21に表示されています。
ファイル名: src/lib.rs
trait FnBox {
fn call_box(self: Box<Self>);
}
impl<F: FnOnce()> FnBox for F {
fn call_box(self: Box<F>) {
(*self)()
}
}
type Job = Box<FnBox + Send + 'static>;
// --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 {} got a job; executing.", id);
job.call_box();
}
});
Worker {
id,
thread,
}
}
}
リスト20-21: 新しいトレイトFnBoxを追加してBox<FnOnce()>の現在の制限を回避する
まず、FnBoxという新しいトレイトを作成します。このトレイトにはcall_boxという1つのメソッドがあり、
これは、self: Box<Self>を取ってselfの所有権を奪い、Box<T>から値をムーブする点を除いて、
他のFn*トレイトのcallメソッドと類似しています。
次に、FnOnce()トレイトを実装する任意の型Fに対してFnBoxトレイトを実装します。実質的にこれは、
あらゆるFnOnce()クロージャがcall_boxメソッドを使用できることを意味します。call_boxの実装は、
(*self)()を使用してBox<T>からクロージャをムーブし、クロージャを呼び出します。
これでJob型エイリアスには、新しいトレイトのFnBoxを実装する何かのBoxである必要が出てきました。
これにより、クロージャを直接呼び出す代わりにJob値を得た時にWorkerのcall_boxを使えます。
任意のFnOnce()クロージャに対してFnBoxトレイトを実装することは、チャンネルに送信する実際の値は何も変えなくてもいいことを意味します。
もうコンパイラは、我々が行おうとしていることが平気なことであると認識できます。
このトリックは非常にこそこそしていて複雑です。完璧に筋が通らなくても心配しないでください; いつの日か、完全に不要になるでしょう。
このトリックの実装で、スレッドプールは動く状態になります!cargo runを実行し、
リクエストを行なってください:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
warning: field is never used: `workers`
--> src/lib.rs:7:5
|
7 | workers: Vec<Worker>,
| ^^^^^^^^^^^^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
warning: field is never used: `id`
--> src/lib.rs:61:5
|
61 | id: usize,
| ^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
warning: field is never used: `thread`
--> src/lib.rs:62:5
|
62 | thread: thread::JoinHandle<()>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
Finished dev [unoptimized + debuginfo] target(s) in 0.99 secs
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にリクエストを行なっても、 サーバは他のスレッドに実行させることで他のリクエストを提供できるでしょう。
第18章でwhile letループを学んだ後で、なぜリスト20-22に示したようにワーカースレッドのコードを記述しなかったのか、
不思議に思っている可能性があります。
ファイル名: src/lib.rs
// --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 {} got a job; executing.", id);
job.call_box();
}
});
Worker {
id,
thread,
}
}
}
リスト20-22: while letを使用したもう1つのWorker::newの実装
このコードはコンパイルでき、動きますが、望み通りのスレッドの振る舞いにはなりません:
遅いリクエストがそれでも、他のリクエストが処理されるのを待機させてしまうのです。理由はどこか捉えがたいものです:
Mutex構造体には公開のunlockメソッドがありません。ロックの所有権が、
lockメソッドが返すLockResult<MutexGuard<T>>内のMutexGuard<T>のライフタイムに基づくからです。
コンパイル時には、ロックを保持していない限り、借用チェッカーはそうしたら、Mutexに保護されるリソースにはアクセスできないという規則を強制できます。
しかし、この実装は、MutexGuard<T>のライフタイムについて熟考しなければ、
意図したよりもロックが長い間保持される結果になり得ます。while式の値がブロックの間中スコープに残り続けるので、
ロックはjob.call_boxの呼び出し中保持されたままになり、つまり、他のワーカーが仕事を受け取れなくなるのです。
代わりにloopを使用し、ロックと仕事をブロックの外ではなく、内側で獲得することで、
lockメソッドが返すMutexGuardはlet job文が終わると同時にドロップされます。
これにより、複数のリクエストを並行で提供し、ロックはrecvの呼び出しの間は保持されるけれども、
job.call_boxの呼び出しの前には解放されることを保証します。
正常なシャットダウンと片付け
リスト20-21のコードは、意図した通り、スレッドプールの使用を通してリクエストに非同期に応答できます。
直接使用していないworkers、id、threadフィールドについて警告が出ます。この警告は、現在のコードは何も片付けていないことを思い出させてくれます。
優美さに欠けるctrl-cを使用してメインスレッドを停止させる方法を使用すると、
リクエストの処理中であっても、他のスレッドも停止します。
では、閉じる前に取り掛かっているリクエストを完了できるように、プールの各スレッドに対してjoinを呼び出すDropトレイトを実装します。
そして、スレッドに新しいリクエストの受付を停止し、終了するように教える方法を実装します。
このコードが動いているのを確かめるために、サーバを変更して正常にスレッドプールを終了する前に2つしかリクエストを受け付けないようにします。
ThreadPoolにDropトレイトを実装する
スレッドプールにDropを実装するところから始めましょう。プールがドロップされると、
スレッドは全てjoinして、作業を完了するのを確かめるべきです。リスト20-23は、Drop実装の最初の試みを表示しています;
このコードはまだ完全には動きません。
ファイル名: src/lib.rs
impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in &mut self.workers {
// ワーカー{}を終了します
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
リスト20-23: スレッドプールがスコープを抜けた時にスレッドをjoinさせる
まず、スレッドプールworkersそれぞれを走査します。selfは可変参照であり、workerを可変化できる必要もあるので、
これには&mutを使用しています。ワーカーそれぞれに対して、特定のワーカーを終了する旨のメッセージを出力し、
それからjoinをワーカースレッドに対して呼び出しています。joinの呼び出しが失敗したら、
unwrapを使用してRustをパニックさせ、正常でないシャットダウンに移行します。
こちらが、このコードをコンパイルする際に出るエラーです:
error[E0507]: cannot move out of borrowed content
--> src/lib.rs:65:13
|
65 | worker.thread.join().unwrap();
| ^^^^^^ cannot move out of borrowed content
各workerの可変参照しかなく、joinは引数の所有権を奪うためにこのエラーはjoinを呼び出せないと教えてくれています。
この問題を解決するには、joinがスレッドを消費できるように、threadを所有するWorkerインスタンスからスレッドをムーブする必要があります。
これをリスト17-15では行いました: Workerが代わりにOption<thread::JoinHandle<()>>を保持していれば、
Optionに対してtakeメソッドを呼び出し、Some列挙子から値をムーブし、その場所にNone列挙子を残すことができます。
言い換えれば、実行中のWorkerにはthreadにSome列挙子があり、Workerを片付けたい時には、
ワーカーが実行するスレッドがないようにSomeをNoneで置き換えるのです。
従って、Workerの定義を以下のように更新したいことがわかります:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::thread; struct Worker { id: usize, thread: Option<thread::JoinHandle<()>>, } }
さて、コンパイラを頼りにして他に変更する必要がある箇所を探しましょう。このコードをチェックすると、 2つのエラーが出ます:
error[E0599]: no method named `join` found for type
`std::option::Option<std::thread::JoinHandle<()>>` in the current scope
--> src/lib.rs:65:27
|
65 | worker.thread.join().unwrap();
| ^^^^
error[E0308]: mismatched types
--> src/lib.rs:89:13
|
89 | thread,
| ^^^^^^
| |
| expected enum `std::option::Option`, found struct
`std::thread::JoinHandle`
| help: try using a variant of the expected type: `Some(thread)`
|
= note: expected type `std::option::Option<std::thread::JoinHandle<()>>`
found type `std::thread::JoinHandle<_>`
2番目のエラーを扱いましょう。これは、Worker::newの最後のコードを指しています; 新しいWorkerを作成する際に、
Someにthreadの値を包む必要があります。このエラーを修正するために以下の変更を行なってください:
ファイル名: src/lib.rs
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
// --snip--
Worker {
id,
thread: Some(thread),
}
}
}
最初のエラーはDrop実装内にあります。先ほど、Option値に対してtakeを呼び出し、
threadをworkerからムーブする意図があることに触れました。以下の変更がそれを行います:
ファイル名: src/lib.rs
impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}
第17章で議論したように、Optionのtakeメソッドは、Some列挙子を取り出し、その箇所にNoneを残します。
if letを使用してSomeを分配し、スレッドを得ています; そして、スレッドに対してjoinを呼び出します。
ワーカーのスレッドが既にNoneなら、ワーカーはスレッドを既に片付け済みであることがわかるので、
その場合には何も起きません。
スレッドに仕事をリッスンするのを止めるよう通知する
これらの変更によって、コードは警告なしでコンパイルできます。ですが悪い知らせは、このコードが期待したようにはまだ機能しないことです。
鍵は、Workerインスタンスのスレッドで実行されるクロージャのロジックです: 現時点でjoinを呼び出していますが、
仕事を求めて永遠にloopするので、スレッドを終了しません。現在のdropの実装でThreadPoolをドロップしようとしたら、
最初のスレッドが完了するのを待機してメインスレッドは永遠にブロックされるでしょう。
この問題を修正するには、スレッドが、実行すべきJobか、リッスンをやめて無限ループを抜ける通知をリッスンするように、
変更します。Jobインスタンスの代わりに、チャンネルはこれら2つのenum列挙子の一方を送信します。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { struct Job; enum Message { NewJob(Job), Terminate, } }
このMessage enumはスレッドが実行すべきJobを保持するNewJob列挙子か、スレッドをループから抜けさせ、
停止させるTerminate列挙子のどちらかになります。
チャンネルを調整し、型Jobではなく、型Messageを使用するようにする必要があります。リスト20-24のようにですね。
ファイル名: src/lib.rs
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Message>,
}
// --snip--
impl ThreadPool {
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static
{
let job = Box::new(f);
self.sender.send(Message::NewJob(job)).unwrap();
}
}
// --snip--
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Message>>>) ->
Worker {
let thread = thread::spawn(move ||{
loop {
let message = receiver.lock().unwrap().recv().unwrap();
match message {
Message::NewJob(job) => {
println!("Worker {} got a job; executing.", id);
job.call_box();
},
Message::Terminate => {
// ワーカー{}は停止するよう指示された
println!("Worker {} was told to terminate.", id);
break;
},
}
}
});
Worker {
id,
thread: Some(thread),
}
}
}
リスト20-24: Message値を送受信し、WorkerがMessage::Terminateを受け取ったら、ループを抜ける
Message enumを具体化するために、2箇所でJobをMessageに変更する必要があります:
ThreadPoolの定義とWorker::newのシグニチャです。ThreadPoolのexecuteメソッドは、
仕事をMessage::NewJob列挙子に包んで送信する必要があります。それから、
Messageがチャンネルから受け取られるWorker::newで、NewJob列挙子が受け取られたら、
仕事が処理され、Terminate列挙子が受け取られたら、スレッドはループを抜けます。
これらの変更と共に、コードはコンパイルでき、リスト20-21の後と同じように機能し続けます。ですが、
Terminateのメッセージを何も生成していないので、警告が出るでしょう。
Drop実装をリスト20-25のような見た目に変更してこの警告を修正しましょう。
ファイル名: src/lib.rs
impl Drop for ThreadPool {
fn drop(&mut self) {
println!("Sending terminate message to all workers.");
for _ in &mut self.workers {
self.sender.send(Message::Terminate).unwrap();
}
// 全ワーカーを閉じます
println!("Shutting down all workers.");
for worker in &mut self.workers {
// ワーカー{}を閉じます
println!("Shutting down worker {}", worker.id);
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}
リスト20-25: 各ワーカースレッドに対してjoinを呼び出す前にワーカーにMessage::Terminateを送信する
今では、ワーカーを2回走査しています: 各ワーカーにTerminateメッセージを送信するために1回と、
各ワーカースレッドにjoinを呼び出すために1回です。メッセージ送信とjoinを同じループで即座に行おうとすると、
現在の繰り返しのワーカーがチャンネルからメッセージを受け取っているものであるか保証できなくなってしまいます。
2つの個別のループが必要な理由をよりよく理解するために、2つのワーカーがある筋書きを想像してください。
単独のループで各ワーカーを走査すると、最初の繰り返しでチャンネルに停止メッセージが送信され、
joinが最初のワーカースレッドで呼び出されます。その最初のワーカーが現在、リクエストの処理で忙しければ、
2番目のワーカーがチャンネルから停止メッセージを受け取り、閉じます。最初のワーカーの終了待ちをしたままですが、
2番目のスレッドが停止メッセージを拾ってしまったので、終了することは絶対にありません。デッドロックです!
この筋書きを回避するために、1つのループでまず、チャンネルに対して全てのTerminateメッセージを送信します;
そして、別のループで全スレッドのjoinを待ちます。一旦停止メッセージを受け取ったら、各ワーカーはチャンネルからのリクエストの受付をやめます。
故に、存在するワーカーと同じ数だけ停止メッセージを送れば、joinがスレッドに対して呼び出される前に、
停止メッセージを各ワーカーが受け取ると確信できるわけです。
このコードが動いているところを確認するために、mainを変更してサーバを正常に閉じる前に2つしかリクエストを受け付けないようにしましょう。
リスト20-26のようにですね。
ファイル名: src/bin/main.rs
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.");
}
リスト20-26: ループを抜けることで、2つのリクエストを処理した後にサーバを閉じる
現実世界のWebサーバには、たった2つリクエストを受け付けた後にシャットダウンしてほしくはないでしょう。 このコードは、単に正常なシャットダウンとクリーンアップが正しく機能することを示すだけです。
takeメソッドは、Iteratorトレイトで定義されていて、最大でも繰り返しを最初の2つの要素だけに制限します。
ThreadPoolはmainの末端でスコープを抜け、drop実装が実行されます。
cargo runでサーバを開始し、3つリクエストを行なってください。3番目のリクエストはエラーになるはずで、
端末にはこのような出力が目撃できるはずです:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished dev [unoptimized + debuginfo] target(s) in 1.0 secs
Running `target/debug/hello`
Worker 0 got a job; executing.
Worker 3 got a job; executing.
Shutting down.
Sending terminate message to all workers.
Shutting down all workers.
Shutting down worker 0
Worker 1 was told to terminate.
Worker 2 was told to terminate.
Worker 0 was told to terminate.
Worker 3 was told to terminate.
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3
ワーカーとメッセージの順番は異なる可能性があります。どうやってこのコードが動くのかメッセージからわかります:
ワーカー0と3が最初の2つのリクエストを受け付け、そして3番目のリクエストではサーバは接続の受け入れをやめます。
mainの最後でThreadPoolがスコープを抜ける際、Drop実装が割り込み、プールが全ワーカーに停止するよう指示します。
ワーカーはそれぞれ、停止メッセージを確認した時にメッセージを出力し、それからスレッドプールは各ワーカースレッドを閉じるjoinを呼び出します。
この特定の実行のある面白い側面に注目してください: ThreadPoolはチャンネルに停止メッセージを送信しますが、
どのワーカーがそのメッセージを受け取るよりも前に、ワーカー0のjoinを試みています。ワーカー0はまだ停止メッセージを受け取っていなかったので、
メインスレッドはワーカー0が完了するまで待機してブロックされます。その間に、各ワーカーは停止メッセージを受け取ります。
ワーカー0が完了したら、メインスレッドは残りのワーカーが完了するのを待機します。その時点で全ワーカーは停止メッセージを受け取った後で、
閉じることができたのです。
おめでとうございます!プロジェクトを完成させました; スレッドプールを使用して非同期に応答する基本的なWebサーバができました。 サーバの正常なシャットダウンを行うことができ、プールの全スレッドを片付けます。
参考までに、こちらが全コードです:
ファイル名: src/bin/main.rs
extern crate hello;
use hello::ThreadPool;
use std::io::prelude::*;
use std::net::TcpListener;
use std::net::TcpStream;
use std::fs::File;
use std::thread;
use std::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 mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
let get = b"GET / HTTP/1.1\r\n";
let sleep = b"GET /sleep HTTP/1.1\r\n";
let (status_line, filename) = if buffer.starts_with(get) {
("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
} else if buffer.starts_with(sleep) {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK\r\n\r\n", "hello.html")
} else {
("HTTP/1.1 404 NOT FOUND\r\n\r\n", "404.html")
};
let mut file = File::open(filename).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let response = format!("{}{}", status_line, contents);
stream.write(response.as_bytes()).unwrap();
stream.flush().unwrap();
}
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::thread; use std::sync::mpsc; use std::sync::Arc; use std::sync::Mutex; enum Message { NewJob(Job), Terminate, } pub struct ThreadPool { workers: Vec<Worker>, sender: mpsc::Sender<Message>, } trait FnBox { fn call_box(self: Box<Self>); } impl<F: FnOnce()> FnBox for F { fn call_box(self: Box<F>) { (*self)() } } type Job = Box<FnBox + 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(Message::NewJob(job)).unwrap(); } } impl Drop for ThreadPool { fn drop(&mut self) { println!("Sending terminate message to all workers."); for _ in &mut self.workers { self.sender.send(Message::Terminate).unwrap(); } println!("Shutting down all workers."); 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<Message>>>) -> Worker { let thread = thread::spawn(move ||{ loop { let message = receiver.lock().unwrap().recv().unwrap(); match message { Message::NewJob(job) => { println!("Worker {} got a job; executing.", id); job.call_box(); }, Message::Terminate => { println!("Worker {} was told to terminate.", id); break; }, } } }); Worker { id, thread: Some(thread), } } } }
ここでできることはまだあるでしょう!よりこのプロジェクトを改善したいのなら、こちらがアイディアの一部です:
ThreadPoolとその公開メソッドにもっとドキュメンテーションを追加する。- ライブラリの機能のテストを追加する。
unwrapの呼び出しをもっと頑健なエラー処理に変更する。ThreadPoolを使用してWebリクエスト以外のなんらかの作業を行う。- https://crates.io でスレッドプールのクレートを探して、そのクレートを代わりに使用して似たWebサーバを実装する。 そして、APIと頑健性を我々が実装したものと比較する。
まとめ
よくやりました!本の最後に到達しました!Rustのツアーに参加していただき、感謝の辞を述べたいです。 もう、ご自身のRustプロジェクトや他の方のプロジェクトのお手伝いをする準備ができています。 あなたがこれからのRustの旅で遭遇する、あらゆる困難の手助けを是非とも行いたいRustaceanたちの温かいコミュニティがあることを心に留めておいてくださいね。
付録
以下の節は、Rustの旅で役に立つと思えるかもしれない参考資料を含んでいます。
付録A: キーワード
以下のリストは、現在、あるいは将来Rust言語により使用されるために予約されているキーワードです。 そのため、識別子として使用することはできません。識別子には、関数名、変数名、引数名、構造体のフィールド名、モジュール名、クレート名、定数名、マクロ名、静的な値の名前、属性名、型名、トレイト名、ライフタイム名などがあります。 ただし、生識別子のところで議論する生識別子は例外です。
現在使用されているキーワード
以下のキーワードは、解説された通りの機能が現状あります。
as- 基礎的なキャストの実行、要素を含む特定のトレイトの明確化、useやextern crate文の要素名を変更するasync- 現在のスレッドをブロックする代わりにFutureを返すawait-Futureの結果が準備できるまで実行を停止するbreak- 即座にループを抜けるconst- 定数要素か定数の生ポインタを定義するcontinue- 次のループの繰り返しに継続するcrate- 外部のクレートかマクロが定義されているクレートを表すマクロ変数をリンクするelse-ifとif let制御フロー構文の規定enum- 列挙型を定義するextern- 外部のクレート、関数、変数をリンクするfalse- bool型のfalseリテラルfn- 関数か関数ポインタ型を定義するfor- イテレータの要素を繰り返す、トレイトの実装、高階ライフタイムの指定if- 条件式の結果によって条件分岐impl- 固有の機能やトレイトの機能を実装するin-forループ記法の一部let- 変数を束縛するloop- 無条件にループするmatch- 値をパターンとマッチさせるmod- モジュールを定義するmove- クロージャにキャプチャした変数全ての所有権を奪わせるmut- 参照、生ポインタ、パターン束縛で可変性に言及するpub- 構造体フィールド、implブロック、モジュールで公開性について言及するref- 参照で束縛するreturn- 関数から帰るSelf- 定義しようとしている・実装(implement)しようとしている型の型エイリアスself- メソッドの主題、または現在のモジュールstatic- グローバル変数、またはプログラム全体に渡るライフタイムstruct- 構造体を定義するsuper- 現在のモジュールの親モジュールtrait- トレイトを定義するtrue- bool型のtrueリテラルtype- 型エイリアスか関連型を定義するunsafe- unsafeなコード、関数、トレイト、実装に言及するuse- スコープにシンボルを持ち込むwhere- 型を制限する節に言及するwhile- 式の結果に基づいて条件的にループする
将来的な使用のために予約されているキーワード
以下のキーワードには機能が何もないものの、将来的に使用される可能性があるので、Rustにより予約されています。
abstractbecomeboxdofinalmacrooverrideprivtrytypeofunsizedvirtualyield
生識別子
生識別子 とは、普段は使うことが許されないキーワードを使わせてくれる構文です。
生識別子はキーワードの前にr#を置いて使うことができます。
たとえば、matchはキーワードです。
次の、名前がmatchである関数をコンパイルしようとすると:
ファイル名: src/main.rs
fn match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
// 訳注: 引数名は、"a needle in a haystack" すなわち「干し草の中の針」という、
// 「見つかりそうにない捜し物」を意味する成句からもじった命名。
// 検索をする関数でよく使われる。
次のエラーを得ます:
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")); }
このコードはなんのエラーもなくコンパイルできます。
r#は、定義のときも、main内で呼ばれたときにも、関数名の前につけられていることに注意してください。
生識別子を使えば、仮にそれが予約されたキーワードであろうとも、任意の単語を識別子として使えるようになります。
更に、あなたのクレートが使っているRustのeditionとは異なるeditionで書かれたライブラリを呼び出すこともできるようになります。
たとえば、tryは2015 editionではキーワードではありませんでしたが、2018 editionではキーワードです。
もし、2015 editionで書かれており、try関数を持っているライブラリに依存している場合、あなたの2018 editionのコードからその関数を呼び出すためには、生識別子構文を使う必要がでてくるでしょう。今回ならr#tryですね。
editionに関して、より詳しくは付録 Eを見てください。
付録B: 演算子と記号
この付録は、演算子や、単独で現れたり、パス、ジェネリクス、トレイト境界、マクロ、属性、コメント、タプル、 かっこの文脈で現れる他の記号を含むRustの記法の用語集を含んでいます。
演算子
表B-1は、Rustの演算子、演算子が文脈で現れる例、短い説明、その演算子がオーバーロード可能かどうかを含んでいます。 演算子がオーバーロード可能ならば、オーバーロードするのに使用する関係のあるトレイトも列挙されています。
表B-1: 演算子
| 演算子 | 例 | 説明 | オーバーロードできる? |
|---|---|---|---|
! | ident!(...), ident!{...}, ident![...] | マクロ展開 | |
! | !expr | ビット反転、または論理反転 | Not |
!= | var != 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 |
* | *expr | 参照外し | |
* | *const type, *mut type | 生ポインタ | |
*= | var *= expr | 掛け算後に代入 | MulAssign |
+ | 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.., ..expr, expr..expr | 未満範囲リテラル | |
.. | ..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 | ビットXOR | BitXor |
^= | var ^= expr | ビットXOR後に代入 | BitXorAssign |
| | pat | pat | パターンOR | |
| | |…| expr | クロージャ | |
| | 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"..." | バイト文字列リテラル、文字列の代わりに[u8]を構築します |
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 | ライフタイム'aよりも長生きしなければならないジェネリック型T(型がライフタイムより長生きするとは、'aよりも短いライフタイムの参照を何も遷移的に含められないことを意味する) |
T : 'static | ジェネリック型Tが'staticなもの以外の借用された参照を何も含まない |
'b: 'a | ジェネリックなライフタイム'bがライフタイム'aより長生きしなければならない |
T: ?Sized | ジェネリック型引数が動的サイズ決定型であることを許容する |
'a + trait, trait + trait | 複合型制約 |
表B-6は、マクロの呼び出しや定義、要素に属性を指定する文脈で出現する記号を示しています。
表B-6: マクロと属性
| シンボル | 説明 |
|---|---|
#[meta] | 外部属性 |
#![meta] | 内部属性 |
$ident | マクロ代用 |
$ident:kind | マクロキャプチャ |
$(…)… | マクロの繰り返し |
表B-7は、コメントを生成する記号を示しています。
表B-7: コメント
| シンボル | 説明 |
|---|---|
// | 行コメント |
//! | 内部行docコメント |
/// | 外部行docコメント |
/*...*/ | ブロックコメント |
/*!...*/ | 内部ブロックdocコメント |
/**...*/ | 外部ブロックdocコメント |
タプル
表B-8は、タプルの文脈で出現する記号を示しています。
表B-8: タプル
| シンボル | 説明 |
|---|---|
() | 空のタプル (ユニットとしても知られる)、リテラル、型両方 |
(expr) | 括弧付きの式 |
(expr,) | 1要素タプル式 |
(type,) | 1要素タプル型 |
(expr, ...) | タプル式 |
(type, ...) | タプル型 |
expr(expr, ...) | 関数呼び出し式; タプルstructやタプルenum列挙子を初期化するのにも使用される |
ident!(...), ident!{...}, ident![...] | マクロ呼び出し |
expr.0, expr.1, など | タプル添え字アクセス |
表B-9は、波括弧が使用される文脈を表示しています。
表B-9: 波括弧
| 文脈 | 説明 |
|---|---|
{...} | ブロック式 |
Type {...} | structリテラル |
表B-10は、角括弧が使用される文脈を表示しています。
表B-10: 角括弧
| 文脈 | 説明 |
|---|---|
[...] | 配列リテラル |
[expr; len] | len個exprを含む配列リテラル |
[type; len] | len個のtypeのインスタンスを含む配列型 |
expr[expr] | コレクション添え字アクセス。オーバーロード可能 (Index, IndexMut) |
expr[..], expr[a..], expr[..b], expr[a..b] | Range、RangeFrom、RangeTo、RangeFullを「添え字」として使用してコレクション・スライシングの振りをするコレクション添え字アクセス |
付録C: 導出可能なトレイト
本のいろんな箇所でderive属性について議論しました。これは構造体や、enum定義に適用できます。
derive属性は、derive記法で注釈した型に対して独自の既定の実装でトレイトを実装するコードを生成します。
この付録では、標準ライブラリのderiveと共に使用できる全トレイトの参照を提供します。各節は以下を講義します:
- このトレイトを導出する演算子やメソッドで可能になること
deriveが提供するトレイトの実装がすること- トレイトを実装することが型についてどれほど重要か
- そのトレイトを実装できたりできなかったりする条件
- そのトレイトが必要になる処理の例
derive属性が提供する以外の異なる振る舞いが欲しいなら、それらを手動で実装する方法の詳細について、
各トレイトの標準ライブラリのドキュメンテーションを調べてください。
標準ライブラリで定義されている残りのトレイトは、deriveで自分の型に実装することはできません。
これらのトレイトには知覚できるほどの既定の振る舞いはないので、自分が達成しようしていることに対して、
道理が通る方法でそれらを実装するのはあなた次第です。
導出できないトレイトの例はDisplayで、これはエンドユーザ向けのフォーマットを扱います。常に、エンドユーザ向けに型を表示する適切な方法について、
考慮すべきです。型のどの部分をエンドユーザは見ることができるべきでしょうか?どの部分を関係があると考えるでしょうか?
どんな形式のデータがエンドユーザにとって最も関係があるでしょうか?Rustコンパイラには、
この見識がないため、適切な既定動作を提供してくれないのです。
この付録で提供される導出可能なトレイトのリストは、包括的ではありません: ライブラリは、自身のトレイトにderiveを実装でき、
deriveと共に使用できるトレイトのリストが実に限りのないものになってしまうのです。deriveの実装には、
プロシージャルなマクロが関連します。マクロについては、付録Dで講義します。
プログラマ用の出力のDebug
Debugトレイトにより、フォーマット文字列でのデバッグ整形が可能になり、
{}プレースホルダー内に:?を追記することで表します。
Debugトレイトにより、デバッグ目的で型のインスタンスを出力できるようになるので、あなたや型を使用する他のプログラマが、
プログラムの実行の特定の箇所でインスタンスを調べられます。
Debugトレイトは、例えば、assert_eq!マクロを使用する際などに必要になります。
このマクロは、プログラマがどうして2つのインスタンスが等価でなかったのか確認できるように、
等価アサートが失敗したら、引数として与えられたインスタンスの値を出力します。
等価比較のためのPartialEqとEq
PartialEqトレイトにより、型のインスタンスを比較して、等価性をチェックでき、==と!=演算子の使用を可能にします。
PartialEqを導出すると、eqメソッドを実装します。構造体にPartialEqを導出すると、
全フィールドが等しい時のみ2つのインスタンスは等価になり、いずれかのフィールドが等価でなければ、
インスタンスは等価ではなくなります。enumに導出すると、各列挙子は、自身には等価ですが、他の列挙子には等価ではありません。
PartialEqトレイトは例えば、assert_eq!マクロを使用する際に必要になります。
これは、等価性のためにとある型の2つのインスタンスを比較できる必要があります。
Eqトレイトにはメソッドはありません。その目的は、注釈された型の全値に対して、値が自身と等しいことを通知することです。
Eqトレイトは、PartialEqを実装する全ての型がEqを実装できるわけではないものの、
PartialEqも実装する型に対してのみ適用できます。これの一例は、浮動小数点数型です:
浮動小数点数の実装により、非数字(NaN)値の2つのインスタンスはお互いに等価ではないことが宣言されます。
Eqが必要になる一例が、HashMap<K, V>のキーで、HashMap<K, V>が、2つのキーが同じであると判定できます。
順序付き比較のためのPartialOrdとOrd
PartialOrdトレイトにより、ソートする目的で型のインスタンスを比較できます。PartialOrdを実装する型は、
<、>、<=、>=演算子を使用することができます。PartialEqも実装する型に対してのみ、
PartialOrdトレイトを適用できます。
PartialOrdを導出すると、partial_cmpメソッドを実装し、これは、与えられた値が順序付けられない時にNoneになるOption<Ordering>を返します。
その型のほとんどの値は比較できるものの、順序付けできない値の例として、非数字(NaN)浮動小数点値が挙げられます。
partial_cmpをあらゆる浮動小数点数とNaN浮動小数点数で呼び出すと、Noneが返るでしょう。
構造体に導出すると、フィールドが構造体定義で現れる順番で各フィールドの値を比較することで2つのインスタンスを比較します。 enumに導出すると、enum定義で先に定義された列挙子が、後に列挙された列挙子よりも小さいと考えられます。
PartialOrdトレイトが必要になる例には、低い値と高い値で指定される範囲の乱数を生成するrandクレートのgen_rangeメソッドが挙げられます。
Ordトレイトにより、注釈した型のあらゆる2つの値に対して、合法な順序付けが行えることがわかります。
Ordトレイトはcmpメソッドを実装し、これは、常に合法な順序付けが可能なので、Option<Ordering>ではなく、
Orderingを返します。PartialOrdとEq(EqはPartialEqも必要とします)も実装している型にしか、
Ordトレイトを適用することはできません。構造体とenumで導出したら、PartialOrdで、
partial_cmpの導出した実装と同じように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を導出することができます。Cloneも実装する型に対してのみ、
Copyトレイトを適用することができます。何故なら、Copyを実装する型には、
Copyと同じ作業を行う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章の「構造体更新記法で他のインスタンスからインスタンスを生成する」節で議論した構造体更新記法と組み合わせてよく使用されます。
構造体のいくつかのフィールドをカスタマイズし、それから..Default::default()を使用して、
残りのフィールドに対して既定値をセットし使用することができます。
例えば、Defaultトレイトは、Option<T>インスタンスに対してメソッドunwrap_or_defaultを使用する時に必要になります。
Option<T>がNoneならば、メソッドunwrap_or_defaultは、Option<T>に格納された型Tに対してDefault::defaultの結果を返します。
付録D - 便利な開発ツール
この付録では、Rustプロジェクトの提供する便利な開発ツールについていくつかお話します。 自動フォーマット、警告に対する修正をすばやく適用する方法、lintツール、そしてIDEとの統合について見ていきます。
rustfmtを使った自動フォーマット
rustfmtというツールは、コミュニティのコードスタイルに合わせてあなたのコードをフォーマットしてくれます。
Rustを書くときにどのスタイルを使うかで揉めないように、多くの共同で行われるプロジェクトがrustfmtを使っています:全員がこのツールでコードをフォーマットするのです。
rustfmtをインストールするには、以下を入力してください:
$ rustup component add rustfmt
これでrustfmtとcargo-fmtが使えるようになります。これはrustcとcargoの両方のコマンドがあるのと似たようなものです。
どんなCargoのプロジェクトも、次を入力するとフォーマットできます:
$ cargo fmt
このコマンドを実行すると、現在のクレートのあらゆるRustコードをフォーマットし直します。
これを行うと、コードのスタイルのみが変わり、コードの意味は変わりません。
rustfmtについてより詳しく知るにはドキュメントを読んでください。
rustfixでコードを修正する
rustfixというツールはRustをインストールすると同梱されており、コンパイラの警告 (warning) を自動で直してくれます。 Rustでコードを書いたことがある人なら、コンパイラの警告を見たことがあるでしょう。 たとえば、下のコードを考えます:
Filename: src/main.rs
fn do_something() {} fn main() { for i in 0..100 { do_something(); } }
ここで、do_something関数を100回呼んでいますが、forループの内部で変数iを一度も使っていません。
Rustはこれについて警告します:
$ cargo build
Compiling myprogram v0.1.0 (file:///projects/myprogram)
warning: unused variable: `i`
--> src/main.rs:4:9
|
4 | for i in 1..100 {
| ^ help: consider using `_i` instead
|
= note: #[warn(unused_variables)] on by default
Finished dev [unoptimized + debuginfo] target(s) in 0.50s
警告は、変数名に_iを使ってはどうかと提案しています:アンダーバーはその変数を使わないという意図を示すのです。
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によってコードが変更されていることがわかります。
Filename: src/main.rs
fn do_something() {} fn main() { for _i in 0..100 { do_something(); } }
forループの変数は_iという名前になったので、警告はもう現れません。
cargo fixコマンドを使うと、異なるRust editionの間でコードを変換することもできます。
editionについては付録Eに書いています。
Clippyでもっとlintを
Clippyというツールは、コードを分析することで、よくある間違いを見つけ、Rustのコードを改善させてくれるlintを集めたものです(訳注:いわゆる静的解析ツール)。
Clippyをインストールするには、次を入力してください:
$ rustup component add clippy
Clippyのlintは、次のコマンドでどんなCargoプロジェクトに対しても実行できます:
$ cargo clippy
たとえば、下のように、円周率などの数学定数の近似を使ったプログラムを書いているとします。
ファイル名: src/main.rs
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. Consider using it directly
--> src/main.rs:2:13
|
2 | let x = 3.1415;
| ^^^^^^
|
= note: #[deny(clippy::approx_constant)] on by default
= help: for further information visit https://rust-lang-nursery.github.io/rust-clippy/master/index.html#approx_constant
あなたは、このエラーのおかげで、Rustにはより正確に定義された定数がすでにあり、これを代わりに使うとプログラムがより正しくなるかもしれないと気づくことができます。
なので、あなたはコードを定数PIを使うように変更するでしょう。
以下のコードはもうClippyからエラーや警告は受けません。
ファイル名: src/main.rs
fn main() { let x = std::f64::consts::PI; let r = 8.0; println!("the area of the circle is {}", x * r * r); }
Clippyについてより詳しく知るには、ドキュメントを読んでください。
Rust Language Serverを使ってIDEと統合する
IDEでの開発の助けになるよう、Rustプロジェクトは Rust Language Server (rls)を配布しています。
このツールは、Language Server Protocolという、IDEとプログラミング言語が対話するための仕様に対応しています。
Visual Studio CodeのRustプラグインをはじめ、様々なクライアントがrlsを使うことができます。
rlsをインストールするには、以下を入力してください:
$ rustup component add rls
つづけて、あなたのIDE向けのlanguage serverサポートをインストールしてください。 すると、自動補完、定義へのジャンプ、インラインのエラー表示などの機能が得られるはずです。
rlsについてより詳しく知るにはドキュメントを読んでください。
付録E:エディション
第1章で、cargo newが エディションに関するちょっとしたメタデータを Cargo.toml に追加しているのを見ましたね。
この付録ではその意味についてお話します!
Rust言語とコンパイラは6週間のリリースサイクルを採用しています。つまり、ユーザにはコンスタントに新しい機能が流れてくるというわけです。 他のプログラミング言語は、より少ない回数で、より大きなリリースを行いますが、Rustは小さなアップデートを頻繁に行います。 しばらくすると、これらの小さな変更が溜まっていきます。 しかし、これらを振り返って、「Rust 1.10とRust 1.31を比較すると、すごく変わったねえ!」などとリリースごとに言うのは難しいです。
2、3年ごとに、RustチームはRustの新しい エディション を作ります。 それぞれのエディションには、それまでにRustにやってきた新しい機能が、完全に更新されたドキュメントとツール群とともに、一つのパッケージとなってまとめられています。 新しいエディションは通常の6週間ごとのリリースの一部として配布されます。
それぞれの人々にとってエディションは異なる意味を持ちます。
- アクティブなRustユーザにとっては、新しいエディションは、少しずつ増えてきた変更点を理解しやすいパッケージにしてまとめるものです。
- Rustユーザでない人にとっては、新しいエディションは、何かしら大きな達成がなされたことを示します。Rustには今一度目を向ける価値があると感じさせるかもしれません。
- Rustを開発している人にとっては、新しいエディションは、プロジェクト全体の目標地点となります。
この文書を書いている時点(訳注:原文のコミットは2021年12月23日)では、3つのRustのエディションが利用できます。 Rust 2015、Rust 2018、Rust 2021です。 この本はRust 2021エディションの慣例に従って書かれています。
Cargo.toml におけるeditionキーは、コードに対してコンパイラがどのエディションを適用すべきかを示しています。
もしキーが存在しなければ、Rustは後方互換性のため2015をエディションの値として使います。
標準の2015エディション以外のエディションを使うという選択はそれぞれのプロジェクトですることができます。 エディションには、コード内の識別子と衝突してしまう新しいキーワードの導入など、互換性のない変更が含まれる可能性があります。 しかし、それらの変更を選択しない限り、Rustのコンパイラのバージョンを更新しても、コードは変わらずコンパイルできます。
Rustコンパイラは全バージョンにおいて、そのコンパイラのリリースまでに存在したすべてのエディションをサポートしており、またサポートされているエディションのクレートはすべてリンクできます。 エディションの変更はコンパイラが最初にコードを構文解析するときにのみ影響します。 なので、あなたがRust 2015を使っていて、依存先にRust 2018を使うものがあったとしても、あなたのプロジェクトはコンパイルでき、その依存先を使うことができます。 逆に、あなたのプロジェクトがRust 2018を、依存先がRust 2015を使っていても、同じく問題はありません。
まあ実のところ、ほとんどの機能はすべてのエディションで利用可能でしょう。 どのRustエディションを使っている開発者も、新しい安定リリースが出ると改善したなと感じるのは変わらないでしょう。 しかし、場合によって(多くは新しいキーワードが追加されたとき)は、新機能が新しいエディションでしか利用できないことがあるかもしれません。 そのような機能を利用したいなら、エディションを切り替える必要があるでしょう。
より詳しく知りたいなら、エディションガイドという、エディションに関するすべてを説明している本があります。
エディション同士の違いや、cargo fixを使って自動的にコードを新しいエディションにアップグレードする方法が書かれています。
訳注:日本語版のエディションガイドはこちらにあります。
付録F: 本の翻訳
英語以外の言語の資料についてです。ほとんどはまだ翻訳中です。 手助けいただける際や、新しい翻訳について教えていただける際は、 Translationsラベルを確認してください!
- Português (BR)
- Português (PT)
- 简体中文
- Українська
- Español, alternate
- Italiano
- Русский
- 한국어
- 日本語
- Français
- Polski
- עברית
- Cebuano
- Tagalog
- Esperanto
- ελληνική
- Svenska
- Farsi
- Deutsch
付録G: Rustの作られ方と“Nightly Rust”
この付録は、Rustのでき方と、それがRust開発者としてあなたにどう影響するかについてです。 この本の出力は安定版Rust 1.21.0で生成されていますが、コンパイルできるいかなる例も、 それより新しいRustのどんな安定版でもコンパイルでき続けられるはずということに触れました。 この節は、これが本当のことであると保証する方法を説明します!
停滞なしの安定性
言語として、Rustはコードの安定性について大いに注意しています。Rustには、その上に建築できる岩のように硬い基礎であってほしく、 物事が定期的に変わっていたら、それは実現できません。同時に新しい機能で実験できなければ、もはや何も変更できないリリースの時まで、 重大な瑕疵を発見できなくなるかもしれません。
この問題に対する我々の解決策は「停滞なしの安定性」と呼ばれるもので、ガイドの原則は以下の通りです: 安定版Rustの新しいバージョンにアップグレードするのを恐れる必要は何もないはずです。各アップグレードは痛みのないもののはずですが、 新しい機能、より少ないバグ、高速なコンパイル時間も齎すべきです。
シュポシュポ!リリースチャンネルと列車に乗ること
Rust開発は、電車のダイヤに合わせて処理されます。つまり、全開発はRustリポジトリのmasterブランチで行われます。
リリースはソフトウェアのリリーストレインモデル(software release train model)に従い、これはCisco IOSや他のソフトウェアプロジェクトで活用されています。
Rustにはリリースチャンネルが3つあります:
注釈: software release train modelとは、あるバージョンのソフトウェアリリースの順番を列車に見立て、 列車のダイヤのように、決まった間隔でリリースに持って行く手法のことの模様。一つの列車は、Rustの場合、 ナイトリー、ベータ、安定版の順に「駅」に停車していくものと思われる。
- ナイトリー
- ベータ
- 安定版
多くのRust開発者は主に安定版チャンネルを使用しますが、新しい実験的な機能を試したい方は、 ナイトリーやベータを使用するかもしれません。
こちらが、開発とリリースプロセスの動き方の例です: RustチームがRust1.5のリリースに取り掛かっていると想定しましょう。
そのリリースは、2015年の11月に発生しましたが、現実的なバージョンナンバーを与えてくれるでしょう。
新しい機能がRustに追加されます: 新しいコミットがmasterブランチに着地します。毎晩、新しいナイトリ版のRustが生成されます。
毎日がリリース日で、これらのリリースは、リリースインフラにより自動で作成されます。故に、
時間が経てばリリースは、毎晩1回、以下のような見た目になります:
nightly: * - - * - - *
6週間ごとに、新しいリリースを準備するタイミングになります!Rustリポジトリのbetaブランチが、
ナイトリで使用されるmasterブランチから枝分かれします。さて、リリースが二つになりました:
nightly: * - - * - - *
|
beta: *
ほとんどのRustユーザはベータリリースを積極的には使用しませんが、自身のCIシステム内でベータに対してテストを行い、 Rustが不具合の可能性を発見するのを手伝います。その間も、やはりナイトリリリースは毎晩あります:
注釈: CIはContinuous Integration(継続統合といったところか)のことと思われる。開発者のコードを1日に何度も、 メインのブランチに統合することらしい。
nightly: * - - * - - * - - * - - *
|
beta: *
不具合が見つかったとしましょう。よいことに、不具合が安定版のリリースにこっそり持ち込まれる前にベータリリースをテストする時間がありました!
修正がmasterに適用されるので、ナイトリは修正され、それから修正がbetaブランチにバックポートされ、
ベータの新しいリリースが生成されます:
nightly: * - - * - - * - - * - - * - - *
|
beta: * - - - - - - - - *
最初のベータが作成されてから6週間後、安定版のリリースの時間です!stableブランチがbetaブランチから生成されます:
nightly: * - - * - - * - - * - - * - - * - * - *
|
beta: * - - - - - - - - *
|
stable: *
やりました!Rust1.5が完了しました!ですが、1つ忘れていることがあります: 6週間が経過したので、
次のバージョンのRust(1.6)の新しいベータも必要です。従って、stableがbetaから枝分かれした後に、
次のバージョンのbetaがnightlyから再度枝分かれします:
nightly: * - - * - - * - - * - - * - - * - * - *
| |
beta: * - - - - - - - - * *
|
stable: *
これが「トレイン・モデル」と呼ばれます。6週間ごとにリリースが「駅を出発する」からですが、 安定版リリースとして到着する前にベータチャンネルの旅をそれでもしなければなりません。
Rustは6週間ごとに時計仕掛けのようにリリースされます。あるRustリリースの日付を知っていれば、 次のリリースの日付もわかります: 6週間後です。6週間ごとにリリースを組むことのいい側面は、次の列車がすぐにやってくることです。 ある機能が偶然、特定のリリースを逃しても、心配する必要はありません: 別のリリースがすぐに起きます! これにより、リリースの締め切りが近い洗練されていない可能性のある機能をこっそり持ち込むプレッシャーが減る助けになるのです。
このプロセスのおかげで、Rustの次のビルドを常に確認し、アップグレードするのが容易であると自身に対して確かめることができます:
ベータリリースが予想した通りに動かなければ、チームに報告して、次の安定版のリリースが起きる前に直してもらうことができるのです!
ベータリリースでの破損はどちらかといえば稀ですが、rustcもソフトウェアの一種であり、バグは確実に存在します。
安定しない機能
このリリースモデルにはもう一つ掴み所があります: 安定しない機能です。Rustは「機能フラグ」と呼ばれるテクニックを使用して、
あるリリースで有効にする機能を決定します。新しい機能が活発に開発中なら、masterに着地し、
故にナイトリーでは機能フラグの背後に存在します。ユーザとして、絶賛作業中の機能を試したいとお望みならば、
可能ですが、ナイトリリリースのRustを使用し、ソースコードに適切なフラグを注釈して同意しなければなりません。
ベータか安定リリースのRustを使用しているなら、機能フラグは使用できません。これが、永遠に安定であると宣言する前に、 新しい機能を実用に供することができる鍵になっています。最先端を選択するのをお望みの方はそうすることができ、 岩のように硬い経験をお望みの方は、安定版に執着し自分のコードが壊れることはないとわかります。停滞なしの安定性です。
この本は安定な機能についての情報のみ含んでいます。現在進行形の機能は、変化中であり、 確実にこの本が執筆された時と安定版ビルドで有効化された時で異なるからです。ナイトリ限定の機能についてのドキュメンテーションは、 オンラインで発見できます。
RustupとRustナイトリの役目
rustupは、グローバルかプロジェクトごとにRustのリリースチャンネルを変更しやすくしてくれます。 標準では、安定版のRustがインストールされます。例えば、ナイトリをインストールするには:
$ rustup install nightly
rustupでインストールした全ツールチェーン(Rustのリリースと関連するコンポーネント)も確認できます。
こちらは、著者の一人のWindowsコンピュータの例です:
> rustup toolchain list
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc
おわかりのように、安定版のツールチェーンが標準です。ほとんどのRustユーザは、ほとんどの場合、安定版を使用します。
あなたもほとんどの場合安定版を使用したい可能性がありますが、最前線の機能が気になるので、特定のプロジェクトではナイトリを使用したいかもしれません。
そうするためには、そのプロジェクトのディレクトリでrustup overrideを使用して、そのディレクトリにいる時に、
rustupが使用するべきツールチェーンとしてナイトリ版のものをセットします。
$ cd ~/projects/needs-nightly
$ rustup override set nightly
これで ~/projects/needs-nightly内でrustcやcargoを呼び出す度に、rustupは既定の安定版のRustではなく、
ナイトリRustを使用していることを確かめます。Rustプロジェクトが大量にある時には、重宝します。
RFCプロセスとチーム
では、これらの新しい機能をどう習うのでしょうか?Rustの開発モデルは、Request For Comments (RFC; コメントの要求)プロセスに従っています。 Rustに改善を行いたければ、RFCと呼ばれる提案を書き上げます。
誰もがRFCを書いてRustを改善でき、提案はRustチームにより査読され議論され、このチームは多くの話題のサブチームから構成されています。 RustのWebサイトにはチームの完全なリストがあり、 プロジェクトの各分野のチームも含みます: 言語設計、コンパイラ実装、インフラ、ドキュメンテーションなどです。 適切なチームが提案とコメントを読み、自身のコメントを書き、最終的にその機能を受け入れるか拒否するかの同意があります。
機能が受け入れられれば、Rustリポジトリでissueが開かれ、誰かがそれを実装します。うまく実装できる人は、
そもそもその機能を提案した人ではないかもしれません!実装の準備ができたら、
「安定しない機能」節で議論したように、機能ゲートの背後のmasterに着地します。
時間経過後、一旦ナイトリリリースを使用するRust開発者が新しい機能を試すことができたら、チームのメンバーがその機能と、 ナイトリでどう機能しているかについて議論し、安定版のRustに導入すべきかどうか決定します。 決定が進行させることだったら、機能ゲートは取り除かれ、その機能はもう安定と考えられます! Rustの新しい安定版リリースまで、列車に乗っているのです。