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の機能を講義していますが、 最初その3章すら飛ばして、まっすぐに第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のエディションについて説明します。
この本を読む間違った方法なんてありません: 飛ばしたければ、どうぞご自由に! 混乱したら、前の章に戻らなければならない可能性もあります。ですが、自分に合った方法でどうぞ。
Rustを学ぶ過程で重要な部分は、コンパイラが表示するエラーメッセージを読む方法を学ぶことです: それは動くコードへと導いてくれます。そのため、各場面でコンパイラが表示するエラーメッセージとともに、 コンパイルできない例を多く提供します。適当に例を選んで走らせたら、コンパイルできないかもしれないことを知ってください! 周りのテキストを読んで実行しようとしている例がエラーになることを意図しているのか確認することを確かめてください。 フェリスもコードが動作するとは意図されていないコードを見分けるのを手助けしてくれます:
Ferris | Meaning |
---|---|
このコードはコンパイルできません! | |
このコードはパニックします! | |
このコードはアンセーフなコードを含みます。 | |
このコードは求められている振る舞いをしません。 |
ほとんどの場合、コンパイルできないあらゆるコードの正しいバージョンへと導きます。
ソースコード
この本が生成されるソースファイルは、GitHubで見つかります。
訳注: 日本語版はこちらです。
事始め
Rustの旅を始めましょう!学ぶべきことはたくさんありますが、いかなる旅もどこかから始まります。 この章では、以下のことを説明します:
- RustをLinux、macOS、Windowsにインストールする
Hello, world!
と表示するプログラムを書くcargo
というRustのパッケージマネージャ兼ビルドシステムを使用する
インストール
最初の手順は、Rustをインストールすることです。Rustは、Rustのバージョンと関連するツールを管理する、rustup
というコマンドラインツールを使用してダウンロードします。ダウンロードには、インターネットへの接続が必要になります。
注釈: なんらかの理由で
rustup
を使用したくない場合、Rustインストールページで、 他の選択肢をご覧になってください。
訳注:日本語版のRustインストールページはこちらです。
以下の手順で最新の安定版の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コンパイラは今のうちにインストールしておく価値があるかもしれません。
Windowsでrustup
をインストールする
Windowsでは、https://www.rust-lang.org/tools/installに行き、手順に従ってRustをインストールしてください。 インストールの途中で、Visual Studio 2013以降用のC++ビルドツールも必要になるという旨のメッセージが出るでしょう。 ビルドツールを取得する最も簡単な方法は、Visual Studio 2019用のビルドツールをインストールすることです。 どのワークロード (workloads) をインストールするかと質問されたときは、"C++ build tools"が選択されており、Windows 10 SDKと英語の言語パック (English language pack) が含まれていることを確かめてください。
訳注:Windowsの言語を日本語にしている場合は言語パックのところで「日本語」が選択されており、そのままの設定でインストールしても基本的に問題ないはずです。しかし、サードパーティーのツールやライブラリの中には英語の言語パックを必要とするものがあるため、「日本語」に加えて「英語」も選択することをお勧めします。
これ以降、cmd.exeとPowerShellの両方で動くコマンドを使用します。 特段の違いがあったら、どちらを使用すべきか説明します。
更新及びアンインストール
rustup
経由でRustをインストールしたなら、最新版へ更新するのは簡単です。
シェルから以下の更新スクリプトを実行してください:
$ rustup update
Rustとrustup
をアンインストールするには、シェルから以下のアンインストールスクリプトを実行してください:
$ rustup self uninstall
トラブルシューティング
Rustが正常にインストールされているか確かめるには、シェルを開いて以下の行を入力してください:
$ rustc --version
バージョンナンバー、コミットハッシュ、最新の安定版がリリースされたコミット日時が以下のフォーマットで表示されるのを目撃するはずです。
rustc x.y.z (abcabcabc yyyy-mm-dd)
この情報が見られたなら、Rustのインストールに成功しています!この情報が出ず、Windowsを使っているなら、
Rustが%PATH%
システム環境変数にあることを確認してください。これらが全て正常であるのに、それでもRustがうまく動かないなら、
助力を得られる場所はたくさんあります。最も簡単なのがRustの公式Discordの#beginnersチャンネルです。そのアドレスで、助けてくれる他のRustacean (Rustユーザが自分たちのことを呼ぶ、冗談めいたニックネーム) たちとチャットできます。
他にも、素晴らしいリソースとしてユーザ・フォーラムとStack Overflowが挙げられます。
訳注1:Rustaceanについて、いらないかもしれない補足です。公式Twitter曰く、Rustaceanはcrustaceans(甲殻類)から来ているそうです。 そのため、Rustのマスコットは(非公式らしいですが)カニ。上の会話でCの欠点を削ぎ落としているからcを省いてるの?みたいなことを聞いていますが、 違うそうです。検索したら、堅牢性が高いから甲殻類という意見もありますが、真偽は不明です。 明日使えるかもしれないトリビアでした。
訳注2:上にある公式Discordは英語話者のコミュニティです。日本語話者のためのコミュニティがZulip rust-lang-jpにあり、こちらでもRustaceanたちが活発に議論をしています。 公式Discord同様、初心者向けの#beginnersチャンネルが存在するので、気軽に質問してみてください。
ローカルのドキュメンテーション
インストールされたRustには、ローカルに複製されたドキュメンテーションのコピーが含まれているので、これをオフラインで閲覧することができます。
ブラウザでローカルのドキュメンテーションを開くには、rustup doc
を実行してください。
標準ライブラリにより提供される型や関数がなんなのかや、それをどう使えば良いのかがよくわからないときは、いつでもAPIのドキュメンテーションを検索してみてください!
Hello, World!
Rustをインストールしたので、最初のRustプログラムを書きましょう。新しい言語を学ぶ際に、
Hello, world!
というテキストを画面に出力する小さなプログラムを書くことは伝統的なことなので、
ここでも同じようにしましょう!
注釈: この本は、コマンドラインに基礎的な馴染みがあることを前提にしています。Rustは、編集やツール、 どこにコードがあるかについて特定の要求をしないので、コマンドラインではなくIDEを使用することを好むのなら、 どうぞご自由にお気に入りのIDEを使用してください。今では、多くのIDEがなんらかの形でRustをサポートしています; 詳しくは、IDEのドキュメンテーションをご覧ください。最近、Rustチームは優れたIDEサポートを有効にすることに注力し、 その前線で急激に成果があがっています!
プロジェクトのディレクトリを作成する
Rustコードを格納するディレクトリを作ることから始めましょう。Rustにとって、コードがどこにあるかは問題ではありませんが、 この本の練習とプロジェクトのために、ホームディレクトリにprojectsディレクトリを作成してプロジェクトを全てそこに保管することを推奨します。
端末を開いて以下のコマンドを入力し、projectsディレクトリと、 projectsディレクトリ内にHello, world!プロジェクトのディレクトリを作成してください。
LinuxとmacOSなら、こう入力してください:
$ 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
WindowsのPowerShellなら、こう:
> mkdir $env:USERPROFILE\projects
> cd $env: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!"); }
ファイルを保存し、端末ウィンドウに戻ってください。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() { }
これらの行でRustで関数を定義しています。main
関数は特別です: 常に全ての実行可能なRustプログラムで走る最初のコードになります。
1行目は、引数がなく、何も返さないmain
という関数を宣言しています。引数があるなら、かっこ(()
)の内部に入ります。
また、関数の本体が波括弧({}
)に包まれていることにも注目してください。Rustでは、全ての関数本体の周りにこれらが必要になります。
スペースを1つあけて、開き波括弧を関数宣言と同じ行に配置するのがいいスタイルです。
複数のRustプロジェクトに渡って標準的なスタイルにこだわりたいなら、rustfmt
を使うことでコードを決まったスタイルに整形できるでしょう。
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
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)、 そして、CMDを使用しているなら、.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"
[dependencies]
このファイルは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)に作成します。 以下のコマンドで実行ファイルを実行できます。
$ ./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が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); // 次のように予想しました: {}
}
このコードには多くの情報が詰め込まれています。
行ごとに見ていきましょう。
ユーザ入力を受け付け、結果を出力するためには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
もしかしたら異なるバージョンナンバー(とはいえ、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);
}
まず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!"), //やったね!
}
}
まず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;
}
}
}
}
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;
}
}
}
}
まとめ
数当てゲームを無事に作り上げることができました。 おめでとうございます!
このプロジェクトではハンズオンを通して、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
)を使用して、整数値の型を宣言することができます。
大きさ | 符号付き | 符号なし |
---|---|---|
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
)に付加することができます。
数値リテラル | 例 |
---|---|
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; }
関数定義も文になります。つまり、先の例は全体としても文になるわけです。
文は値を返しません。故に、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); }
この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!!!"); }
この文法要素により、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; } }
ここで、コードは配列の要素を順番にカウントアップして覗いています。番号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-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は有効ではない }
言い換えると、ここまでに重要な点は二つあります:
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; }
もしかしたら、何をしているのか予想することができるでしょう:
「値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つの部品でできています:
文字列の中身を保持するメモリへのポインタと長さ、そして、許容量です。この種のデータは、スタックに保持されます。
右側には、中身を保持したヒープ上のメモリがあります。
長さは、String
型の中身が現在使用しているメモリ量をバイトで表したものです。許容量は、
String
型がOSから受け取った全メモリ量をバイトで表したものです。長さと許容量の違いは問題になることですが、
この文脈では違うので、とりあえずは、許容量を無視しても構わないでしょう。
s1
をs2
に代入すると、String
型のデータがコピーされます。つまり、スタックにあるポインタ、長さ、
許容量をコピーするということです。ポインタが指すヒープ上のデータはコピーしません。言い換えると、
メモリ上のデータ表現は図4-2のようになるということです。
メモリ上の表現は、図4-3のようにはなりません。これは、
Rustが代わりにヒープデータもコピーするという選択をしていた場合のメモリ表現ですね。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に示してみました。
これにて一件落着です。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がスコープを抜ける。何も特別なことはない。
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が返され、呼び出し元関数にムーブされる }
変数の所有権は、毎回同じパターンを辿っています: 別の変数に値を代入すると、ムーブされます。
ヒープにデータを含む変数がスコープを抜けると、データが別の変数に所有されるようムーブされていない限り、
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) }
でも、これでは、大袈裟すぎますし、ありふれているはずの概念に対して、作業量が多すぎます。 私たちにとって幸運なことに、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はその図解です。
注釈:
&
による参照の逆は、参照外しであり、参照外し演算子の*
で達成できます。 第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");
}
これがエラーです:
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() {}
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は今や完全に無効なのだ! }
このプログラムは何のエラーもなくコンパイルが通り、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は、これを図解しています。
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);
}
もし、文字列スライスがあるなら、それを直接渡せます。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, } }
構造体を定義した後に使用するには、各フィールドに対して具体的な値を指定して構造体のインスタンスを生成します。
インスタンスは、構造体名を記述し、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, }; }
構造体から特定の値を得るには、ドット記法が使えます。このユーザの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"); }
インスタンス全体が可変でなければならないことに注意してください; 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, } } }
構造体のフィールドと同じ名前を関数の引数にもつけることは筋が通っていますが、
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, } } }
ここで、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-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のコードも、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 }
では、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 }
ある意味では、このプログラムはマシです。タプルのおかげで少し構造的になり、一引数を渡すだけになりました。 しかし別の意味では、このバージョンは明確性を失っています: タプルは要素に名前を付けないので、 計算が不明瞭になったのです。なぜなら、タプルの一部に添え字アクセスする必要があるからです。
面積計算で幅と高さを混在させるのなら問題はないのですが、長方形を画面に描画したいとなると、問題になるのです!
タプルの添え字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 }
ここでは、構造体を定義し、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);
}
このコードを走らせると、こんな感じのエラーが出ます:
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); }
これでプログラムを実行すれば、エラーは出ず、以下のような出力が得られるでしょう:
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() ); }
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));
}
そして、予期される出力は以下のようになります。なぜなら、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-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 } } }
ここでこれらのメソッドを個々の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"), }; }
ここでは、二つのフィールドを持つ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), } }
この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, } } }
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), } }
友人の一人が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); }
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"), _ => (), } }
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() {}
モジュールは、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
このツリーを見ると、どのモジュールがどのモジュールの中にネストしているのかがわかります(例えば、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();
}
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.
エラーメッセージは、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 のコードも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.
何が起きたのでしょう?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() {}
これでこのコードはコンパイルできます!
絶対パスと相対パスをもう一度確認して、どうして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() {}
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"); } }
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; } }
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() {}
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() {}
慣例に従った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-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); }
こちらの慣例の背後には、はっきりとした理由はありません。自然に発生した慣習であり、みんな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(()) } }
このように、親モジュールを使うことで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(()) } }
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() {}
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!"),
}
}
大きなプログラムにおいては、同じクレートやモジュールからのたくさんの要素をネストしたパスで持ち込むようにすれば、独立した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; }
これらの2つのパスの共通部分はstd::io
であり、そしてこれは最初のパスにほかなりません。これらの2つのパスを1つのuse
文へと合体させるには、Listing 7-20 に示されるように、ネストしたパスにself
を使いましょう。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::io::{self, Write}; }
この行は 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-22 のように、src/front_of_house.rs にはfront_of_house
モジュールの中身の定義を与えます。
ファイル名: src/front_of_house.rs
pub mod hosting {
pub fn add_to_waitlist() {}
}
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(); }
ここで、型注釈を付けていることに注目してください。
なぜなら、このベクタに対して何も値を挿入していないので、コンパイラには私たちがどんなデータを保持させるつもりか推測できないからです。
これは重要な点です。
ベクタはジェネリクスを使用して実装されています。
あなた自身の型でどうジェネリクスを使用するかついては第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]; }
初期値の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); }
第3章で説明したとおり、どんな変数でも、その値を変更したかったらmut
キーワードで可変にする必要があります。
中に配置する数値は全てi32
型であり、Rustはこのことをデータから推論するので、Vec<i32>
という注釈は不要です。
ベクタをドロップすれば、要素もドロップする
他のあらゆるstruct
(構造体)と同様に、ベクタもスコープを抜ければ解放されます。
その様子をリスト8-4に示します。
fn main() { { let v = vec![1, 2, 3, 4]; // vで作業をする } // <- vはここでスコープを抜け、解放される }
ベクタがドロップされると、その中身もドロップされます。 つまり、保持されていた整数値が片付けられるということです。 これは一見単純そうですが、ベクタの要素に対する参照を使い始めると少し複雑になり得ます。 次はそれに挑戦しましょう!
ベクタの要素を読む
ベクタを生成し、更新し、破棄する方法がわかったので、次のステップでは中身を読む方法について学ぶのが良いでしょう。 ベクタに保持された値を参照する方法は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."), } }
ここでは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); }
このコードを走らせると、最初の[]
メソッドはプログラムをパニックさせます。
なぜなら存在しない要素を参照しているからです。
このメソッドは、ベクタの終端を超えて要素にアクセスしようとしたときにプログラムをクラッシュさせたい場合に最適です。
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);
}
このコードをコンパイルすると、こんなエラーになります。
$ 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-9のfor
ループでは各要素に50
を足しています。
fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } }
可変参照が参照している値を変更するには、+=
演算子を使用する前に、参照外し演算子(*
)を使用して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), ]; }
個々の要素を格納するのにヒープ上で必要となるメモリの量を正確に把握するめに、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(); }
この行は、新しい空の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(); }
このコードは、initial contents
(初期値)を含む文字列を生成します。
さらに、String::from
関数を使っても、文字列リテラルからString
を生成することができます。
リスト8-13のコードは、to_string
を使用するリスト8-12のコードと等価です。
#![allow(unused)] fn main() { let s = String::from("initial contents"); }
文字列は、非常に多くのものに使用されるので、多くの異なる一般的な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"); }
これらは全て、有効な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"); }
この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); }
もし、push_str
メソッドがs2
の所有権を奪っていたら、最後の行でその値を出力することは不可能でしょう。
ところが、このコードは予想通りに動きます!
push
メソッドは、1文字を引数として取り、String
に追加します。リスト8-15は、
push
メソッドでlをString
に追加するコードを呈示しています。
#![allow(unused)] fn main() { let mut s = String::from("lo"); s.push('l'); }
このコードの結果、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はムーブされ、もう使用できないことに注意 }
このコードの結果、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];
このコードは、以下のようなエラーに落ち着きます:
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); }
最初に標準ライブラリのコレクション部分から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(); }
ここでは、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はこの時点で無効になる。試しに使ってみて // どんなコンパイルエラーが出るか確認してみて! }
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); }
ここで、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); }
このコードは、{"Blue": 25}
と出力するでしょう。10
という元の値は上書きされたのです。
キーに値がなかった時のみ値を挿入する
特定のキーに値があるか確認することは一般的であり、存在しない時に値を挿入することも一般的です。
ハッシュマップには、これを行うentry
と呼ばれる特別なAPIがあり、これは、引数としてチェックしたいキーを取ります。
このentry
メソッドの戻り値は、Entry
と呼ばれるenumであり、これは存在したりしなかったりする可能性のある値を表します。
イエローチームに対するキーに値が紐づけられているか否か確認したくなったとしましょう。存在しなかったら、
50という値を挿入したく、ブルーチームに対しても同様です。entry
APIを使用すれば、コードはリスト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); }
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); }
このコードは、{"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]; }
ここでは、ベクタの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>
出力が多いですね!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"); }
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) }, }; }
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
)
},
};
}
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), } } }
まずは、関数の戻り値型に注目してください: 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) } }
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) } }
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 } } }
まず、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); }
このコードは、整数のリストを変数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-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); }
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'); }
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);
}
直ちにこのコードをコンパイルしたら、以下のようなエラーが出ます:
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 }; }
構造体定義でジェネリクスを使用する記法は、関数定義のものと似ています。まず、山カッコ内に型引数の名前を構造体名の直後に宣言します。 そうすると、本来具体的なデータ型を記述する構造体定義の箇所に、ジェネリックな型を使用できます。
ジェネリックな型を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 };
}
この例で、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 }; }
これで、示された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()); }
ここで、フィールド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() } } }
このコードは、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); }
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; } }
ここでは、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) } } }
型にトレイトを実装することは、普通のメソッドを実装することに似ています。違いは、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) } } }
独自の実装を定義するのではなく、デフォルト実装を利用して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); }
もし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); } } } }
また、別のトレイトを実装するあらゆる型に対するトレイト実装を条件分けすることもできます。
トレイト境界を満たすあらゆる型にトレイトを実装することは、ブランケット実装(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-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); // |
} // ---------+
}
ここで、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); // | | // --+ | } // ----------+ }
ここで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);
}
関数に取ってほしい引数が文字列スライス、つまり参照であることに注意してください。
何故なら、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
}
}
代わりに、以下のようなライフタイムに言及するエラーが出ます:
$ 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-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 } }
この例において、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
}
}
このコードのコンパイルを試みると、こんなエラーになります:
$ 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, }; }
この構造体には文字列スライスを保持する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); }
この関数がライフタイム注釈なしでコンパイルできる理由には、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() {}
とりあえず、最初の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
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() {}
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'
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() {}
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() {}
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() {}
$ 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() {}
#[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() {}
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); } } }
これらのテストを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)); } } }
以前見かけたように、引数なしでテストを走らせたら、全テストが並行に走ります:
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)); } } }
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));
}
コードの頂点に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); }
まず、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); }
ベクタを出力した時に確認したように、プログラム名がベクタの最初の値、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日に告げるのなんて。
感服するような沼地にね!
テキストを適当な場所に置いて、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); }
最初に、もう何個か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)
}
それでもまだ、コマンドライン引数をベクタに集結させていますが、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 } }
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 } } }
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--
このコードは、リスト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 })
}
}
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--
このリストにおいて、以前には講義していないメソッドを使用しました: 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--
これで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(())
}
ここでは、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--
}
ここでは、寛大に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--
}
}
ライブラリクレートをバイナリクレートに持っていくのに、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) ); } } }
このテストは、"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![] } }
明示的なライフタイムの'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
}
}
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
}
}
}
合致した行を保存する
また、クエリ文字列を含む行を保存する方法が必要です。そのために、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
}
これで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) ); } } }
古いテストの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 } }
まず、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(()) } }
最後に、環境変数を確認する必要があります。環境変数を扱う関数は、標準ライブラリの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 }) } } }
ここで、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);
}
}
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 } }
次は、この例で重要なトレーニングアプリの部分を含む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) {}
簡潔性のために、変数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のコードには、遅い計算を行う関数への呼び出しが複数あります。最初の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 ); } } } }
この変更により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); }
クロージャ定義が=
に続き、変数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) ); } } } }
今では、重い計算はたった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 }; }
型注釈を付け加えると、クロージャの記法は、関数の記法により酷似して見えます。以下が、引数に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);
コンパイラは、次のエラーを返します:
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>, } }
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 }, } } } }
呼び出し元のコードにこれらのフィールドの値を直接変えてもらうのではなく、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) ); } } } }
クロージャを変数に直接保存する代わりに、クロージャを保持する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)); }
ここで、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(); }
一旦イテレータを生成したら、いろんな手段で使用することができます。第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); } }
標準ライブラリにより提供されるイテレータが存在しない言語では、変数を添え字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); } }
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); } }
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); }
出る警告は以下の通りです:
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]); }
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") }, ] ); } }
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 } } } }
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 } } } }
イテレータの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); } }
このテストは、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); } }
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 })
}
}
その際、将来的に除去する予定なので、非効率的な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--
}
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--
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 }) } }
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
}
イテレータアダプタメソッドを使用して、このコードをもっと簡潔に書くことができます。そうすれば、
可変な中間の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()
}
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
}
ここで、add_one
関数がすることの説明を与え、Examples
というタイトルでセクションを開始し、
add_one
関数の使用法を模擬するコードを提供しています。このドキュメンテーションコメントからcargo doc
を実行することで、
HTMLドキュメントを生成することができます。このコマンドはコンパイラとともに配布されているrustdoc
ツールを実行し、
生成されたHTMLドキュメントをtarget/docディレクトリに配置します。
利便性のために、cargo doc --open
を走らせれば、現在のクレートのドキュメント用のHTML(と、
自分のクレートが依存している全てのドキュメント)を構築し、その結果をWebブラウザで開きます。
add_one
関数まで下り、図14-1に示したように、ドキュメンテーションコメントのテキストがどう描画されるかを確認しましょう:
よく使われるセクション
# 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--
//!
で始まる最後の行のあとにコードがないことに注目してください。///
ではなく、//!
でコメントを開始しているので、
このコメントに続く要素ではなく、このコメントを含む要素にドキュメントをつけているわけです。
今回の場合、このコメントを含む要素はsrc/lib.rsファイルであり、クレートのルートです。
これらのコメントは、クレート全体を解説しています。
cargo doc --open
を実行すると、これらのコメントは、my_crate
のドキュメントの最初のページ、
クレートの公開要素のリストの上部に表示されます。図14-2のようにですね:
要素内のドキュメンテーションコメントは、特にクレートやモジュールを解説するのに有用です。 コンテナの全体の目的を説明し、クレートの使用者がクレートの体系を理解する手助けをするのに使用してください。
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は、cargo doc
により生成されるこのクレートのドキュメンテーションの最初のページがどんな見た目になるか示しています:
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
クレートを使用していますが、このコードの筆者は、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--
}
このクレートに対してcargo doc
が生成するAPIドキュメンテーションは、これで図14-4のようにトップページに再エクスポートを列挙しリンクするので、
PrimaryColor
型とSecondaryColor
型とmix
関数を見つけやすくします。
art
クレートのユーザは、それでも、リスト14-4にデモされているように、リスト14-3の内部構造を見て使用することもできますし、
リスト14-5のより便利な構造を使用することもできます。リスト14-6に示したようにですね:
ファイル名: src/main.rs
extern crate art;
use art::PrimaryColor;
use art::mix;
fn main() {
// --snip--
}
ネストされたモジュールがたくさんあるような場合、最上位階層で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));
}
最上位の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); }
変数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,
}
注釈: この例のために
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)));
}
最初の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`を表現可能にしてください)
エラーは、この型は「無限のサイズである」と表示しています。理由は、再帰的な列挙子を含む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のようにですね。
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)))))); }
Cons
列挙子は、1つのi32
のサイズに加えてボックスのポインタデータを格納する領域を必要とするでしょう。
Nil
列挙子は値を格納しないので、Cons
列挙子よりも必要な領域は小さいです。これで、
どんなList
値もi32
1つのサイズに加えてボックスのポインタデータのサイズを必要とすることがわかりました。
ボックスを使うことで無限に続く再帰の連鎖を断ち切ったので、コンパイラはList
値を格納するのに必要なサイズを計算できます。
図15-2は、Cons
列挙子の今の見た目を示しています。
ボックスは、間接参照とヒープメモリ確保だけを提供します。他のスマートポインタ型に見られるような別の特別な能力は何もありません。 これらの特別な能力が招くパフォーマンスのオーバーヘッドもないので、 コンスリストのように間接参照だけが必要な機能である場合には便利でしょう。 より多くのボックスのユースケースは第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); }
変数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とリスト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) } } }
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);
}
こちらが結果として出るコンパイルエラーです。
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 } } }
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); } }
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); }
ここで、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)[..]); }
(*m)
がMyBox<String>
をString
に参照外ししています。そして、&
と[..]
により、
文字列全体と等しいString
の文字列スライスを取り、hello
のシグニチャと一致するわけです。
参照外し型強制のないコードは、これらの記号が関係するので、読むのも書くのも理解するのもより難しくなります。
参照外し型強制により、コンパイラはこれらの変換を自動的に扱えるのです。
Deref
トレイトが関係する型に定義されていると、コンパイラは、型を分析し必要なだけDeref::deref
を使用して、
参照を得、引数の型と一致させます。Deref::deref
が挿入される必要のある回数は、コンパイル時に解決されるので、
参照外し型強制を活用するための実行時の代償は何もありません。
参照外し型強制が可変性と相互作用する方法
Deref
トレイトを使用して不変参照に対して*
をオーバーライドするように、
DerefMut
トレイトを使用して可変参照の*
演算子をオーバーライドできます。
以下の3つの場合に型やトレイト実装を見つけた時にコンパイラは、参照外し型強制を行います:
T: Deref<Target=U>
の時、&T
から&U
T: DerefMut<Target=U>
の時、&mut T
から&mut U
T: 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が生成された }
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.");
}
このコードをコンパイルしてみようとすると、こんなエラーが出ます:
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."); }
このコードを実行すると、以下のように出力されます:
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のような見た目になります:
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));
}
このコードをコンパイルすると、こんなエラーが出ます:
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)); }
初期化処理に含まれていないので、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)); }
プログラム内で参照カウントが変更される度に、参照カウントを出力します。参照カウントは、
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!"); } } } }
このコードの重要な部分の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); } } }
このテストコードは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); } } }
さて、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));
}
}
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); }
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-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の次の要素 = {:?} }
最初のリストが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に図式を作成しました:
最後の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)]), }); }
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()); }
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), ); }
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)); } }
この関数では、新しいスレッドは、実行が終わったかどうかにかかわらず、メインスレッドが終了したら停止することに注意してください。 このプログラムからの出力は毎回少々異なる可能性がありますが、だいたい以下のような感じでしょう:
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(); }
ハンドルに対して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();
}
クロージャは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();
}
このコードを実行できてしまうなら、立ち上げたスレッドはまったく実行されることなく即座にバックグラウンドに置かれる可能性があります。
立ち上げたスレッドは内部に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(); }
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(); }
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(); }); }
今回も、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); }
チャンネルの受信側には有用なメソッドが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);
}
ここで、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); } }
今回は、メインスレッドに送信したい文字列のベクタを立ち上げたスレッドが持っています。
それらを繰り返し、各々個別に送信し、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-- }
今回、最初のスレッドを立ち上げる前に、チャンネルの送信側に対して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); }
多くの型同様、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-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());
}
再三、コンパイルし……別のエラーが出ました!コンパイラはいろんなことを教えてくれています。
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()); }
このコードは、以下のように出力します:
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, } }
構造体は、他のコードが使用できるように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; } } }
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); } }
この記法は、第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>>, } }
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(); } } } }
これは、トレイト境界を伴うジェネリックな型引数を使用する構造体を定義するのとは異なる動作をします。
ジェネリックな型引数は、一度に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(); } } } }
こうすると、全てのコンポーネントの型が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 // 実際にボタンを描画するコード } } }
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
//セレクトボックスを実際に描画するコード
}
}
ライブラリの使用者はもう、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();
}
ライブラリを記述した時点では、誰かが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();
}
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());
}
ユーザが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 {} }
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); } } }
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 { "" } } }
この追加された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 } } }
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 } } }
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-- } }
目的は、これらの規則全てを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 } } }
空の文字列スライスを返すデフォルト実装を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); } } }
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, } } } }
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());
}
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"); } }
ユーザがお気に入りの色を指定したら、その色が背景色になります。今日が火曜日なら、背景色は緑です。 ユーザが年齢を文字列で指定し、数値として解析することができたら、背景色は、その数値の値によって紫かオレンジになります。 どの条件も適用できなければ、背景色は青になります:
この条件分岐構造により、複雑な要件をサポートさせてくれます。ここにあるハードコードされた値では、
この例は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); } }
この例は、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のコードは、以下のように出力するでしょう:
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); }
ここでタプルに対してパターンをマッチさせています。Rustは値(1, 2, 3)
をパターン(x, y, z)
と比較し、
値がパターンに合致すると確認するので、1
をx
に、2
をy
に、3
をz
に束縛します。
このタプルパターンを個別の3つの変数パターンが内部にネストされていると考えることもできます。
パターンの要素数がタプルの要素数と一致しない場合、全体の型が一致せず、コンパイルエラーになるでしょう。 例えば、リスト18-5は、3要素のタプルを2つの変数に分配しようとしているところを表示していて、動きません。
let (x, y) = (1, 2, 3);
このコードのコンパイルを試みると、このような型エラーに落ち着きます:
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 } }
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); }
このコードは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;
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-10のように、x
のような常にマッチするパターンをif let
に与えたら、コンパイルできないでしょう。
if let x = 5 {
println!("{}", x);
};
コンパイラは、論駁不可能なパターンと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); }
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); }
このコードは、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); }
このコードは、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), } }
最初のアームは、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 ) } } }
このコードは、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(); }
このコードは、値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); }
このコードは、最初の引数として渡された値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); }
このコードは、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) }, } }
このコードは、Some numbers: 2, 8, 32
と出力し、値4と16は無視されます。
名前を_
で始めて未使用の変数を無視する
変数を作っているのにどこでも使用していなければ、バグかもしれないのでコンパイラは通常、警告を発します。 しかし時として、まだ使用しない変数を作るのが有用なこともあります。プロトタイプを開発していたり、 プロジェクトを始めた直後だったりなどです。このような場面では、変数名をアンダースコアで始めることで、 コンパイラに未使用変数について警告しないよう指示することができます。リスト18-20で2つの未使用変数を生成していますが、 このコードを実行すると、そのうちの1つにしか警告が出ないはずです。
ファイル名: src/main.rs
fn main() { let _x = 5; let y = 10; }
ここで、変数y
を使用していないことに対して警告が出ていますが、アンダースコアが接頭辞になっている変数には、
使用していないという警告が出ていません。
_
だけを使うのとアンダースコアで始まる名前を使うことには微妙な違いがあることに注意してください。
_x
記法はそれでも、値を変数に束縛する一方で、_
は全く束縛しません。この差異が問題になる場合を示すために、
リスト18-21はエラーを提示するでしょう。
// こんにちは!
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
// 文字列が見つかりました
println!("found a string");
}
println!("{:?}", s);
それでも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); }
このコードは、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), } }
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); }, } }
このコードにおいて、最初と最後の値はfirst
とlast
に合致します。..
は、
途中のもの全部に合致し、無視します。
しかしながら、..
を使うのは明確でなければなりません。どの値がマッチしてどの値が無視されるべきかが不明瞭なら、
コンパイラはエラーを出します。リスト18-25は、..
を曖昧に使用する例なので、コンパイルできません。
ファイル名: src/main.rs
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {}", second)
},
}
}
この例をコンパイルすると、こんなエラーが出ます:
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);
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); }
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); }
この例はコンパイルが通り、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 => (), } }
この例は、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); }
このコードは今度は、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"), } }
マッチの条件は、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; }
このコードにはunsafe
キーワードを含めていないことに注意してください。safeコードで生ポインタを生成できます;
もうすぐわかるように、unsafeブロックの外では、生ポインタを参照外しできないだけなのです。
as
を使って不変と可変な参照を対応する生ポインタの型にキャストして生ポインタを生成しました。
有効であることが保証される参照から直接生ポインタを生成したので、これらの特定の生ポインタは有効であることがわかりますが、
その前提をあらゆる生ポインタに敷くことはできません。
次に、有効であることが確信できない生ポインタを生成します。リスト19-2は、メモリの任意の箇所を指す生ポインタの生成法を示しています。 任意のメモリを使用しようとすることは未定義です: そのアドレスにデータがある可能性もあるし、ない可能性もあり、 コンパイラがコードを最適化してメモリアクセスがなくなる可能性もあるし、プログラムがセグメンテーションフォールトでエラーになる可能性もあります。 通常、このようなコードを書くいい理由はありませんが、可能ではあります。
#![allow(unused)] fn main() { let address = 0x012345usize; let r = address as *const i32; }
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-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]); }
この関数を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..])
}
この関数はまず、スライスの全体の長さを得ます。それから引数で与えられた添え字が長さ以下であるかを確認してスライス内にあることをアサートします。 このアサートは、スライスを分割する添え字よりも大きい添え字を渡したら、その添え字を使用しようとする前に関数がパニックすることを意味します。
そして、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)) } } }
第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) }; }
この任意の場所のメモリは所有していなく、このコードが生成するスライスに有効な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)); } }
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); }
静的変数は、定数に似ています。定数については、第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); } }
普通の変数同様、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 // メソッドの実装がここに来る } }
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>; } }
型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<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 }); }
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)) } } }
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*"); } } }
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(); }
このコードを実行すると、*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(); }
メソッド名の前にトレイト名を指定すると、コンパイラにどの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()); }
このコードは、全ての子犬をスポットと名付けたいアニマル・シェルター(訳注
: 身寄りのないペットを保護する保健所みたいなところ)用で、
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());
}
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()); }
コンパイラに山カッコ内で型注釈を提供し、これは、この関数呼び出しでは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)); } } }
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); }
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とだけ相互作用するでしょう; そのコードは、内部でi32
IDを名前に代入していることを知る必要はないでしょう。
ニュータイプパターンは、カプセル化を実現して実装の詳細を隠匿する軽い方法であり、
実装の詳細を隠匿することは、第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-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(|| ()) } }
このコードの方が遥かに読み書きしやすいです!型エイリアスに意味のある名前を選択すると、 意図を伝えるのにも役に立つことがあります(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; } }
この時点では、このコードの詳細の一部を飛ばしました。第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); }
このコードは、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 } }; } }
標準ライブラリの
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 {
}
手続き的マクロを定義する関数は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();
}
このコードは完成したら、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)
}
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
)
}
)
}
この構造体のフィールドは、構文解析した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()
}
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のような見た目になります。
こちらが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!"); } }
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[..])); }
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(); } }
新しい最初の行に成功したメッセージのデータを保持する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>
これは、ヘッドとテキストのある最低限の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(); } }
先頭に行を追加して標準ライブラリの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 } } }
まず、/ リクエストに対応するデータを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(); } } }
ここでは、レスポンスにはステータスコード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>
これらの変更とともに、もう一度サーバを実行してください。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(); } }
これで、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-- } }
このコードはちょっと汚いですが、シミュレーション目的には十分です。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) {}
第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) {}
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-- } }
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--
}
ライブラリクレート内で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, } } } }
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, } } } }
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,
}
}
}
多少些細で単純な変更を行いました: チャンネルの受信側を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, } } } }
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-- }
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,
}
}
}
ここで、まず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,
}
}
}
まず、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,
}
}
}
このコードはコンパイルでき、動きますが、望み通りのスレッドの振る舞いにはなりません:
遅いリクエストがそれでも、他のリクエストが処理されるのを待機させてしまうのです。理由はどこか捉えがたいものです:
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();
}
}
}
まず、スレッドプール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),
}
}
}
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();
}
}
}
}
今では、ワーカーを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.");
}
現実世界の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により予約されています。
abstract
become
box
do
final
macro
override
priv
try
typeof
unsized
virtual
yield
生識別子
生識別子 とは、普段は使うことが許されないキーワードを使わせてくれる構文です。
生識別子はキーワードの前に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の演算子、演算子が文脈で現れる例、短い説明、その演算子がオーバーロード可能かどうかを含んでいます。 演算子がオーバーロード可能ならば、オーバーロードするのに使用する関係のあるトレイトも列挙されています。
演算子 | 例 | 説明 | オーバーロードできる? |
---|---|---|---|
! | 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は、単独で出現し、いろんな箇所で合法になる記号を示しています。
シンボル | 説明 |
---|---|
'ident | 名前付きのライフタイム、あるいはループラベル |
...u8 , ...i32 , ...f64 , ...usize など | 特定の型の数値リテラル |
"..." | 文字列リテラル |
r"..." , r#"..."# , r##"..."## など | 生文字列リテラル、エスケープ文字は処理されません |
b"..." | バイト文字列リテラル、文字列の代わりに[u8] を構築します |
br"..." , br#"..."# , br##"..."## など | 生バイト文字列リテラル、生文字列とバイト文字列の組み合わせ |
'...' | 文字リテラル |
b'...' | ASCIIバイトリテラル |
|...| expr | クロージャ |
! | 常に発散関数の空のボトム型 |
_ | 「無視」パターン束縛: 整数リテラルを見やすくするのにも使われる |
表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は、ジェネリックな型引数の文脈で出現する記号を示しています。
シンボル | 説明 |
---|---|
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は、ジェネリック型引数をトレイト境界で制約する文脈で出現する記号を示しています。
シンボル | 説明 |
---|---|
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は、マクロの呼び出しや定義、要素に属性を指定する文脈で出現する記号を示しています。
シンボル | 説明 |
---|---|
#[meta] | 外部属性 |
#![meta] | 内部属性 |
$ident | マクロ代用 |
$ident:kind | マクロキャプチャ |
$(…)… | マクロの繰り返し |
表B-7は、コメントを生成する記号を示しています。
シンボル | 説明 |
---|---|
// | 行コメント |
//! | 内部行docコメント |
/// | 外部行docコメント |
/*...*/ | ブロックコメント |
/*!...*/ | 内部ブロックdocコメント |
/**...*/ | 外部ブロックdocコメント |
タプル
表B-8は、タプルの文脈で出現する記号を示しています。
シンボル | 説明 |
---|---|
() | 空のタプル (ユニットとしても知られる)、リテラル、型両方 |
(expr) | 括弧付きの式 |
(expr,) | 1要素タプル式 |
(type,) | 1要素タプル型 |
(expr, ...) | タプル式 |
(type, ...) | タプル型 |
expr(expr, ...) | 関数呼び出し式; タプルstruct やタプルenum 列挙子を初期化するのにも使用される |
ident!(...) , ident!{...} , ident![...] | マクロ呼び出し |
expr.0 , expr.1 , など | タプル添え字アクセス |
表B-9は、波括弧が使用される文脈を表示しています。
文脈 | 説明 |
---|---|
{...} | ブロック式 |
Type {...} | struct リテラル |
表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の新しい安定版リリースまで、列車に乗っているのです。