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は英語話者のコミュニティです。日本語話者のためのコミュニティがslackにあり、こちらでも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!"); }
リスト1-1: 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]
リスト1-2:cargo new
で生成されたCargo.tomlの内容
このファイルはTOML(Tom's Obvious, Minimal Language、トムの明確な最小限の言語)形式で、Cargoの設定フォーマットです。
最初の行の[package]
はセクションヘッダーで、それ以降の文がパッケージを設定することを示します。
このファイルに情報を追加してく中で、他のセクションも追加していくことになります。
次の3行はCargoがプログラムをコンパイルするのに必要となる設定情報を指定します。
ここでは、名前、バージョン、使用するRustのエディションを指定しています。
edition
キーについては付録Eで説明されています。
最後の行の[dependencies]
は、プロジェクトの依存を列挙するためのセクションの始まりです。
Rustではコードのパッケージのことをクレートと呼びます。
このプロジェクトでは他のクレートは必要ありませんが、第2章の最初のプロジェクトでは必要になるので、そのときにこの依存セクションを使用します。
では、src/main.rsを開いて見てみましょう。
ファイル名: src/main.rs
fn main() { println!("Hello, world!"); }
Cargoはリスト1-1で書いたような「Hello, world!」プログラムを生成してくれています。 これまでのところ、以前のプロジェクトとCargoが生成したプロジェクトの違いは、Cargoがコードをsrcディレクトリに配置したことと、 最上位のディレクトリにCargo.toml設定ファイルがあることです。
Cargoはソースファイルがsrcディレクトリにあることを期待します。 プロジェクトの最上位のディレクトリは、READMEファイル、ライセンス情報、設定ファイル、その他のコードに関係しないものだけを置きます。 Cargoを使うとプロジェクトを整理することができます。 すべてのものに決まった場所があり、すべてがその場所にあるのです。
「Hello, world!」プロジェクトのようにCargoを使用しないプロジェクトを開始したときでも、Cargoを使用するプロジェクトへと変換できます。 プロジェクトのコードをsrcディレクトリに移動し、適切なCargo.tomlファイルを作成すればいいのです。
Cargoプロジェクトをビルドし、実行する
では「Hello, world!」プログラムをCargoでビルドして実行すると、何が違うのかを見てみましょう! hello_cargoディレクトリから以下のコマンドを入力して、プロジェクトをビルドします。
$ cargo build
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs
このコマンドは実行ファイルを現在のディレクトリではなく、target/debug/hello_cargo(Windowsではtarget/debug/hello_cargo.exe)に作成します。 以下のコマンドで実行ファイルを実行できます。
$ ./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); // 次のように予想しました: {}
}
リスト2-1:ユーザに予想を入力してもらい、それを出力するコード
このコードには多くの情報が詰め込まれています。
行ごとに見ていきましょう。
ユーザ入力を受け付け、結果を出力するためにはio
(入出力)ライブラリをスコープに入れる必要があります。
io
ライブラリは、std
と呼ばれる標準ライブラリに含まれています。
use std::io;
fn main() {
println!("Guess the number!"); // 数を当ててごらん
println!("Please input your guess."); // ほら、予想を入力してね
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line"); // 行の読み込みに失敗しました
println!("You guessed: {}", guess); // 次のように予想しました: {}
}
Rustはデフォルトで、標準ライブラリで定義されているアイテムの中のいくつかを、すべてのプログラムのスコープに取り込みます。 このセットはprelude(プレリュード)と呼ばれ、標準ライブラリのドキュメントでその中のすべてを見ることができます。
使いたい型がpreludeにない場合は、その型をuse
文で明示的にスコープに入れる必要があります。
std::io
ライブラリをuse
すると、ユーザ入力を受け付ける機能など(入出力に関する)多くの便利な機能が利用できるようになります。
第1章で見た通り、main
関数がプログラムへのエントリーポイント(訳注:スタート地点)になります。
use std::io;
fn main() {
println!("Guess the number!"); // 数を当ててごらん
println!("Please input your guess."); // ほら、予想を入力してね
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line"); // 行の読み込みに失敗しました
println!("You guessed: {}", guess); // 次のように予想しました: {}
}
fn
構文は関数を新しく宣言し、かっこの()
は引数がないことを示し、波括弧の{
は関数の本体を開始します。
また、第1章で学んだように、println!
は画面に文字列を表示するマクロです.
use std::io;
fn main() {
println!("Guess the number!"); // 数を当ててごらん
println!("Please input your guess."); // ほら、予想を入力してね
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line"); // 行の読み込みに失敗しました
println!("You guessed: {}", guess); // 次のように予想しました: {}
}
このコードはゲームの内容などを示すプロンプトを表示し、ユーザに入力を求めています。
値を変数に保持する
次に、ユーザの入力を格納するための変数を作りましょう。 こんな感じです。
use std::io;
fn main() {
println!("Guess the number!"); // 数を当ててごらん
println!("Please input your guess."); // ほら、予想を入力してね
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line"); // 行の読み込みに失敗しました
println!("You guessed: {}", guess); // 次のように予想しました: {}
}
プログラムが少し興味深いものになってきました。
この小さな行の中でいろいろなことが起きています。
let
文を使って変数を作っています。
別の例も見てみましょう。
let apples = 5;
この行ではapples
という名前の新しい変数を作成し5
という値に束縛しています。
Rustでは変数はデフォルトで不変(immutable)になります。
この概念については第3章の「変数と可変性」の節で詳しく説明します。
変数を可変(mutable)にするには、変数名の前にmut
をつけます。
let apples = 5; // immutable
// 不変
let mut bananas = 5; // mutable
// 可変
注:
//
構文は行末まで続くコメントを開始し、Rustはコメント内のすべて無視します。 コメントについては第3章で詳しく説明します。
数当てゲームのプログラムに戻りましょう。
ここまでの話でlet mut guess
がguess
という名前の可変変数を導入することがわかったと思います。
等号記号(=
)はRustに、いまこの変数を何かに束縛したいことを伝えます。
等号記号の右側にはguess
が束縛される値があります。
これはString::new
関数を呼び出すことで得られた値で、この関数はString
型の新しいインスタンスを返します。
String
は標準ライブラリによって提供される文字列型で、サイズが拡張可能な、UTF-8でエンコードされたテキスト片になります。
::new
の行にある::
構文はnew
がString
型の関連関数であることを示しています。
関連関数とは、ある型(ここではString
)に対して実装される関数のことです。
このnew
関数は新しい空の文字列を作成します。
new
関数は多くの型に見られます。
なぜなら、何らかの新しい値を作成する関数によくある名前だからです。
つまりlet mut guess = String::new();
という行は可変変数を作成し、その変数は現時点では新しい空のString
のインスタンスに束縛されているわけです。
ふう!
ユーザの入力を受け取る
プログラムの最初の行にuse std::io
と書いて、標準ライブラリの入出力機能を取り込んだことを思い出してください。
ここでio
モジュールのstdin
関数を呼び出して、ユーザ入力を処理できるようにしましょう。
use std::io;
fn main() {
println!("Guess the number!"); // 数を当ててごらん
println!("Please input your guess."); // ほら、予想を入力してね
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line"); // 行の読み込みに失敗しました
println!("You guessed: {}", guess); // 次のように予想しました: {}
}
もし、プログラムの最初にuse std::io
と書いてio
ライブラリをインポートしていなかったとしても、std::io::stdin
のように呼び出せば、この関数を利用できます。
stdin
関数はターミナルの標準入力へのハンドルを表す型であるstd::io::Stdin
のインスタンスを返します。
次の.read_line(&mut guess)
行は、標準入力ハンドルのread_line
メソッドを呼び出し、ユーザからの入力を得ています。
また、read_line
の引数として&mut guess
を渡し、ユーザ入力をどの文字列に格納するかを指示しています。
read_line
メソッドの仕事は、ユーザが標準入力に入力したものを文字列に(いまの内容を上書きせずに)追加することですので、文字列を引数として渡しているわけです。
引数の文字列は、その内容をメソッドが変更できるように、可変である必要があります。
この&
は、この引数が参照であることを示し、これによりコードの複数の部分が同じデータにアクセスしても、そのデータを何度もメモリにコピーしなくて済みます。
参照は複雑な機能(訳注:一部のプログラム言語では正しく使うのが難しい機能)ですが、Rustの大きな利点の一つは参照を安全かつ簡単に使用できることです。
このプログラムを完成させるのに、そのような詳細を知る必要はないしょう。
とりあえず知っておいてほしいのは、変数のように参照もデフォルトで不変であることです。
したがって、&guess
ではなく&mut guess
と書いて可変にする必要があります。
(参照については第4章でより詳しく説明します)
Result
型で失敗の可能性を扱う
まだ、このコードの行は終わってません。 これから説明するのはテキスト上は3行目になりますが、まだ一つの論理的な行の一部分に過ぎません。 次の部分はこのメソッドです。
use std::io;
fn main() {
println!("Guess the number!"); // 数を当ててごらん
println!("Please input your guess."); // ほら、予想を入力してね
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line"); // 行の読み込みに失敗しました
println!("You guessed: {}", guess); // 次のように予想しました: {}
}
このコードは、こう書くこともできました。
io::stdin().read_line(&mut guess).expect("Failed to read line");
しかし、長い行は読みづらいので分割したほうがよいでしょう。
.method_name()
構文でメソッドを呼び出すとき、長い行を改行と空白で分割するのが賢明なことがよくあります。
それでは、この行(expect()
メソッド)が何をするのか説明します。
前述したように、read_line
メソッドは渡された文字列にユーザが入力したものを入れます。
しかし、同時に値(この場合はio::Result
)も返します。
Rustの標準ライブラリにはResult
という名前の型がいくつかあります。
汎用のResult
と、io::Result
といったサブモジュール用の特殊な型などです。
これらのResult
型は列挙型になります。
列挙型はenumとも呼ばれ、取りうる値として決まった数の列挙子(variant)を持ちます。
列挙型はよくmatch
と一緒に使われます。
これは条件式の一種で、評価時に、列挙型の値がどの列挙子であるかに基づいて異なるコードを実行できるという便利なものです。
enumについては第6章で詳しく説明します。
これらのResult
型の目的は、エラー処理に関わる情報を符号化(エンコード)することです。
Result
の列挙子はOk
かErr
です。
Ok
列挙子は処理が成功したことを示し、Ok
の中には正常に生成された値が入っています。
Err
列挙子は処理が失敗したことを意味し、Err
には処理が失敗した過程や理由についての情報が含まれています。
Result
型の値にも、他の型と同様にメソッドが定義されています。
io::Result
のインスタンスにはexpect
メソッドがありますので、これを呼び出せます。
このio::Result
インスタンスがErr
の値の場合、expect
メソッドはプログラムをクラッシュさせ、引数として渡されたメッセージを表示します。
read_line
メソッドがErr
を返したら、それはおそらく基礎となるオペレーティング・システムに起因するものでしょう。
もしこのio::Result
オブジェクトがOk
値の場合、expect
メソッドはOk
列挙子が保持する戻り値を取り出して、その値だけを返してくれます。
こうして私たちはその値を使うことができるわけです。
今回の場合、その値はユーザ入力のバイト数になります。
もしexpect
メソッドを呼び出さなかったら、コンパイルはできるものの警告が出るでしょう。
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
(警告: 使用されなければならない`std::result::Result`が使用されていません)
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
Rustは私たちがread_line
から返されたResult
値を使用していないことを警告し、これはプログラムがエラーの可能性に対処していないことを示します。
警告を抑制する正しい方法は実際にエラー処理を書くことです。
しかし、現時点では問題が起きたときにこのプログラムをクラッシュさせたいだけなので、expect
が使えるわけです。
エラーからの回復については第9章で学びます。
println!
マクロのプレースホルダーで値を表示する
閉じ波かっこを除けば、ここまでのコードで説明するのは残り1行だけです。
use std::io;
fn main() {
println!("Guess the number!"); // 数を当ててごらん
println!("Please input your guess."); // ほら、予想を入力してね
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line"); // 行の読み込みに失敗しました
println!("You guessed: {}", guess); // 次のように予想しました: {}
}
この行はユーザの入力を現在保持している文字列を表示します。
一組の波括弧の{}
はプレースホルダーです。
{}
は値を所定の場所に保持する小さなカニのはさみだと考えてください。
波括弧をいくつか使えば複数の値を表示できます。
最初の波括弧の組はフォーマット文字列のあとに並んだ最初の値に対応し、2組目は2番目の値、というように続いていきます。
一回のprintln!
の呼び出しで複数の値を表示するなら次のようになります。
#![allow(unused)] fn main() { let x = 5; let y = 10; println!("x = {} and y = {}", x, y); }
このコードはx = 5 and y = 10
と表示するでしょう。
最初の部分をテストする
数当てゲームの最初の部分をテストしてみましょう。
cargo run
で走らせてください。
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
これで、キーボードからの入力を得て、それを表示するという、ゲームの最初の部分は完成になります。
秘密の数字を生成する
次にユーザが数当てに挑戦する秘密の数字を生成する必要があります。
この数字を毎回変えることで何度やっても楽しいゲームになります。
ゲームが難しくなりすぎないように1から100までの乱数を使用しましょう。
Rustの標準ライブラリには、まだ乱数の機能は含まれていません。
ですが、Rustの開発チームがこの機能を持つrand
クレートを提供してくれています。
クレートを使用して機能を追加する
クレートはRustソースコードを集めたものであることを思い出してください。
私たちがここまで作ってきたプロジェクトはバイナリクレートであり、これは実行可能ファイルになります。
rand
クレートはライブラリクレートです。
他のプログラムで使用するためのコードが含まれており、単独で実行することはできません。
Cargoがその力を発揮するのは外部クレートと連携するときです。
rand
を使ったコードを書く前に、Cargo.tomlファイルを編集してrand
クレートを依存関係に含める必要があります。
そのファイルを開いて、Cargoが作ってくれた[dependencies]
セクションヘッダの下に次の行を追加してください。
バージョンナンバーを含め、ここに書かれている通り正確にrand
を指定してください。
そうしないと、このチュートリアルのコード例が動作しないかもしれません。
ファイル名:Cargo.toml
rand = "0.8.3"
Cargo.tomlファイルでは、ヘッダに続くものはすべて、他のセクションが始まるまで続くセクションの一部になります。
(訳注:Cargo.tomlファイル内には複数のセクションがあり、各セクションは[ ]
で囲まれたヘッダ行から始まります)
[dependecies]
はプロジェクトが依存する外部クレートと必要とするバージョンをCargoに伝えます。
今回はrand
クレートを0.8.3
というセマンティックバージョン指定子で指定します。
Cargoはセマンティックバージョニング(SemVerと呼ばれることもあります)を理解しており、これはバージョンナンバーを記述するための標準です。
0.8.3
という数字は実際には^0.8.3
の省略記法で、0.8.3
以上0.9.0
未満の任意のバージョンを意味します。
Cargoはこれらのバージョンを、バージョン0.8.3
と互換性のある公開APIを持つものとみなします。
この仕様により、この章のコードが引き続きコンパイルできるようにしつつ、最新のパッチリリースを取得できるようになります。
0.9.0以降のバージョンは、以下の例で使用しているものと同じAPIを持つことを保証しません。
さて、コードを一切変えずに、次のリスト2-2のようにプロジェクトをビルドしてみましょう。
$ cargo build
Updating crates.io index
(crates.ioインデックスを更新しています)
Downloaded rand v0.8.3
(rand v0.8.3をダウンロードしています)
Downloaded libc v0.2.86
Downloaded getrandom v0.2.2
Downloaded cfg-if v1.0.0
Downloaded ppv-lite86 v0.2.10
Downloaded rand_chacha v0.3.0
Downloaded rand_core v0.6.2
Compiling rand_core v0.6.2
(rand_core v0.6.2をコンパイルしています)
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_chacha v0.3.0
Compiling rand v0.8.3
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
(guessing_game v0.1.0をコンパイルしています)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
リスト2-2:randクレートを依存として追加した後のcargo build
コマンドの出力
もしかしたら異なるバージョンナンバー(とはいえ、SemVerのおかげですべてのコードに互換性があります)や、 異なる行(オペレーティングシステムに依存します)が表示されるかもしれません。 また、行の順序も違うかもしれません。
外部依存を持つようになると、Cargoはその依存関係が必要とするすべてについて最新のバージョンをレジストリから取得します。 レジストリとはCrates.ioのデータのコピーです。 Crates.ioは、Rustのエコシステムにいる人たちがオープンソースのRustプロジェクトを投稿し、他の人が使えるようにする場所です。
レジストリの更新後、Cargoは[dependencies]
セクションにリストアップされているクレートをチェックし、まだ取得していないものがあればダウンロードします。
ここでは依存関係としてrand
だけを書きましたが、rand
が動作するために依存している他のクレートも取り込まれています。
クレートをダウンロードしたあと、Rustはそれらをコンパイルし、依存関係が利用できる状態でプロジェクトをコンパイルします。
何も変更せずにすぐにcargo build
コマンドを再度実行すると、Finished
の行以外は何も出力されないでしょう。
Cargoはすでに依存関係をダウンロードしてコンパイル済みであることを認識しており、また、あなたがCargo.tomlファイルを変更していないことも知っているからです。
さらに、Cargoはあなたがコードを何も変更していないことも知っているので、再コンパイルもしません。
何もすることがないので単に終了します。
src/main.rsファイルを開いて些細な変更を加え、それを保存して再度ビルドすると2行しか表示されません。
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs
これらの行はCargoがsrc/main.rsファイルへの小さな変更に対して、ビルドを更新していることを示しています。 依存関係は変わっていないので、Cargoは既にダウンロードしてコンパイルしたものが再利用できることを知っています。
Cargo.lockファイルで再現可能なビルドを確保する
Cargoはあなたや他の人があなたのコードをビルドするたびに、同じ生成物をリビルドできるようにするしくみを備えています。
Cargoは何も指示されない限り、指定したバージョンの依存のみを使用します。
たとえば来週rand
クレートのバージョン0.8.4が出て、そのバージョンには重要なバグ修正が含まれていますが、同時にあなたのコードを破壊するリグレッションも含まれているとします。
これに対応するため、Rustはcargo build
を最初に実行したときにCargo.lockファイルを作成します。
(いまのguessing_gameディレクトリにもあるはずです)
プロジェクトを初めてビルドするとき、Cargoは条件に合うすべての依存関係のバージョンを計算しCargo.lockファイルに書き込みます。
次にプロジェクトをビルドすると、CargoはCargo.lockファイルが存在することを確認し、バージョンを把握するすべての作業を再び行う代わりに、そこで指定されているバージョンを使います。
これにより再現性のあるビルドを自動的に行えます。
言い換えれば、Cargo.lockファイルのおかげで、あなたが明示的にアップグレードするまで、プロジェクトは0.8.3
を使い続けます。
クレートを更新して新バージョンを取得する
クレートを本当にアップグレードしたくなったときのために、Cargoはupdate
コマンドを提供します。
このコマンドはCargo.lockファイルを無視して、Cargo.tomlファイル内の全ての指定に適合する最新バージョンを算出します。
成功したらCargoはそれらのバージョンをCargo.lockファイルに記録します。
ただし、デフォルトでCargoは0.8.3
以上、0.9.0
未満のバージョンのみを検索します。
もしrand
クレートの新しいバージョンとして0.8.4
と0.9.0
の二つがリリースされていたなら、cargo update
を実行したときに以下のようなメッセージが表示されるでしょう。
$ cargo update
Updating crates.io index
(crates.ioインデックスを更新しています)
Updating rand v0.8.3 -> v0.8.4
(randクレートをv0.8.3 -> v0.8.4に更新しています)
Cargoは0.9.0
リリースを無視します。
またそのとき、Cargo.lockファイルが変更され、rand
クレートの現在使用中のバージョンが0.8.4
になったことにも気づくでしょう。
そうではなく、rand
のバージョン0.9.0
か、0.9.x
系のどれかを使用するには、Cargo.tomlファイルを以下のように変更する必要があります。
[dependencies]
rand = "0.9.0"
次にcargo build
コマンドを実行したとき、Cargoは利用可能なクレートのレジストリを更新し、あなたが指定した新しいバージョンに従ってrand
の要件を再評価します。
Cargoとそのエコシステムについては、まだ伝えたいことが山ほどありますが、それらについては第14章で説明します。 いまのところは、これだけ知っていれば十分です。 Cargoはライブラリの再利用をとても簡単にしてくれるので、Rustaceanが数多くのパッケージから構成された小さなプロジェクトを書くことが可能になっています。
乱数を生成する
rand
クレートを使って予想する数字を生成しましょう。
次のステップはsrc/main.rsファイルをリスト2-3のように更新することです。
ファイル名:src/main.rs
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
println!("The secret number is: {}", secret_number); //秘密の数字は次の通り: {}
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
リスト2-3:乱数を生成するコードの追加
まずuse rand::Rng
という行を追加します。
Rng
トレイトは乱数生成器が実装すべきメソッドを定義しており、それらのメソッドを使用するには、このトレイトがスコープ内になければなりません。
トレイトについて詳しくは第10章で解説します。
次に、途中に2行を追加しています。
最初の行ではrand::thread_rng
関数を呼び出して、これから使う、ある特定の乱数生成器を取得しています。
なお、この乱数生成器は現在のスレッドに固有で、オペレーティングシステムからシード値を得ています。
そして、この乱数生成器のgen_range
メソッドを呼び出しています。
このメソッドはuse rand::Rng
文でスコープに導入したRng
トレイトで定義されています。
gen_range
メソッドは範囲式を引数にとり、その範囲内の乱数を生成してくれます。
ここで使っている範囲式の種類は開始..終了
という形式で、下限値は含みますが上限値は含みません。
そのため、1から100までの数をリクエストするには1..101
と指定する必要があります。
あるいは、これと同等の1..=100
という範囲を渡すこともできます。
注:クレートのどのトレイトを
use
するかや、どのメソッドや関数を呼び出すかを知るために、各クレートにはその使い方を説明したドキュメントが用意されています。 Cargoのもう一つの素晴らしい機能は、cargo doc --open
コマンドを走らせると、すべての依存クレートが提供するドキュメントをローカルでビルドして、ブラウザで開いてくれることです。 たとえばrand
クレートの他の機能に興味があるなら、cargo doc --open
コマンドを実行して、左側のサイドバーにあるrand
をクリックしてください。
コードに追加した2行目は秘密の数字を表示します。 これはプログラムを開発している間のテストに便利ですが、最終版からは削除する予定です。 プログラムが始まってすぐに答えが表示されたらゲームになりませんからね!
試しにプログラムを何回か走らせてみてください。
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 2.53s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5
毎回異なる乱数を取得し、それらはすべて1から100の範囲内の数字になるはずです。 よくやりました!
予想と秘密の数字を比較する
さて、ユーザ入力と乱数が揃ったので両者を比較してみましょう。 このステップをリスト2-4に示します。 これから説明するように、このコードはまだコンパイルできないことに注意してください。
ファイル名:src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
// --snip--
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
println!("The secret number is: {}", secret_number);
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"), //小さすぎ!
Ordering::Greater => println!("Too big!"), //大きすぎ!
Ordering::Equal => println!("You win!"), //やったね!
}
}
リスト2-4:二つの数値を比較したときに返される可能性のある値を処理する
まずuse
文を追加して標準ライブラリからstd::cmp::Ordering
という型をスコープに導入しています。
Ordering
もenumの一つでLess
、Greater
、Equal
という列挙子を持っています。
これらは二つの値を比較したときに得られる3種類の結果です。
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
それからOrdering
型を使用する新しい5行をいちばん下に追加してしています。
cmp
メソッドは二つの値の比較を行い、比較できるものになら何に対しても呼び出せます。
比較対象への参照をとり、ここではguess
とsecret_number
を比較しています。
そしてuse
文でスコープに導入したOrdering
列挙型の列挙子を返します。
ここではmatch
式を使用しており、guess
とsecret_number
の値に対してcmp
を呼んだ結果返されたOrdering
の列挙子に基づき、次の動作を決定しています。
match
式は複数のアーム(腕)で構成されます。
各アームはマッチさせるパターンと、match
に与えられた値がそのアームのパターンにマッチしたときに実行されるコードで構成されます。
Rustはmatch
に与えられた値を受け取って、各アームのパターンを順に照合していきます。
パターンとmatch
式はRustの強力な機能で、コードか遭遇する可能性のあるさまざまな状況を表現し、それらすべてを確実に処理できるようにします。
これらの機能については、それぞれ第6章と第18章で詳しく説明します。
ここで使われているmatch
式に対して、例を通して順に見ていきましょう。
たとえばユーザが50と予想し、今回ランダムに生成された秘密の数字は38だったとしましょう。
コードが50と38を比較すると、50は38よりも大きいのでcmp
メソッドはOrdering::Greater
を返します。
match
式はOrdering::Greater
の値を取得し、各アームのパターンを吟味し始めます。
まず最初のアームのパターンであるOrdering::Less
を見て、Ordering::Greater
の値とOrdering::Less
がマッチしないことがわかります。
そのため、このアームのコードは無視して、次のアームに移ります。
次のアームのパターンはOrdering::Greater
で、これはOrdering::Greater
とマッチします!
このアームに関連するコードが実行され、画面にToo big!
と表示されます。
このシナリオでは最後のアームと照合する必要がないためmatch
式(の評価)は終了します。
ところがリスト2-4のコードはまだコンパイルできません。 試してみましょう。
$ cargo build
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_core v0.6.2
Compiling rand_chacha v0.3.0
Compiling rand v0.8.3
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types (型が合いません)
--> src/main.rs:22:21
|
22 | match guess.cmp(&secret_number) {
| ^^^^^^^^^^^^^^ expected struct `String`, found integer
| (構造体`std::string::String`を予期したけど、整数型変数が見つかりました)
|
= note: expected reference `&String`
found reference `&{integer}`
error[E0283]: type annotations needed for `{integer}`
--> src/main.rs:8:44
|
8 | let secret_number = rand::thread_rng().gen_range(1..101);
| ------------- ^^^^^^^^^ cannot infer type for type `{integer}`
| |
| consider giving `secret_number` a type
|
= note: multiple `impl`s satisfying `{integer}: SampleUniform` found in the `rand` crate:
- impl SampleUniform for i128;
- impl SampleUniform for i16;
- impl SampleUniform for i32;
- impl SampleUniform for i64;
and 8 more
note: required by a bound in `gen_range`
--> /Users/carolnichols/.cargo/registry/src/github.com-1ecc6299db9ec823/rand-0.8.3/src/rng.rs:129:12
|
129 | T: SampleUniform,
| ^^^^^^^^^^^^^ required by this bound in `gen_range`
help: consider specifying the type arguments in the function call
|
8 | let secret_number = rand::thread_rng().gen_range::<T, R>(1..101);
| ++++++++
Some errors have detailed explanations: E0283, E0308.
For more information about an error, try `rustc --explain E0283`.
error: could not compile `guessing_game` due to 2 previous errors (先の2つのエラーのため、`guessing_game`をコンパイルできませんでした)
このエラーの核心は型の不一致があると述べていることです。
Rustは強い静的型システムを持ちますが、型推論も備えています。
let guess = String::new()
と書いたとき、Rustはguess
がString
型であるべきと推論したので、私たちはその型を書かずに済みました。
一方でsecret_number
は数値型です。
Rustのいくつかの数値型は1から100までの値を表現でき、それらの型には32ビット数値のi32
、符号なしの32ビット数値のu32
、64ビット数値のi64
などがあります。
Rustのデフォルトはi32
型で、型情報をどこかに追加してRustに異なる数値型だと推論させない限りsecret_number
の型はこれになります。
エラーの原因はRustが文字列と数値型を比較できないためです。
最終的にはプログラムが入力として読み込んだString
を実数型に変換し、秘密の数字と数値として比較できるようにしたいわけです。
そのためにはmain
関数の本体に次の行を追加します。
ファイル名:src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
println!("The secret number is: {}", secret_number);
println!("Please input your guess.");
// --snip--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse()
.expect("Please type a number!"); //数値を入力してください!
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
その行とはこれのことです。
let guess: u32 = guess.trim().parse().expect("Please type a number!");
guess
という名前の変数を作成しています。
しかし待ってください、このプログラムには既にguess
という名前の変数がありませんでしたか?
たしかにありますが、Rustではguess
の前の値を新しい値で覆い隠す(shadowする)ことが許されているのです。
シャドーイング(shadowing)は、guess_str
とguess
のような重複しない変数を二つ作る代わりに、guess
という変数名を再利用させてくれるのです。
これについては第3章で詳しく説明しますが、今のところ、この機能はある型から別の型に値を変換するときによく使われることを知っておいてください。
この新しい変数をguess.trim().parse()
という式に束縛しています。
式の中にあるguess
は、入力が文字列として格納されたオリジナルのguess
変数を指しています。
String
インスタンスのtrim
メソッドは文字列の先頭と末尾の空白をすべて削除します。
これは数値データのみを表現できるu32
型とこの文字列を比較するために(準備として)行う必要があります。
ユーザは予想を入力したあとread_line
の処理を終えるためにEnterキーを押す必要がありますが、これにより文字列に改行文字が追加されます。
たとえばユーザが5と入力してEnterキーを押すと、guess
は5\n
になります。
この\n
は「改行」を表しています。(WindowsではEnterキーを押すとキャリッジリターンと改行が入り\r\n
となります)
trim
メソッドは\n
や\r\n
を削除するので、その結果5
だけになります。
文字列のparse
メソッドは文字列をパース(解析)して何らかの数値にします。
このメソッドは(文字列を)さまざまな数値型へとパースできるので、let guess: u32
としてRustに正確な数値型を伝える必要があります。
guess
の後にコロン(:
)を付けることで変数の型に注釈をつけることをRustに伝えています。
Rustには組み込みの数値型がいくつかあります。
ここにあるu32
は符号なし32ビット整数で、小さな正の数を表すデフォルトの型に適しています。
他の数値型については第3章で学びます。
さらに、このサンプルプログラムでは、u32
という注釈とsecret_number
変数との比較していることから、Rustはsecret_number
変数もu32
型であるべきだと推論しています。
つまり、いまでは二つの同じ型の値を比較することになるわけです!
parse
メソッドは論理的に数値に変換できる文字にしか使えないので、よくエラーになります。
たとえば文字列にA👍%
が含まれていたら数値に変換する術はありません。
解析に失敗する可能性があるため、parse
メソッドはread_line
メソッドと同様にResult
型を返します
(「Result
型で失敗の可能性を扱う」で説明しました)
今回もexpect
メソッドを使用してResult
型を同じように扱います。
parse
メソッドが文字列から数値を作成できなかったためにResult
型のErr
列挙子を返したら、expect
の呼び出しはゲームをクラッシュさせ、私たちが与えたメッセージを表示します。
parse
が文字列をうまく数値へ変換できたときはResult
型のOk
列挙子を返し、expect
はOk
値から欲しい数値を返してくれます。
さあ、プログラムを走らせましょう!
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
いい感じです! 予想の前にスペースを追加したにもかかわらず、プログラムはちゃんとユーザが76と予想したことを理解しました。 このプログラムを何回か走らせ、数字を正しく言い当てたり、大きすぎる数字や小さすぎる数字を予想したりといった、異なる種類の入力に対する動作の違いを検証してください。
現在、ゲームの大半は動作していますが、まだユーザは1回しか予想できません。 ループを追加して、その部分を変更しましょう!
ループで複数回の予想を可能にする
loop
キーワードは無限ループを作成します。
ループを追加してユーザが数字を予想する機会を増やします。
ファイル名:src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
// --snip--
println!("The secret number is: {}", secret_number);
loop {
println!("Please input your guess.");
// --snip--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
}
見ての通り予想入力のプロンプト以降をすべてループ内に移動しました。 ループ内の行をさらに4つのスペースでインデントして、もう一度プログラムを実行してください。 プログラムはいつまでも推測を求めるようになりましたが、実はこれが新たな問題を引き起こしています。 これではユーザが(ゲームを)終了できません!
ユーザはキーボードショートカットのctrl-cを使えば、いつでもプログラムを中断させられます。
しかし「予想と秘密の数字を比較する」のparse
で述べたように、この飽くなきモンスターから逃れる方法はもう一つあります。
ユーザが数字以外の答えを入力すればプログラムはクラッシュします。
それを利用して以下のようにすれば終了できます。
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 1.50s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError { kind: InvalidDigit }', src/main.rs:28:47
(スレッド'main'は'数字を入力してください!:ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:785でパニックしました)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
(注:`RUST_BACKTRACE=1`で走らせるとバックトレースを見れます)
quit
と入力すればゲームが終了しますが、数字以外の入力でもそうなります。
これは控えめに言っても最適ではありません。
私たちは正しい数字が予想されたときにゲームが停止するようにしたいのです。
正しい予想をした後に終了する
break
文を追加して、ユーザが勝ったらゲームが終了するようにプログラムしましょう。
ファイル名:src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
println!("The secret number is: {}", secret_number);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {}", guess);
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
You win!
の後にbreak
の行を追記することで、ユーザが秘密の数字を正確に予想したときにプログラムがループを抜けるようになりました。
ループはmain
関数の最後の部分なので、ループを抜けることはプログラムを抜けることを意味します。
不正な入力を処理する
このゲームの動作をさらに洗練させるために、ユーザが数値以外を入力したときにプログラムをクラッシュさせるのではなく、数値以外を無視してユーザが数当てを続けられるようにしましょう。
これはリスト2-5のように、String
からu32
にguess
を変換する行を変えることで実現できます。
ファイル名:src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
println!("The secret number is: {}", secret_number);
loop {
println!("Please input your guess.");
let mut guess = String::new();
// --snip--
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {}", guess);
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
リスト2-5:数値以外の予想を無視し、プログラムをクラッシュさせるのではなく、もう1回予想してもらう
expect
の呼び出しからmatch
式に切り替えて、エラーによるクラッシュからエラー処理へと移行します。
parse
がResult
型を返すことと、Result
がOk
とErr
の列挙子を持つ列挙型であることを思い出してください。
ここではmatch
式を、cmp
メソッドから返されるOrdering
を処理したときと同じように使っています。
もしparse
メソッドが文字列から数値への変換に成功したなら、結果の数値を保持するOk
値を返します。
このOk
値は最初のアームのパターンにマッチします。
match
式はparse
メソッドが生成してOk
値に格納したnum
の値を返します。
その数値は私たちが望んだように、これから作成する新しいguess
変数に収まります。
もしparse
メソッドが文字列から数値への変換に失敗したなら、エラーに関する詳細な情報を含むErr
値を返します。
このErr
値は最初のmatch
アームのOk(num)
パターンにはマッチしませんが、2番目のアームのErr(_)
パターンにはマッチします。
アンダースコアの_
はすべての値を受け付けます。
この例ではすべてのErr
値に対して、その中にどんな情報があってもマッチさせたいと言っているのです。
したがってプログラムは2番目のアームのコードであるcontinue
を実行します。
これはloop
の次の繰り返しに移り、別の予想を求めるようプログラムに指示します。
つまり実質的にプログラムはparse
メソッドが遭遇し得るエラーをすべて無視するようになります!
これでプログラム内のすべてが期待通りに動作するはずです。 試してみましょう。
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished dev [unoptimized + debuginfo] target(s) in 4.45s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!
素晴らしい!
最後にほんの少し手を加えれば数当てゲームは完成です。
このプログラムはまだ秘密の数字を表示していることを思い出してください。
テストには便利でしたが、これではゲームが台無です。
秘密の数字を表示しているprintln!
を削除しましょう。
最終的なコードをリスト2-6に示します。
ファイル名:src/main.rs
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
リスト2-6:数当てゲームの完全なコード
まとめ
数当てゲームを無事に作り上げることができました。 おめでとうございます!
このプロジェクトではハンズオンを通して、let
、match
、メソッド、関連関数、外部クレートの使いかたなど、多くの新しいRustの概念に触れました。
以降の章では、これらの概念についてより詳しく学びます。
第3章では変数、データ型、関数など多くのプログラミング言語が持つ概念を取り上げ、Rustでの使い方を説明します。
第4章ではRustを他の言語とは異なるものに特徴づける、所有権について説明します。
第5章では構造体とメソッドの構文について説明し、第6章では列挙型がどのように動くのかについて説明します。
一般的なプログラミングの概念
この章では、ほとんど全てのプログラミング言語で見られる概念を講義し、それらがRustにおいて、 どう動作するかを見ていきます。多くのプログラミング言語は、その核心において、いろいろなものを共有しています。 この章で提示する概念は、全てRustに固有のものではありませんが、Rustの文脈で議論し、 これらの概念を使用することにまつわる仕様を説明します。
具体的には、変数、基本的な型、関数、コメント、そして制御フローについて学びます。 これらの基礎は全てのRustプログラムに存在するものであり、それらを早期に学ぶことにより、強力な基礎を築くことになるでしょう。
キーワード
Rust言語にも他の言語同様、キーワードが存在し、これらは言語だけが使用できるようになっています。 これらの単語は、変数や関数名には使えないことを弁えておいてください。ほとんどのキーワードは、特別な意味を持っており、 自らのRustプログラムにおいて、様々な作業をこなすために使用することができます; いくつかは、紐付けられた機能がないものの、将来Rustに追加されるかもしれない機能用に予約されています。 キーワードの一覧は、付録Aで確認できます。
変数と可変性
第2章で触れた通り、変数は標準で不変になります。これは、 Rustが提供する安全性や簡便な並行性の利点を享受する形でコードを書くための選択の1つです。 ところが、まだ変数を可変にするという選択肢も残されています。 どのように、そしてなぜRustは不変性を推奨するのか、さらには、なぜそれとは違う道を選びたくなることがあるのか見ていきましょう。
変数が不変であると、値が一旦名前に束縛されたら、その値を変えることができません。
これを具体的に説明するために、projectsディレクトリにcargo new --bin variables
コマンドを使って、
variablesという名前のプロジェクトを生成しましょう。
それから、新規作成したvariablesディレクトリで、src/main.rsファイルを開き、 その中身を以下のコードに置き換えましょう。このコードはまだコンパイルできません:
ファイル名: src/main.rs
fn main() {
let x = 5;
println!("The value of x is: {}", x); // xの値は{}です
x = 6;
println!("The value of x is: {}", x);
}
これを保存し、cargo run
コマンドでプログラムを走らせてください。次の出力に示されているようなエラーメッセージを受け取るはずです:
error[E0384]: cannot assgin twice immutable variable `x`
(不変変数`x`に2回代入できません)
--> src/main.rs:4:5
|
2 | let x = 5;
| - first assignment to `x`
| (`x`への最初の代入)
3 | println!("The value of x is: {}", x);
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
この例では、コンパイラがプログラムに潜むエラーを見つけ出す手助けをしてくれることが示されています。 コンパイルエラーは、イライラすることもあるものですが、まだプログラムにしてほしいことを安全に行えていないだけということなのです; エラーが出るからといって、あなたがいいプログラマではないという意味ではありません! 経験豊富な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.30 secs
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 is: {}", x); }
このプログラムはまず、x
を5
という値に束縛します。それからlet x =
を繰り返すことでx
を覆い隠し、
元の値に1
を加えることになるので、x
の値は6
になります。
3番目のlet
文もx
を覆い隠し、以前の値に2
をかけることになるので、x
の最終的な値は12
になります。
このプログラムを走らせたら、以下のように出力するでしょう:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
Running `target/debug/variables`
The value of x is: 12
シャドーイングは、変数をmut
にするのとは違います。なぜなら、let
キーワードを使わずに、
誤ってこの変数に再代入を試みようものなら、コンパイルエラーが出るからです。let
を使うことで、
値にちょっとした加工は行えますが、その加工が終わったら、変数は不変になるわけです。
mut
と上書きのもう一つの違いは、再度let
キーワードを使用したら、実効的には新しい変数を生成していることになるので、
値の型を変えつつ、同じ変数名を使いまわせることです。例えば、
プログラムがユーザに何らかのテキストに対して空白文字を入力することで何個分のスペースを表示したいかを尋ねますが、
ただ、実際にはこの入力を数値として保持したいとしましょう:
#![allow(unused)] fn main() { let spaces = " "; let spaces = spaces.len(); }
この文法要素は、容認されます。というのも、最初のspaces
変数は文字列型であり、2番目のspaces
変数は、
たまたま最初の変数と同じ名前になったまっさらな変数のわけですが、数値型になるからです。故に、シャドーイングのおかげで、
異なる名前を思いつく必要がなくなるわけです。spaces_str
とspaces_num
などですね; 代わりに、
よりシンプルなspaces
という名前を再利用できるわけです。一方で、この場合にmut
を使おうとすると、
以下に示した通りですが、コンパイルエラーになるわけです:
let mut spaces = " ";
spaces = spaces.len();
変数の型を可変にすることは許されていないと言われているわけです:
error[E0308]: mismatched types (型が合いません)
--> src/main.rs:3:14
|
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected &str, found usize
| (&str型を予期しましたが、usizeが見つかりました)
|
= note: expected type `&str`
found type `usize`
さあ、変数が動作する方法を見てきたので、今度は変数が取りうるデータ型について見ていきましょう。
データ型
Rustにおける値は全て、何らかのデータ型になり、コンパイラがどんなデータが指定されているか知れるので、 そのデータの取り扱い方も把握できるというわけです。2種のデータ型のサブセットを見ましょう: スカラー型と複合型です。
Rustは静的型付き言語であることを弁えておいてください。つまり、
コンパイル時に全ての変数の型が判明している必要があるということです。コンパイラは通常、値と使用方法に基づいて、
使用したい型を推論してくれます。複数の型が推論される可能性がある場合、例えば、
第2章の「予想と秘密の数字を比較する」節でparse
メソッドを使ってString
型を数値型に変換した時のように、
複数の型が可能な場合には、型注釈をつけなければいけません。以下のようにですね:
#![allow(unused)] fn main() { let guess: u32 = "42".parse().expect("Not a number!"); // 数字ではありません! }
ここで型注釈を付けなければ、コンパイラは以下のエラーを表示し、これは可能性のある型のうち、 どの型を使用したいのかを知るのに、コンパイラがプログラマからもっと情報を得る必要があることを意味します:
error[E0282]: type annotations needed
(型注釈が必要です)
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ cannot infer type for `_`
| (`_`の型が推論できません)
|
= note: type annotations or generic parameter binding required
(注釈: 型注釈、またはジェネリクス引数束縛が必要です)
他のデータ型についても、様々な型注釈を目にすることになるでしょう。
スカラー型
スカラー型は、単独の値を表します。Rustには主に4つのスカラー型があります: 整数、浮動小数点数、論理値、最後に文字です。他のプログラミング言語でも、これらの型を見かけたことはあるでしょう。 Rustでの動作方法に飛び込みましょう。
整数型
整数とは、小数部分のない数値のことです。第2章で一つの整数型を使用しましたね。u32
型です。
この型定義は、紐付けられる値が、符号なし整数(符号付き整数はu
ではなく、i
で始まります)になり、
これは、32ビット分のサイズを取ります。表3-1は、Rustの組み込み整数型を表示しています。
符号付きと符号なし欄の各バリアント(例: i16
)を使用して、整数値の型を宣言することができます。
表3-1: Rustの整数型
大きさ | 符号付き | 符号なし |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
arch | isize | usize |
各バリアントは、符号付きか符号なしかを選べ、明示的なサイズを持ちます。符号付きと符号なしは、 数値が正負を持つかどうかを示します。つまり、数値が符号を持つ必要があるかどうか(符号付き)、または、 絶対に正数にしかならず符号なしで表現できるかどうか(符号なし)です。これは、数値を紙に書き下すのと似ています: 符号が問題になるなら、数値はプラス記号、またはマイナス記号とともに表示されます; しかしながら、 その数値が正数であると仮定することが安全なら、符号なしで表示できるわけです。符号付き数値は、 2の補数表現で保持されます(これが何なのか確信を持てないのであれば、ネットで検索することができます。 まあ要するに、この解説は、この本の範疇外というわけです)。
各符号付きバリアントは、-(2n - 1)以上2n - 1 - 1以下の数値を保持でき、
ここでnはこのバリアントが使用するビット数です。以上から、i8
型は-(27)から27 - 1まで、
つまり、-128から127までを保持できます。符号なしバリアントは、0以上2n - 1以下を保持できるので、
u8
型は、0から28 - 1までの値、つまり、0から255までを保持できることになります。
加えて、isize
とusize
型は、プログラムが動作しているコンピュータの種類に依存します:
64ビットアーキテクチャなら、64ビットですし、32ビットアーキテクチャなら、32ビットになります。
整数リテラル(訳注
: リテラルとは、見たままの値ということ)は、表3-2に示すどの形式でも記述することができます。
バイトリテラルを除く数値リテラルは全て、
型接尾辞(例えば、57u8
)と_
を見た目の区切り記号(例えば、1_000
)に付加することができます。
表3-2: Rustの整数リテラル
数値リテラル | 例 |
---|---|
10進数 | 98_222 |
16進数 | 0xff |
8進数 | 0o77 |
2進数 | 0b1111_0000 |
バイト (u8 だけ) | b'A' |
では、どの整数型を使うべきかはどう把握すればいいのでしょうか?もし確信が持てないのならば、
Rustの基準型は一般的にいい選択肢になります。整数型の基準はi32
型です: 64ビットシステム上でも、
この型が普通最速になります。isize
とusize
を使う主な状況は、何らかのコレクションにアクセスすることです。
浮動小数点型
Rustにはさらに、浮動小数点数に対しても、2種類の基本型があり、浮動小数点数とは数値に小数点がついたもののことです。
Rustの浮動小数点型は、f32
とf64
で、それぞれ32ビットと64ビットサイズです。基準型はf64
です。
なぜなら、現代のCPUでは、f32
とほぼ同スピードにもかかわらず、より精度が高くなるからです。
実際に動作している浮動小数点数の例をご覧ください:
ファイル名: src/main.rs
fn main() { let x = 2.0; // f64 let y: f32 = 3.0; // f32 }
浮動小数点数は、IEEE-754規格に従って表現されています。f32
が単精度浮動小数点数、
f64
が倍精度浮動小数点数です。
数値演算
Rustにも全数値型に期待されうる標準的な数学演算が用意されています: 足し算、引き算、掛け算、割り算、余りです。
以下の例では、let
文での各演算の使用方法をご覧になれます:
ファイル名: src/main.rs
fn main() { // 足し算 let sum = 5 + 10; // 引き算 let difference = 95.5 - 4.3; // 掛け算 let product = 4 * 30; // 割り算 let quotient = 56.7 / 32.2; // 余り let remainder = 43 % 5; }
これらの文の各式は、数学演算子を使用しており、一つの値に評価され、そして、変数に束縛されます。 付録BにRustで使える演算子の一覧が載っています。
論理値型
他の多くの言語同様、Rustの論理値型も取りうる値は二つしかありません: true
とfalse
です。
Rustの論理値型は、bool
と指定されます。
例です:
ファイル名: src/main.rs
fn main() { let t = true; let f: bool = false; // 明示的型注釈付きで }
論理値を使う主な手段は、条件式です。例えば、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"]; }
配列の要素にアクセスする
配列は、スタック上に確保される一塊のメモリです。添え字によって、 配列の要素にこのようにアクセスすることができます:
ファイル名: 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
fn main() {
let a = [1, 2, 3, 4, 5];
let index = 10;
let element = a[index];
println!("The value of element is: {}", element); // 要素の値は{}です
}
このコードをcargo run
で走らせると、以下のような結果になります:
$ cargo run
Compiling arrays v0.1.0 (file:///projects/arrays)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
Running `target/debug/arrays`
thread '<main>' panicked at 'index out of bounds: the len is 5 but the index is
10', src/main.rs:6
スレッド'<main>'は'範囲外アクセス: 長さは5ですが、添え字は10でした', src/main.rs:6
でパニックしました
note: Run with `RUST_BACKTRACE=1` for 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.28 secs
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.21 secs
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() { another_function(5, 6); } fn another_function(x: i32, y: i32) { println!("The value of x is: {}", x); println!("The value of y is: {}", y); }
この例では、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.31 secs
Running `target/debug/functions`
The value of x is: 5
The value of y is: 6
x
に対して値5
、y
に対して値6
を渡して関数を呼び出したので、この二つの文字列は、
この値で出力されました。
関数本体は、文と式を含む
関数本体は、文が並び、最後に式を置くか文を置くという形で形成されます。現在までには、 式で終わらない関数だけを見てきたわけですが、式が文の一部になっているものなら見かけましたね。Rustは、式指向言語なので、 これは理解しておくべき重要な差異になります。他の言語にこの差異はありませんので、文と式がなんなのかと、 その違いが関数本体にどんな影響を与えるかを見ていきましょう。
実のところ、もう文と式は使っています。文とは、なんらかの動作をして値を返さない命令です。 式は結果値に評価されます。ちょっと例を眺めてみましょう。
let
キーワードを使用して変数を生成し、値を代入することは文になります。
リスト3-1でlet y = 6;
は文です。
ファイル名: src/main.rs
fn main() { let y = 6; }
リスト3-1: 1文を含むmain
関数宣言
関数定義も文になります。つまり、先の例は全体としても文になるわけです。
文は値を返しません。故に、let
文を他の変数に代入することはできません。
以下のコードではそれを試みていますが、エラーになります:
ファイル名: src/main.rs
fn main() {
let x = (let y = 6);
}
このプログラムを実行すると、以下のようなエラーが出るでしょう:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[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
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`を使う変数宣言は、文です)
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^^^^^^^^^^^ help: remove these parentheses
|
= note: `#[warn(unused_parens)]` on by default
error: aborting due to 2 previous errors; 1 warning emitted
For more information about this error, try `rustc --explain E0658`.
error: could not compile `functions`
To learn more, run the command again with --verbose.
この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 x = 5; 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.30 secs
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
error: aborting due to previous error
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions`
To learn more, run the command again with --verbose.
メインのエラーメッセージである「型が合いません」でこのコードの根本的な問題が明らかになるでしょう。
関数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.31 secs
Running `target/debug/branches`
condition was true
number
の値を条件がfalse
になるような値に変更してどうなるか確かめてみましょう:
let number = 7;
再度プログラムを実行して、出力に注目してください:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
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
という値に評価され、コンパイラがエラーを投げます:
error[E0308]: mismatched types
(型が合いません)
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected bool, found integral variable
| (bool型を予期したのに、整数変数が見つかりました)
|
= note: expected type `bool`
found type `{integer}`
このエラーは、コンパイラは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.31 secs
Running `target/debug/branches`
number is divisible by 3
このプログラムを実行すると、if
式が順番に吟味され、最初に条件が真になった本体が実行されます。
6は2で割り切れるものの、number is devisible by 2
や、
else
ブロックのnumber is not divisible by 4, 3, or 2
という出力はされないことに注目してください。
それは、Rustが最初の真条件のブロックのみを実行し、
条件に合ったものが見つかったら、残りはチェックすらしないからです。
else if
式を使いすぎると、コードがめちゃくちゃになってしまうので、1つ以上あるなら、
コードをリファクタリングしたくなるかもしれません。これらのケースに有用なmatch
と呼ばれる、
強力なRustの枝分かれ文法要素については第6章で解説します。
let
文内でif
式を使う
if
は式なので、let
文の右辺に持ってくることができます。リスト3-2のようにですね。
ファイル名: src/main.rs
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; // numberの値は、{}です println!("The value of number is: {}", number); }
リスト3-2: if
式の結果を変数に代入する
このnumber
変数は、if
式の結果に基づいた値に束縛されます。このコードを走らせてどうなるか確かめてください:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.30 secs
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
アームは互換性のない値の型になり、
コンパイラがプログラム内で問題の見つかった箇所をスバリ指摘してくれます:
error[E0308]: if and else have incompatible types
(ifとelseの型に互換性がありません)
--> src/main.rs:4:18
|
4 | let number = if condition {
| __________________^
5 | | 5
6 | | } else {
7 | | "six"
8 | | };
| |_____^ expected integral variable, found &str
| (整数変数を予期しましたが、&strが見つかりました)
|
= note: expected type `{integer}`
found type `&str`
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章の「正しい予想をした後に終了する」節の数当てゲーム内でこれをして、ユーザが予想を的中させ、
ゲームに勝った時にプログラムを終了させたことを思い出してください。
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 = number - 1; } // 発射! println!("LIFTOFF!!!"); }
リスト3-3: 条件が真の間、コードを走らせるwhile
ループを使用する
この文法要素により、loop
、if
、else
、break
を使った時に必要になるネストがなくなり、
より明確になります。条件が真の間、コードは実行されます; そうでなければ、ループを抜けます.
for
でコレクションを覗き見る
while
要素を使って配列などのコレクションの要素を覗き見ることができます。例えば、リスト3-4を見ましょう。
ファイル名: src/main.rs
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { // 値は{}です println!("the value is: {}", a[index]); index = index + 1; } }
リスト3-4: while
ループでコレクションの各要素を覗き見る
ここで、コードは配列の要素を順番にカウントアップして覗いています。番号0から始まり、
配列の最終番号に到達するまでループします(つまり、index < 5
が真でなくなる時です)。
このコードを走らせると、配列内の全要素が出力されます:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
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.iter() { // 値は{}です println!("the value is: {}", element); } }
リスト3-5: for
ループを使ってコレクションの各要素を覗き見る
このコードを走らせたら、リスト3-4と同じ出力が得られるでしょう。より重要なのは、 コードの安全性を向上させ、配列の終端を超えてアクセスしたり、 終端に届く前にループを終えてアイテムを見逃してしまったりするバグの可能性を完全に排除したことです。
例えば、リスト3-4のコードで、a
配列からアイテムを1つ削除したのに、条件式をwhile index < 4
にするのを忘れていたら、
コードはパニックします。for
ループを使っていれば、配列の要素数を変えても、
他のコードをいじることを覚えておく必要はなくなるわけです。
for
ループのこの安全性と簡潔性により、Rustで使用頻度の最も高いループになっています。
リスト3-3でwhile
ループを使ったカウントダウンサンプルのように、一定の回数、同じコードを実行したいような状況であっても、
多くのRustaceanは、for
ループを使うでしょう。どうやってやるかといえば、
Range
型を使うのです。Range型は、標準ライブラリで提供される片方の数字から始まって、
もう片方の数字未満の数値を順番に生成する型です。
for
ループと、まだ話していない別のメソッドrev
を使って範囲を逆順にしたカウントダウンはこうなります:
ファイル名: src/main.rs
fn main() { for number in (1..4).rev() { println!("{}!", number); } println!("LIFTOFF!!!"); }
こちらのコードの方が少しいいでしょう?
まとめ
やりましたね!結構長い章でした: 変数、スカラー値と複合データ型、関数、コメント、if
式、そして、ループについて学びました!
この章で議論した概念について経験を積みたいのであれば、以下のことをするプログラムを組んでみてください:
- 温度を華氏と摂氏で変換する。
- フィボナッチ数列のn番目を生成する。
- クリスマスキャロルの定番、"The Twelve Days of Christmas"の歌詞を、 曲の反復性を利用して出力する。
次に進む準備ができたら、他の言語にはあまり存在しないRustの概念について話しましょう: 所有権です。
所有権を理解する
所有権はRustの最もユニークな機能であり、これのおかげでガベージコレクタなしで安全性担保を行うことができるのです。 故に、Rustにおいて、所有権がどう動作するのかを理解するのは重要です。この章では、所有権以外にも、関連する機能を いくつか話していきます: 借用、スライス、そして、コンパイラがデータをメモリにどう配置するかです。
所有権とは?
Rustの中心的な機能は、所有権です。この機能は、説明するのは簡単なのですが、言語の残りの機能全てにかかるほど 深い裏の意味を含んでいるのです。
全てのプログラムは、実行中にコンピュータのメモリの使用方法を管理する必要があります。プログラムが動作するにつれて、 定期的に使用されていないメモリを検索するガベージコレクションを持つ言語もありますが、他の言語では、 プログラマが明示的にメモリを確保したり、解放したりしなければなりません。Rustでは第3の選択肢を取っています: メモリは、コンパイラがコンパイル時にチェックする一定の規則とともに所有権システムを通じて管理されています。 どの所有権機能も、実行中にプログラムの動作を遅くすることはありません。
所有権は多くのプログラマにとって新しい概念なので、慣れるまでに時間がかかります。 嬉しいことに、Rustと、所有権システムの規則の経験を積むと、より自然に安全かつ効率的なコードを構築できるようになります。 その調子でいきましょう!
所有権を理解した時、Rustを際立たせる機能の理解に対する強固な礎を得ることになるでしょう。この章では、 非常に一般的なデータ構造に着目した例を取り扱うことで所有権を学んでいきます: 文字列です。
スタックとヒープ
多くのプログラミング言語において、スタックとヒープについて考える機会はそう多くないでしょう。 しかし、Rustのようなシステムプログラミング言語においては、値がスタックに積まれるかヒープに置かれるかは、 言語の振る舞い方や、特定の決断を下す理由などに影響以上のものを与えるのです。 この章の後半でスタックとヒープを交えて所有権の一部が解説されるので、ここでちょっと予行演習をしておきましょう。
スタックもヒープも、実行時にコードが使用できるメモリの一部になりますが、異なる手段で構成されています。 スタックは、得た順番に値を並べ、逆の順で値を取り除いていきます。これは、 last in, first out(
訳注
: あえて日本語にするなら、「最後に入れたものが最初に出てくる」といったところでしょうか)と呼ばれます。 お皿の山を思い浮かべてください: お皿を追加する時には、山の一番上に置き、お皿が必要になったら、一番上から1枚を取り去りますよね。 途中や一番下に追加したり、取り除いたりすることもできません。データを追加することは、 スタックにpushするといい、データを取り除くことは、スタックからpopすると表現します(訳注
: 日本語では単純に英語をそのまま活用してプッシュ、ポップと表現するでしょう)。データへのアクセス方法のおかげで、スタックは高速です: 新しいデータを置いたり、 データを取得する場所を探す必要が絶対にないわけです。というのも、その場所は常に一番上だからですね。 スタックを高速にする特性は他にもあり、それはスタック上のデータは全て既知の固定サイズでなければならないということです。
コンパイル時にサイズがわからなかったり、サイズが可変のデータについては、代わりにヒープに格納することができます。 ヒープは、もっとごちゃごちゃしています: ヒープにデータを置く時、あるサイズのスペースを求めます。 OSはヒープ上に十分な大きさの空の領域を見つけ、使用中にし、ポインタを返します。ポインタとは、その場所へのアドレスです。 この過程は、ヒープに領域を確保する(allocating on the heap)と呼ばれ、時としてそのフレーズを単にallocateするなどと省略したりします。 (
訳注
: こちらもこなれた日本語訳はないでしょう。allocateは「メモリを確保する」と訳したいところですが) スタックに値を積むことは、メモリ確保とは考えられません。ポインタは、既知の固定サイズなので、 スタックに保管することができますが、実データが必要になったら、ポインタを追いかける必要があります。レストランで席を確保することを考えましょう。入店したら、グループの人数を告げ、 店員が全員座れる空いている席を探し、そこまで誘導します。もしグループの誰かが遅れて来るのなら、 着いた席の場所を尋ねてあなたを発見することができます。
ヒープへのデータアクセスは、スタックのデータへのアクセスよりも低速です。 ポインタを追って目的の場所に到達しなければならないからです。現代のプロセッサは、メモリをあちこち行き来しなければ、 より速くなります。似た例えを続けましょう。レストランで多くのテーブルから注文を受ける給仕人を考えましょう。最も効率的なのは、 次のテーブルに移らずに、一つのテーブルで全部の注文を受け付けてしまうことです。テーブルAで注文を受け、 それからテーブルBの注文、さらにまたA、それからまたBと渡り歩くのは、かなり低速な過程になってしまうでしょう。 同じ意味で、プロセッサは、 データが隔離されている(ヒープではそうなっている可能性がある)よりも近くにある(スタックではこうなる)ほうが、 仕事をうまくこなせるのです。ヒープに大きな領域を確保する行為も時間がかかることがあります。
コードが関数を呼び出すと、関数に渡された値(ヒープのデータへのポインタも含まれる可能性あり)と、 関数のローカル変数がスタックに載ります。関数の実行が終了すると、それらの値はスタックから取り除かれます。
どの部分のコードがどのヒープ上のデータを使用しているか把握すること、ヒープ上の重複するデータを最小化すること、 メモリ不足にならないようにヒープ上の未使用のデータを掃除することは全て、所有権が解決する問題です。 一度所有権を理解したら、あまり頻繁にスタックとヒープに関して考える必要はなくなるでしょうが、 ヒープデータを管理することが所有権の存在する理由だと知っていると、所有権がありのままで動作する理由を 説明するのに役立つこともあります。
所有権規則
まず、所有権のルールについて見ていきましょう。 この規則を具体化する例を扱っていく間もこれらのルールを肝に銘じておいてください:
- Rustの各値は、所有者と呼ばれる変数と対応している。
- いかなる時も所有者は一つである。
- 所有者がスコープから外れたら、値は破棄される。
変数スコープ
第2章で、Rustプログラムの例はすでに見ています。もう基本的な記法は通り過ぎたので、
fn main() {
というコードはもう例に含みません。従って、例をなぞっているなら、
これからの例はmain
関数に手動で入れ込まなければいけなくなるでしょう。結果的に、例は少々簡潔になり、
定型コードよりも具体的な詳細に集中しやすくなります。
所有権の最初の例として、何らかの変数のスコープについて見ていきましょう。スコープとは、 要素が有効になるプログラム内の範囲のことです。以下のような変数があるとしましょう:
#![allow(unused)] fn main() { let s = "hello"; }
変数s
は、文字列リテラルを参照し、ここでは、文字列の値はプログラムのテキストとしてハードコードされています。
この変数は、宣言された地点から、現在のスコープの終わりまで有効になります。リスト4-1には、
変数s
が有効な場所に関する注釈がコメントで付記されています。
#![allow(unused)] fn main() { { // sは、ここでは有効ではない。まだ宣言されていない let s = "hello"; // sは、ここから有効になる // sで作業をする } // このスコープは終わり。もうsは有効ではない }
リスト4-1: 変数と有効なスコープ
言い換えると、ここまでに重要な点は二つあります:
s
がスコープに入ると、有効になる- スコープを抜けるまで、有効なまま
ここで、スコープと変数が有効になる期間の関係は、他の言語に類似しています。さて、この理解のもとに、
String
型を導入して構築していきましょう。
String
型
所有権の規則を具体化するには、第3章の「データ型」節で講義したものよりも、より複雑なデータ型が必要になります。 以前講義した型は全てスタックに保管され、スコープが終わるとスタックから取り除かれますが、 ヒープに確保されるデータ型を観察して、 コンパイラがどうそのデータを掃除すべきタイミングを把握しているかを掘り下げていきたいと思います。
ここでは、例としてString
型を使用し、String
型の所有権にまつわる部分に着目しましょう。
また、この観点は、標準ライブラリや自分で生成する他の複雑なデータ型にも適用されます。
String
型については、第8章でより深く議論します。
既に文字列リテラルは見かけましたね。文字列リテラルでは、文字列の値はプログラムにハードコードされます。
文字列リテラルは便利ですが、テキストを使いたいかもしれない場面全てに最適なわけではありません。一因は、
文字列リテラルが不変であることに起因します。別の原因は、コードを書く際に、全ての文字列値が判明するわけではないからです:
例えば、ユーザ入力を受け付け、それを保持したいとしたらどうでしょうか?このような場面用に、Rustには、
2種類目の文字列型、String
型があります。この型はヒープにメモリを確保するので、
コンパイル時にはサイズが不明なテキストも保持することができるのです。from
関数を使用して、
文字列リテラルからString
型を生成できます。以下のように:
#![allow(unused)] fn main() { let s = String::from("hello"); }
この二重コロンは、string_from
などの名前を使うのではなく、
String
型直下のfrom
関数を特定する働きをする演算子です。この記法について詳しくは、
第5章の「メソッド記法」節と、第7章の「モジュール定義」でモジュールを使った名前空間分けについて話をするときに議論します。
この種の文字列は、可変化することができます:
#![allow(unused)] fn main() { let mut s = String::from("hello"); s.push_str(", world!"); // push_str()関数は、リテラルをStringに付け加える println!("{}", s); // これは`hello, world!`と出力する }
では、ここでの違いは何でしょうか?なぜ、String
型は可変化できるのに、リテラルはできないのでしょうか?
違いは、これら二つの型がメモリを扱う方法にあります。
メモリと確保
文字列リテラルの場合、中身はコンパイル時に判明しているので、テキストは最終的なバイナリファイルに直接ハードコードされます。 このため、文字列リテラルは、高速で効率的になるのです。しかし、これらの特性は、 その文字列リテラルの不変性にのみ端を発するものです。残念なことに、コンパイル時にサイズが不明だったり、 プログラム実行に合わせてサイズが可変なテキスト片用に一塊のメモリをバイナリに確保しておくことは不可能です。
String
型では、可変かつ伸長可能なテキスト破片をサポートするために、コンパイル時には不明な量のメモリを
ヒープに確保して内容を保持します。つまり:
- メモリは、実行時にOSに要求される。
String
型を使用し終わったら、OSにこのメモリを返還する方法が必要である。
この最初の部分は、既にしています: String::from
関数を呼んだら、その実装が必要なメモリを要求するのです。
これは、プログラミング言語において、極めて普遍的です。
しかしながら、2番目の部分は異なります。ガベージコレクタ(GC)付きの言語では、GCがこれ以上、
使用されないメモリを検知して片付けるため、プログラマは、そのことを考慮する必要はありません。
GCがないなら、メモリがもう使用されないことを見計らって、明示的に返還するコードを呼び出すのは、
プログラマの責任になります。ちょうど要求の際にしたようにですね。これを正確にすることは、
歴史的にも難しいプログラミング問題の一つであり続けています。もし、忘れていたら、メモリを無駄にします。
タイミングが早すぎたら、無効な変数を作ってしまいます。2回解放してしまっても、バグになるわけです。
allocate
とfree
は完璧に1対1対応にしなければならないのです。
Rustは、異なる道を歩んでいます: ひとたび、メモリを所有している変数がスコープを抜けたら、
メモリは自動的に返還されます。こちらの例は、
リスト4-1のスコープ例を文字列リテラルからString
型を使うものに変更したバージョンになります:
#![allow(unused)] fn main() { { let s = String::from("hello"); // sはここから有効になる // sで作業をする } // このスコープはここでおしまい。sは // もう有効ではない }
String
型が必要とするメモリをOSに返還することが自然な地点があります: s
変数がスコープを抜ける時です。
変数がスコープを抜ける時、Rustは特別な関数を呼んでくれます。この関数は、drop
と呼ばれ、
ここにString
型の書き手はメモリを返還するコードを配置することができます。Rustは、閉じ波括弧で自動的にdrop
関数を呼び出します。
注釈: C++では、要素の生存期間の終了地点でリソースを解放するこのパターンを時に、 RAII(Resource Aquisition Is Initialization: リソースの獲得は、初期化である)と呼んだりします。 Rustの
drop
関数は、あなたがRAIIパターンを使ったことがあれば、馴染み深いものでしょう。
このパターンは、Rustコードの書かれ方に甚大な影響をもたらします。現状は簡単そうに見えるかもしれませんが、 ヒープ上に確保されたデータを複数の変数に使用させるようなもっと複雑な場面では、コードの振る舞いは、 予期しないものになる可能性もあります。これから、そのような場面を掘り下げてみましょう。
変数とデータの相互作用法: ムーブ
Rustにおいては、複数の変数が同じデータに対して異なる手段で相互作用することができます。 整数を使用したリスト4-2の例を見てみましょう。
#![allow(unused)] fn main() { let x = 5; let y = x; }
リスト4-2: 変数x
の整数値をy
に代入する
もしかしたら、何をしているのか予想することができるでしょう:
「値5
をx
に束縛する; それからx
の値をコピーしてy
に束縛する。」これで、
二つの変数(x
とy
)が存在し、両方、値は5
になりました。これは確かに起こっている現象を説明しています。
なぜなら、整数は既知の固定サイズの単純な値で、これら二つの5
という値は、スタックに積まれるからです。
では、String
バージョンを見ていきましょう:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; }
このコードは先ほどのコードに酷似していますので、動作方法も同じだと思い込んでしまうかもしれません:
要するに、2行目でs1
の値をコピーし、s2
に束縛するということです。ところが、
これは全く起こることを言い当てていません。
図4-1を見て、ベールの下でString
に何が起きているかを確かめてください。
String
型は、左側に示されているように、3つの部品でできています:
文字列の中身を保持するメモリへのポインタと長さ、そして、許容量です。この種のデータは、スタックに保持されます。
右側には、中身を保持したヒープ上のメモリがあります。
図4-1: s1
に束縛された"hello"
という値を保持するString
のメモリ上の表現
長さは、String
型の中身が現在使用しているメモリ量をバイトで表したものです。許容量は、
String
型がOSから受け取った全メモリ量をバイトで表したものです。長さと許容量の違いは問題になることですが、
この文脈では違うので、とりあえずは、許容量を無視しても構わないでしょう。
s1
をs2
に代入すると、String
型のデータがコピーされます。つまり、スタックにあるポインタ、長さ、
許容量をコピーするということです。ポインタが指すヒープ上のデータはコピーしません。言い換えると、
メモリ上のデータ表現は図4-2のようになるということです。
図4-2: s1
のポインタ、長さ、許容量のコピーを保持する変数s2
のメモリ上での表現
メモリ上の表現は、図4-3のようにはなりません。これは、
Rustが代わりにヒープデータもコピーするという選択をしていた場合のメモリ表現ですね。Rustがこれをしていたら、
ヒープ上のデータが大きい時にs2 = s1
という処理の実行時性能がとても悪くなっていた可能性があるでしょう。
図4-3: Rustがヒープデータもコピーしていた場合にs2 = s1
という処理が行なった可能性のあること
先ほど、変数がスコープを抜けたら、Rustは自動的にdrop
関数を呼び出し、
その変数が使っていたヒープメモリを片付けると述べました。しかし、図4-2は、
両方のデータポインタが同じ場所を指していることを示しています。これは問題です: s2
とs1
がスコープを抜けたら、
両方とも同じメモリを解放しようとします。これは二重解放エラーとして知られ、以前触れたメモリ安全性上のバグの一つになります。
メモリを2回解放することは、memory corruption (訳注
: メモリの崩壊。意図せぬメモリの書き換え) につながり、
セキュリティ上の脆弱性を生む可能性があります。
メモリ安全性を保証するために、Rustにおいてこの場面で起こることの詳細がもう一つあります。
確保されたメモリをコピーしようとする代わりに、コンパイラは、s1
が最早有効ではないと考え、
故にs1
がスコープを抜けた際に何も解放する必要がなくなるわけです。s2
の生成後にs1
を使用しようとしたら、
どうなるかを確認してみましょう。動かないでしょう:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
コンパイラが無効化された参照は使用させてくれないので、以下のようなエラーが出るでしょう:
error[E0382]: use of moved value: `s1`
(ムーブされた値の使用: `s1`)
--> src/main.rs:5:28
|
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value used here after move
| (ムーブ後にここで使用されています)
|
= note: move occurs because `s1` has type `std::string::String`, which does
not implement the `Copy` trait
(注釈: ムーブが起きたのは、`s1`が`std::string::String`という
`Copy`トレイトを実装していない型だからです)
他の言語を触っている間に"shallow copy"と"deep copy"という用語を耳にしたことがあるなら、
データのコピーなしにポインタと長さ、許容量をコピーするという概念は、shallow copyのように思えるかもしれません。
ですが、コンパイラは最初の変数をも無効化するので、shallow copyと呼ばれる代わりに、
ムーブとして知られているわけです。この例では、s1
はs2
にムーブされたと表現するでしょう。
以上より、実際に起きることを図4-4に示してみました。
図4-4: s1
が無効化された後のメモリ表現
これにて一件落着です。s2
だけが有効なので、スコープを抜けたら、それだけがメモリを解放して、
終わりになります。
付け加えると、これにより暗示される設計上の選択があります: Rustでは、 自動的にデータの"deep copy"が行われることは絶対にないわけです。それ故に、あらゆる自動コピーは、実行時性能の観点で言うと、 悪くないと考えてよいことになります。
変数とデータの相互作用法: クローン
仮に、スタック上のデータだけでなく、本当にString
型のヒープデータのdeep copyが必要ならば、
clone
と呼ばれるよくあるメソッドを使うことができます。メソッド記法については第5章で議論しますが、
メソッドは多くのプログラミング言語に見られる機能なので、以前に見かけたこともあるんじゃないでしょうか。
これは、clone
メソッドの動作例です:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); }
これは問題なく動作し、図4-3で示した動作を明示的に生み出します。ここでは、 ヒープデータが実際にコピーされています。
clone
メソッドの呼び出しを見かけたら、何らかの任意のコードが実行され、その実行コストは高いと把握できます。
何か違うことが起こっているなと見た目でわかるわけです。
スタックのみのデータ: コピー
まだ話題にしていない別の問題があります。 この整数を使用したコードは、一部をリスト4-2で示しましたが、うまく動作する有効なものです:
#![allow(unused)] fn main() { let x = 5; let y = x; println!("x = {}, y = {}", x, y); }
ですが、このコードは一見、今学んだことと矛盾しているように見えます:
clone
メソッドの呼び出しがないのに、x
は有効で、y
にムーブされませんでした。
その理由は、整数のようなコンパイル時に既知のサイズを持つ型は、スタック上にすっぽり保持されるので、
実際の値をコピーするのも高速だからです。これは、変数y
を生成した後にもx
を無効化したくなる理由がないことを意味します。
換言すると、ここでは、shallow copyとdeep copyの違いがないことになり、
clone
メソッドを呼び出しても、一般的なshallow copy以上のことをしなくなり、
そのまま放置しておけるということです。
RustにはCopy
トレイトと呼ばれる特別な注釈があり、
整数のようなスタックに保持される型に対して配置することができます(トレイトについては第10章でもっと詳しく話します)。
型がCopy
トレイトに適合していれば、代入後も古い変数が使用可能になります。コンパイラは、
型やその一部分でもDrop
トレイトを実装している場合、Copy
トレイトによる注釈をさせてくれません。
型の値がスコープを外れた時に何か特別なことを起こす必要がある場合に、Copy
注釈を追加すると、コンパイルエラーが出ます。
型にCopy
注釈をつける方法について学ぶには、付録Cの「導出可能なトレイト」をご覧ください。
では、どの型がCopy
なのでしょうか?ある型について、ドキュメントをチェックすればいいのですが、
一般規則として、単純なスカラー値の集合は何でもCopy
であり、メモリ確保が必要だったり、
何らかの形態のリソースだったりするものはCopy
ではありません。ここにCopy
の型の一部を並べておきます。
- あらゆる整数型。
u32
など。 - 論理値型である
bool
。true
とfalse
という値がある。 - あらゆる浮動小数点型、
f64
など。 - 文字型である
char
。 - タプル。ただ、
Copy
の型だけを含む場合。例えば、(i32, i32)
はCopy
だが、(i32, String)
は違う。
所有権と関数
意味論的に、関数に値を渡すことと、値を変数に代入することは似ています。関数に変数を渡すと、 代入のようにムーブやコピーされます。リスト4-3は変数がスコープに入ったり、 抜けたりする地点について注釈してある例です。
ファイル名: src/main.rs
fn main() { let s = String::from("hello"); // sがスコープに入る takes_ownership(s); // sの値が関数にムーブされ... // ... ここではもう有効ではない let x = 5; // xがスコープに入る makes_copy(x); // xも関数にムーブされるが、 // i32はCopyなので、この後にxを使っても // 大丈夫 } // ここでxがスコープを抜け、sもスコープを抜ける。ただし、sの値はムーブされているので、何も特別なことは起こらない。 // fn takes_ownership(some_string: String) { // some_stringがスコープに入る。 println!("{}", some_string); } // ここでsome_stringがスコープを抜け、`drop`が呼ばれる。後ろ盾してたメモリが解放される。 // fn makes_copy(some_integer: i32) { // some_integerがスコープに入る println!("{}", some_integer); } // ここでsome_integerがスコープを抜ける。何も特別なことはない。
リスト4-3: 所有権とスコープが注釈された関数群
takes_ownership
の呼び出し後にs
を呼び出そうとすると、コンパイラは、コンパイルエラーを投げるでしょう。
これらの静的チェックにより、ミスを犯さないでいられます。s
やx
を使用するコードをmain
に追加してみて、
どこで使えて、そして、所有権規則により、どこで使えないかを確認してください。
戻り値とスコープ
値を返すことでも、所有権は移動します。リスト4-4は、リスト4-3と似た注釈のついた例です。
ファイル名: src/main.rs
fn main() { let s1 = gives_ownership(); // gives_ownershipは、戻り値をs1に // ムーブする let s2 = String::from("hello"); // s2がスコープに入る let s3 = takes_and_gives_back(s2); // s2はtakes_and_gives_backにムーブされ // 戻り値もs3にムーブされる } // ここで、s3はスコープを抜け、ドロップされる。s2もスコープを抜けるが、ムーブされているので、 // 何も起きない。s1もスコープを抜け、ドロップされる。 fn gives_ownership() -> String { // gives_ownershipは、戻り値を // 呼び出した関数にムーブする let some_string = String::from("hello"); // some_stringがスコープに入る some_string // some_stringが返され、呼び出し元関数に // ムーブされる } // takes_and_gives_backは、Stringを一つ受け取り、返す。 fn takes_and_gives_back(a_string: String) -> String { // a_stringがスコープに入る。 a_string // a_stringが返され、呼び出し元関数にムーブされる }
リスト4-4: 戻り値の所有権を移動する
変数の所有権は、毎回同じパターンを辿っています: 別の変数に値を代入すると、ムーブされます。
ヒープにデータを含む変数がスコープを抜けると、データが別の変数に所有されるようムーブされていない限り、
drop
により片付けられるでしょう。
所有権を取り、またその所有権を戻す、ということを全ての関数でしていたら、ちょっとめんどくさいですね。 関数に値は使わせるものの所有権を取らないようにさせるにはどうするべきでしょうか。 返したいと思うかもしれない関数本体で発生したあらゆるデータとともに、再利用したかったら、渡されたものをまた返さなきゃいけないのは、 非常に煩わしいことです。
タプルで、複数の値を返すことは可能です。リスト4-5のようにですね。
ファイル名: src/main.rs
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); //'{}'の長さは、{}です println!("The length of '{}' is {}.", s2, len); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len()メソッドは、Stringの長さを返します (s, length) }
リスト4-5: 引数の所有権を返す
でも、これでは、大袈裟すぎますし、ありふれているはずの概念に対して、作業量が多すぎます。 私たちにとって幸運なことに、Rustにはこの概念に対する機能があり、参照と呼ばれます。
参照と借用
リスト4-5のタプルコードの問題は、String
型を呼び出し元の関数に戻さないと、calculate_length
を呼び出した後に、
String
オブジェクトが使えなくなることであり、これはString
オブジェクトがcalculate_length
にムーブされてしまうためでした。
ここで、値の所有権をもらう代わりに引数としてオブジェクトへの参照を取るcalculate_length
関数を定義し、
使う方法を見てみましょう:
ファイル名: src/main.rs
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // '{}'の長さは、{}です println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
まず、変数宣言と関数の戻り値にあったタプルコードは全てなくなったことに気付いてください。
2番目に、&s1
をcalcuate_length
に渡し、その定義では、String
型ではなく、&String
を受け取っていることに注目してください。
これらのアンド記号が参照であり、これのおかげで所有権をもらうことなく値を参照することができるのです。 図4-5はその図解です。
図4-5: String s1
を指す&String s
の図表
注釈:
&
による参照の逆は、参照外しであり、参照外し演算子の*
で達成できます。 第8章で参照外し演算子の使用例を眺め、第15章で参照外しについて詳しく議論します。
ここの関数呼び出しについて、もっと詳しく見てみましょう:
#![allow(unused)] fn main() { fn calculate_length(s: &String) -> usize { s.len() } let s1 = String::from("hello"); let len = calculate_length(&s1); }
この&s1
という記法により、s1
の値を参照する参照を生成することができますが、これを所有することはありません。
所有してないということは、指している値は、参照がスコープを抜けてもドロップされないということです。
同様に、関数のシグニチャでも、&
を使用して引数s
の型が参照であることを示しています。
説明的な注釈を加えてみましょう:
#![allow(unused)] fn main() { fn calculate_length(s: &String) -> usize { // sはStringへの参照 s.len() } // ここで、sはスコープ外になる。けど、参照しているものの所有権を持っているわけではないので // 何も起こらない }
変数s
が有効なスコープは、あらゆる関数の引数のものと同じですが、所有権はないので、s
がスコープを抜けても、
参照が指しているものをドロップすることはありません。関数が実際の値の代わりに参照を引数に取ると、
所有権をもらわないので、所有権を返す目的で値を返す必要はありません。
関数の引数に参照を取ることを借用と呼びます。現実生活のように、誰かが何かを所有していたら、 それを借りることができます。用が済んだら、返さなきゃいけないわけです。
では、借用した何かを変更しようとしたら、どうなるのでしょうか?リスト4-6のコードを試してください。 ネタバレ注意: 動きません!
ファイル名: src/main.rs
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
リスト4-6: 借用した値を変更しようと試みる
これがエラーです:
error[E0596]: cannot borrow immutable borrowed content `*some_string` as mutable
(エラー: 不変な借用をした中身`*some_string`を可変で借用できません)
--> error.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- use `&mut String` here to make mutable
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ cannot borrow as mutable
変数が標準で不変なのと全く同様に、参照も不変なのです。参照している何かを変更することは叶わないわけです。
可変な参照
一捻り加えるだけでリスト4-6のコードのエラーは解決します:
ファイル名: src/main.rs
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
始めに、s
をmut
に変えなければなりませんでした。そして、&mut s
で可変な参照を生成し、
some_string: &mut String
で可変な参照を受け入れなければなりませんでした。
ところが、可変な参照には大きな制約が一つあります: 特定のスコープで、ある特定のデータに対しては、 一つしか可変な参照を持てないことです。こちらのコードは失敗します:
ファイル名: src/main.rs
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
これがエラーです:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
(エラー: 一度に`s`を可変として2回以上借用することはできません)
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
| (最初の可変な参照はここ)
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
| (二つ目の可変な参照はここ)
6 |
7 | println!("{}, {}", r1, r2);
| -- first borrow later used here
error: aborting due to previous error
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership`
To learn more, run the command again with --verbose.
この制約は、可変化を許可するものの、それを非常に統制の取れた形で行えます。これは、新たなRustaceanにとっては、 壁です。なぜなら、多くの言語では、いつでも好きな時に可変化できるからです。
この制約がある利点は、コンパイラがコンパイル時にデータ競合を防ぐことができる点です。 データ競合とは、競合条件と類似していて、これら3つの振る舞いが起きる時に発生します:
- 2つ以上のポインタが同じデータに同時にアクセスする。
- 少なくとも一つのポインタがデータに書き込みを行っている。
- データへのアクセスを同期する機構が使用されていない。
データ競合は未定義の振る舞いを引き起こし、実行時に追いかけようとした時に特定し解決するのが難しい問題です。 しかし、Rustは、データ競合が起こるコードをコンパイルさえしないので、この問題が発生しないようにしてくれるわけです。
いつものように、波かっこを使って新しいスコープを生成し、同時並行なものでなく、複数の可変な参照を作ることができます。
#![allow(unused)] fn main() { let mut s = String::from("hello"); { let r1 = &mut s; } // r1はここでスコープを抜けるので、問題なく新しい参照を作ることができる let r2 = &mut s; }
可変と不変な参照を組み合わせることに関しても、似たような規則が存在しています。このコードはエラーになります:
let mut s = String::from("hello");
let r1 = &s; // 問題なし
let r2 = &s; // 問題なし
let r3 = &mut s; // 大問題!
これがエラーです:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
(エラー: `s`は不変で借用されているので、可変で借用できません)
--> borrow_thrice.rs:6:19
|
4 | let r1 = &s; // no problem
| - immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^ mutable borrow occurs here
7 | }
| - immutable borrow ends here
ふう!さらに不変な参照をしている間は、可変な参照をすることはできません。不変参照の使用者は、 それ以降に値が突然変わることなんて予想してません!しかしながら、複数の不変参照をすることは可能です。 データを読み込んでいるだけの人に、他人がデータを読み込むことに対して影響を与える能力はないからです。
これらのエラーは、時としてイライラするものではありますが、Rustコンパイラがバグの可能性を早期に指摘してくれ(それも実行時ではなくコンパイル時に)、 問題の発生箇所をズバリ示してくれるのだと覚えておいてください。そうして想定通りにデータが変わらない理由を追いかける必要がなくなります。
宙に浮いた参照
ポインタのある言語では、誤ってダングリングポインタを生成してしまいやすいです。ダングリングポインタとは、 他人に渡されてしまった可能性のあるメモリを指すポインタのことであり、その箇所へのポインタを保持している間に、 メモリを解放してしまうことで発生します。対照的にRustでは、コンパイラが、 参照がダングリング参照に絶対ならないよう保証してくれます: つまり、何らかのデータへの参照があったら、 コンパイラは参照がスコープを抜けるまで、データがスコープを抜けることがないよう確認してくれるわけです。
ダングリング参照作りを試してみますが、コンパイラはこれをコンパイルエラーで阻止します:
ファイル名: src/main.rs
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
こちらがエラーです:
error[E0106]: missing lifetime specifier
(エラー: ライフタイム指定子がありません)
--> main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no
value for it to be borrowed from
(助言: この関数の戻り値型は、借用した値を含んでいますが、借用される値がどこにもありません)
= help: consider giving it a 'static lifetime
('staticライフタイムを与えることを考慮してみてください)
このエラーメッセージは、まだ講義していない機能について触れています: ライフタイムです。 ライフタイムについては第10章で詳しく議論しますが、ライフタイムに関する部分を無視すれば、 このメッセージは、確かにこのコードが問題になる理由に関する鍵を握っています:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from.
dangle
コードの各段階で一体何が起きているのかを詳しく見ていきましょう:
ファイル名: src/main.rs
fn dangle() -> &String { // dangleはStringへの参照を返す
let s = String::from("hello"); // sは新しいString
&s // String sへの参照を返す
} // ここで、sはスコープを抜け、ドロップされる。そのメモリは消される。
// 危険だ
s
は、dangle
内で生成されているので、dangle
のコードが終わったら、s
は解放されてしまいますが、
そこへの参照を返そうとしました。つまり、この参照は無効なString
を指していると思われるのです。
よくないことです!コンパイラは、これを阻止してくれるのです。
ここでの解決策は、String
を直接返すことです:
#![allow(unused)] fn main() { fn no_dangle() -> String { let s = String::from("hello"); s } }
これは何の問題もなく動きます。所有権はムーブされ、何も解放されることはありません。
参照の規則
参照について議論したことを再確認しましょう:
- 任意のタイミングで、一つの可変参照か不変な参照いくつでものどちらかを行える。
- 参照は常に有効でなければならない。
次は、違う種類の参照を見ていきましょう: スライスです。
スライス型
所有権のない別のデータ型は、スライスです。スライスにより、コレクション全体ではなく、 その内の一連の要素を参照することができます。
ちょっとしたプログラミングの問題を考えてみましょう: 文字列を受け取って、その文字列中の最初の単語を返す関数を書いてください。 関数が文字列中に空白を見つけられなかったら、文字列全体が一つの単語に違いないので、文字列全体が返されるべきです。
この関数のシグニチャについて考えてみましょう:
fn first_word(s: &String) -> ?
この関数、first_word
は引数に&String
をとります。所有権はいらないので、これで十分です。
ですが、何を返すべきでしょうか?文字列の一部について語る方法が全くありません。しかし、
単語の終端の添え字を返すことができますね。リスト4-7に示したように、その方法を試してみましょう。
ファイル名: src/main.rs
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() {}
リスト4-7: String
引数へのバイト数で表された添え字を返すfirst_word
関数
String
の値を要素ごとに見て、空白かどうかを確かめる必要があるので、
as_bytes
メソッドを使って、String
オブジェクトをバイト配列に変換しています。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
次に、そのバイト配列に対して、iter
メソッドを使用してイテレータを生成しています:
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
イテレータについて詳しくは、第13章で議論します。今は、iter
は、コレクション内の各要素を返すメソッドであること、
enumerate
がiter
の結果をラップして、(結果をそのまま返す)代わりにタプルの一部として各要素を返すことを知っておいてください。
enumerate
から返ってくるタプルの第1要素は、添え字であり、2番目の要素は、(コレクションの)要素への参照になります。
これは、手動で添え字を計算するよりも少しだけ便利です。
enumerate
メソッドがタプルを返すので、Rustのあらゆる場所同様、パターンを使って、そのタプルを分配できます。
従って、for
ループ内で、タプルの添え字に対するi
とタプルの1バイトに対応する&item
を含むパターンを指定しています。
.iter().enumerate()
から要素への参照を取得するので、パターンに&
を使っています。
for
ループ内で、バイトリテラル表記を使用して空白を表すバイトを検索しています。空白が見つかったら、その位置を返します。
それ以外の場合、s.len()
を使って文字列の長さを返します。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
さて、文字列内の最初の単語の終端の添え字を見つけ出せるようになりましたが、問題があります。
usize
型を単独で返していますが、これは&String
の文脈でのみ意味を持つ数値です。
言い換えると、String
から切り離された値なので、将来的にも有効である保証がないのです。
リスト4-7のfirst_word
関数を使用するリスト4-8のプログラムを考えてください。
ファイル名: src/main.rs
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() { let mut s = String::from("hello world"); let word = first_word(&s); // word will get the value 5 // wordの中身は、値5になる s.clear(); // this empties the String, making it equal to "" // Stringを空にする。つまり、""と等しくする // word still has the value 5 here, but there's no more string that // we could meaningfully use the value 5 with. word is now totally invalid! // wordはまだ値5を保持しているが、もうこの値を正しい意味で使用できる文字列は存在しない。 // wordは今や完全に無効なのだ! }
リスト4-8: first_word
関数の呼び出し結果を保持し、String
の中身を変更する
このプログラムは何のエラーもなくコンパイルが通り、word
をs.clear()
の呼び出し後に使用しても、
コンパイルが通ります。word
はs
の状態に全く関連づけられていないので、その中身はまだ値5
のままです。
その値5
を変数s
に使用し、最初の単語を取り出そうとすることはできますが、これはバグでしょう。
というのも、s
の中身は、5
をword
に保存した後変わってしまったからです。
word
内の添え字がs
に格納されたデータと同期されなくなるのを心配することは、面倒ですし間違いになりやすいです!
これらの添え字の管理は、second_word
関数を書いたら、さらに難しくなります。
そのシグニチャは以下のようになるはずです:
fn second_word(s: &String) -> (usize, usize) {
今、私たちは開始と終端の添え字を追うようになりました。特定の状態のデータから計算されたが、 その状態に全く紐付けられていない値がさらに増えました。いつの間にか変わってしまうので、同期を取る必要のある、関連性のない変数が3つになってしまいました。
運のいいことに、Rustにはこの問題への解決策が用意されています: 文字列スライスです。
文字列スライス
文字列スライスとは、String
の一部への参照で、こんな見た目をしています:
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; }
これは、String
全体への参照を取ることに似ていますが、余計な[0..5]
という部分が付いています。
String
全体への参照ではなく、String
の一部への参照です。
[starting_index..ending_index]
と指定することで、角かっこに範囲を使い、スライスを生成できます。
ここで、starting_index
はスライスの最初の位置、ending_index
はスライスの終端位置よりも、
1大きい値です。内部的には、スライスデータ構造は、開始地点とスライスの長さを保持しており、
スライスの長さはending_index
からstarting_index
を引いたものに対応します。以上より、
let world = &s[6..11];
の場合には、world
はs
の添え字6のバイトへのポインタと5という長さを持つスライスになるでしょう。
図4-6は、これを図解しています。
図4-6: String
オブジェクトの一部を参照する文字列スライス
Rustの..
という範囲記法で、最初の番号(ゼロ)から始めたければ、2連ピリオドの前に値を書かなければいいです。
換言すれば、これらは等価です:
#![allow(unused)] fn main() { let s = String::from("hello"); let slice = &s[0..2]; let slice = &s[..2]; }
同様の意味で、String
の最後のバイトをスライスが含むのならば、末尾の数値を書かなければいいです。
つまり、これらは等価になります:
#![allow(unused)] fn main() { let s = String::from("hello"); let len = s.len(); let slice = &s[3..len]; let slice = &s[3..]; }
さらに、両方の値を省略すると、文字列全体のスライスを得られます。故に、これらは等価です:
#![allow(unused)] fn main() { let s = String::from("hello"); let len = s.len(); let slice = &s[0..len]; let slice = &s[..]; }
注釈: 文字列スライスの範囲添え字は、有効なUTF-8文字境界に置かなければなりません。 マルチバイト文字の真ん中で文字列スライスを生成しようとしたら、エラーでプログラムは落ちるでしょう。 この節では文字列スライスを導入することが目的なので、ASCIIのみを想定しています; UTF-8に関するより徹底した議論は、 第8章の「文字列でUTF-8エンコードされたテキストを格納する」節で行います。
これらの情報を念頭に、first_word
を書き直してスライスを返すようにしましょう。
文字列スライスを意味する型は、&str
と記述します:
ファイル名: src/main.rs
fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() {}
リスト4-7で取った方法と同じように、最初の空白を探すことで単語の終端の添え字を取得しています。 空白を発見したら、文字列の最初を開始地点、空白の添え字を終了地点として使用して文字列スライスを返しています。
これで、first_word
を呼び出すと、元のデータに紐付けられた単独の値を得られるようになりました。
この値は、スライスの開始地点への参照とスライス中の要素数から構成されています。
second_word
関数についても、スライスを返すことでうまくいくでしょう:
fn second_word(s: &String) -> &str {
これで、ずっと混乱しにくい素直なAPIになりました。なぜなら、String
への参照が有効なままであることをコンパイラが、
保証してくれるからです。最初の単語の終端添え字を得た時に、
文字列を空っぽにして先ほどの添え字が無効になってしまったリスト4-8のプログラムのバグを覚えていますか?
そのコードは、論理的に正しくないのですが、即座にエラーにはなりませんでした。問題は後になってから発生し、
それは空の文字列に対して、最初の単語の添え字を使用し続けようとした時でした。スライスならこんなバグはあり得ず、
コードに問題があるなら、もっと迅速に判明します。スライスバージョンのfirst_word
を使用すると、
コンパイルエラーが発生します:
ファイル名: src/main.rs
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error! (エラー!)
println!("the first word is: {}", word);
}
こちらがコンパイルエラーです:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
(エラー: 不変として借用されているので、`s`を可変で借用できません)
--> src/main.rs:18:5
|
16 | let word = first_word(&s);
| -- immutable borrow occurs here
| (不変借用はここで発生しています)
17 |
18 | s.clear(); // error!
| ^^^^^^^^^ mutable borrow occurs here
| (可変借用はここで発生しています)
19 |
20 | println!("the first word is: {}", word);
| ---- immutable borrow later used here
(不変借用はその後ここで使われています)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership`.
To learn more, run the command again with --verbose.
借用規則から、何かへの不変な参照がある時、さらに可変な参照を得ることはできないことを思い出してください。
clear
はString
を切り詰める必要があるので、可変な参照を得る必要があります。Rustはこれを認めないので、コンパイルが失敗します。
RustのおかげでAPIが使いやすくなるだけでなく、ある種のエラー全てを完全にコンパイル時に排除してくれるのです!
文字列リテラルはスライスである
文字列は、バイナリに埋め込まれると話したことを思い出してください。今やスライスのことを知ったので、 文字列リテラルを正しく理解することができます。
#![allow(unused)] fn main() { let s = "Hello, world!"; }
ここでのs
の型は、&str
です: バイナリのその特定の位置を指すスライスです。
これは、文字列が不変である理由にもなっています。要するに、&str
は不変な参照なのです。
引数としての文字列スライス
リテラルやString
値のスライスを得ることができると知ると、first_word
に対して、もう一つ改善点を見出すことができます。
シグニチャです:
fn first_word(s: &String) -> &str {
もっと経験を積んだRustaceanなら、代わりにリスト4-9のようなシグニチャを書くでしょう。というのも、こうすると、
同じ関数を&String
値と&str
値両方に使えるようになるからです。
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// first_word works on slices of `String`s
// first_wordは`String`のスライスに対して機能する
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word works on slices of string literals
// first_wordは文字列リテラルのスライスに対して機能する
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
// 文字列リテラルは「それ自体すでに文字列スライスなので」、
// スライス記法なしでも機能するのだ!
let word = first_word(my_string_literal);
}
リスト4-9: s
引数の型に文字列スライスを使用してfirst_word
関数を改善する
もし、文字列スライスがあるなら、それを直接渡せます。String
があるなら、
そのString
全体のスライスを渡せます。String
への参照の代わりに文字列スライスを取るよう関数を定義すると、
何も機能を失うことなくAPIをより一般的で有益なものにできるのです。
Filename: src/main.rs
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("hello world"); // first_word works on slices of `String`s // first_wordは`String`のスライスに対して機能する let word = first_word(&my_string[..]); let my_string_literal = "hello world"; // first_word works on slices of string literals // first_wordは文字列リテラルのスライスに対して機能する let word = first_word(&my_string_literal[..]); // Because string literals *are* string slices already, // this works too, without the slice syntax! // 文字列リテラルは「それ自体すでに文字列スライスなので」、 // スライス記法なしでも機能するのだ! let word = first_word(my_string_literal); }
他のスライス
文字列リテラルは、ご想像通り、文字列に特化したものです。ですが、もっと一般的なスライス型も存在します。 この配列を考えてください:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; }
文字列の一部を参照したくなる可能性があるのと同様、配列の一部を参照したくなる可能性もあります。 以下のようにすれば、参照することができます:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; }
このスライスは、&[i32]
という型になります。これも文字列スライスと同じように動作します。
つまり、最初の要素への参照と長さを保持するのです。
この種のスライスは、他のすべての種類のコレクションに対して使用することになるでしょう。
それらのコレクションについて、詳しくは、第8章でベクタについて話すときに議論します。
まとめ
所有権、借用、スライスの概念は、Rustプログラムにおいて、コンパイル時にメモリ安全性を保証します。 Rust言語も他のシステムプログラミング言語と同じように、メモリの使用法について制御させてくれるわけですが、 データの所有者がスコープを抜けたときに、所有者に自動的にデータを片付けさせることは、この制御をするために、 余計なコードを書いたりデバッグしたりする必要がないことを意味します。
所有権は、Rustの他のいろんな部分が動作する方法に影響を与えるので、これ以降もこれらの概念についてさらに語っていく予定です。
第5章に移って、struct
でデータをグループ化することについて見ていきましょう。
構造体を使用して関係のあるデータを構造化する
structまたは、構造体は、意味のあるグループを形成する複数の関連した値をまとめ、名前付けできる独自のデータ型です。 あなたがオブジェクト指向言語に造詣が深いなら、structはオブジェクトのデータ属性みたいなものです。 この章では、タプルと構造体を対照的に比較し、構造体の使用法をデモし、メソッドや関連関数を定義して、 構造体のデータに紐付く振る舞いを指定する方法について議論します。構造体とenum(第6章で議論します)は、 自分のプログラム領域で新しい型を定義し、Rustのコンパイル時型精査機能をフル活用する構成要素になります。
構造体を定義し、インスタンス化する
構造体は第3章で議論したタプルと似ています。タプル同様、構造体の一部を異なる型にできます。 一方タプルとは違って、各データ片には名前をつけるので、値の意味が明確になります。 この名前のおかげで、構造体はタプルに比して、より柔軟になるわけです: データの順番に頼って、 インスタンスの値を指定したり、アクセスしたりする必要がないのです。
構造体の定義は、struct
キーワードを入れ、構造体全体に名前を付けます。構造体名は、
一つにグループ化されるデータ片の意義を表すものであるべきです。そして、波かっこ内に、
データ片の名前と型を定義し、これはフィールドと呼ばれます。例えば、リスト5-1では、
ユーザアカウントに関する情報を保持する構造体を示しています。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } }
リスト5-1: User
構造体定義
構造体を定義した後に使用するには、各フィールドに対して具体的な値を指定して構造体のインスタンスを生成します。
インスタンスは、構造体名を記述し、key: value
ペアを含む波かっこを付け加えることで生成します。
ここで、キーはフィールド名、値はそのフィールドに格納したいデータになります。フィールドは、
構造体で宣言した通りの順番に指定する必要はありません。換言すると、構造体定義とは、
型に対する一般的な雛形のようなものであり、インスタンスは、その雛形を特定のデータで埋め、その型の値を生成するわけです。
例えば、リスト5-2で示されたように特定のユーザを宣言することができます。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; }
リスト5-2: User
構造体のインスタンスを生成する
構造体から特定の値を得るには、ドット記法が使えます。このユーザのEメールアドレスだけが欲しいなら、
この値を使いたかった場所全部でuser1.email
が使えます。インスタンスが可変であれば、
ドット記法を使い特定のフィールドに代入することで値を変更できます。リスト5-3では、
可変なUser
インスタンスのemail
フィールド値を変更する方法を示しています。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } let mut user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; user1.email = String::from("anotheremail@example.com"); }
リスト5-3: あるUser
インスタンスのemail
フィールド値を変更する
インスタンス全体が可変でなければならないことに注意してください; Rustでは、一部のフィールドのみを可変にすることはできないのです。 また、あらゆる式同様、構造体の新規インスタンスを関数本体の最後の式として生成して、 そのインスタンスを返すことを暗示できます。
リスト5-4は、与えられたemailとusernameでUser
インスタンスを生成するbuild_user
関数を示しています。
active
フィールドにはtrue
値が入り、sign_in_count
には値1
が入ります。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } fn build_user(email: String, username: String) -> User { User { email: email, username: username, active: true, sign_in_count: 1, } } }
リスト5-4: Eメールとユーザ名を取り、User
インスタンスを返すbuild_user
関数
構造体のフィールドと同じ名前を関数の引数にもつけることは筋が通っていますが、
email
とusername
というフィールド名と変数を繰り返さなきゃいけないのは、ちょっと面倒です。
構造体にもっとフィールドがあれば、名前を繰り返すことはさらに煩わしくなるでしょう。
幸運なことに、便利な省略記法があります!
フィールドと変数が同名の時にフィールド初期化省略記法を使う
仮引数名と構造体のフィールド名がリスト5-4では、全く一緒なので、フィールド初期化省略記法を使ってbuild_user
を書き換えても、
振る舞いは全く同じにしつつ、リスト5-5に示したようにemail
とusername
を繰り返さなくてもよくなります。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } fn build_user(email: String, username: String) -> User { User { email, username, active: true, sign_in_count: 1, } } }
リスト5-5: email
とusername
引数が構造体のフィールドと同名なので、
フィールド初期化省略法を使用するbuild_user
関数
ここで、email
というフィールドを持つUser
構造体の新規インスタンスを生成しています。
email
フィールドをbuild_user
関数のemail
引数の値にセットしたいわけです。
email
フィールドとemail
引数は同じ名前なので、email: email
と書くのではなく、
email
と書くだけで済むのです。
構造体更新記法で他のインスタンスからインスタンスを生成する
多くは前のインスタンスの値を使用しつつ、変更する箇所もある形で新しいインスタンスを生成できるとしばしば有用です。 構造体更新記法でそうすることができます。
まず、リスト5-6では、更新記法なしでuser2
に新しいUser
インスタンスを生成する方法を示しています。
email
とusername
には新しい値をセットしていますが、それ以外にはリスト5-2で生成したuser1
の値を使用しています。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let user2 = User { email: String::from("another@example.com"), username: String::from("anotherusername567"), active: user1.active, sign_in_count: user1.sign_in_count, }; }
リスト5-6: user1
の一部の値を使用しつつ、新しいUser
インスタンスを生成する
構造体更新記法を使用すると、リスト5-7に示したように、コード量を減らしつつ、同じ効果を達成できます。..
という記法により、
明示的にセットされていない残りのフィールドが、与えられたインスタンスのフィールドと同じ値になるように指定します。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let user2 = User { email: String::from("another@example.com"), username: String::from("anotherusername567"), ..user1 }; }
リスト5-7: 構造体更新記法を使用して、新しいUser
インスタンス用の値に新しいemail
とusername
をセットしつつ、
残りの値は、user1
変数のフィールド値を使う
リスト5-7のコードも、email
とusername
については異なる値、active
とsign_in_count
フィールドについては、
user1
と同じ値になるインスタンスをuser2
に生成します。
異なる型を生成する名前付きフィールドのないタプル構造体を使用する
構造体名により追加の意味を含むものの、フィールドに紐づけられた名前がなく、むしろフィールドの型だけのタプル構造体と呼ばれる、 タプルに似た構造体を定義することもできます。タプル構造体は、構造体名が提供する追加の意味は含むものの、 フィールドに紐付けられた名前はありません; むしろ、フィールドの型だけが存在します。タプル構造体は、タプル全体に名前をつけ、 そのタプルを他のタプルとは異なる型にしたい場合に有用ですが、普通の構造体のように各フィールド名を与えるのは、 冗長、または余計になるでしょう。
タプル構造体を定義するには、struct
キーワードの後に構造体名、さらにタプルに含まれる型を続けます。
例えば、こちらは、Color
とPoint
という2種類のタプル構造体の定義と使用法です:
#![allow(unused)] fn main() { struct Color(i32, i32, i32); struct Point(i32, i32, i32); let black = Color(0, 0, 0); let origin = Point(0, 0, 0); }
black
とorigin
の値は、違う型であることに注目してください。これらは、異なるタプル構造体のインスタンスだからですね。
定義された各構造体は、構造体内のフィールドが同じ型であっても、それ自身が独自の型になります。
例えば、Color
型を引数に取る関数は、Point
を引数に取ることはできません。たとえ、両者の型が、
3つのi32
値からできていてもです。それ以外については、タプル構造体のインスタンスは、
タプルと同じように振る舞います: 分配して個々の部品にしたり、.
と添え字を使用して個々の値にアクセスするなどです。
フィールドのないユニット様構造体
また、一切フィールドのない構造体を定義することもできます!これらは、()
、ユニット型と似たような振る舞いをすることから、
ユニット様構造体と呼ばれます。ユニット様構造体は、ある型にトレイトを実装するけれども、
型自体に保持させるデータは一切ない場面に有効になります。トレイトについては第10章で議論します。
構造体データの所有権
リスト5-1の
User
構造体定義において、&str
文字列スライス型ではなく、所有権のあるString
型を使用しました。 これは意図的な選択です。というのも、この構造体のインスタンスには全データを所有してもらう必要があり、 このデータは、構造体全体が有効な間はずっと有効である必要があるのです。構造体に、他の何かに所有されたデータへの参照を保持させることもできますが、 そうするにはライフタイムという第10章で議論するRustの機能を使用しなければなりません。 ライフタイムのおかげで構造体に参照されたデータが、構造体自体が有効な間、ずっと有効であることを保証してくれるのです。 ライフタイムを指定せずに構造体に参照を保持させようとしたとしましょう。以下の通りですが、これは動きません:
ファイル名: src/main.rs
struct User { username: &str, email: &str, sign_in_count: u64, active: bool, } fn main() { let user1 = User { email: "someone@example.com", username: "someusername123", active: true, sign_in_count: 1, }; }
コンパイラは、ライフタイム指定子が必要だと怒るでしょう:
error[E0106]: missing lifetime specifier (エラー: ライフタイム指定子がありません) --> | 2 | username: &str, | ^ expected lifetime parameter (ライフタイム引数を予期しました) error[E0106]: missing lifetime specifier --> | 3 | email: &str, | ^ expected lifetime parameter
第10章で、これらのエラーを解消して構造体に参照を保持する方法について議論しますが、 当面、今回のようなエラーは、
&str
のような参照の代わりに、String
のような所有された型を使うことで修正します。
構造体を使ったプログラム例
構造体を使用したくなる可能性のあるケースを理解するために、長方形の面積を求めるプログラムを書きましょう。 単一の変数から始め、代わりに構造体を使うようにプログラムをリファクタリングします。
Cargoでrectanglesという新規バイナリプロジェクトを作成しましょう。このプロジェクトは、 長方形の幅と高さをピクセルで指定し、その面積を求めます。リスト5-8に、プロジェクトのsrc/main.rsで、 正にそうする一例を短いプログラムとして示しました。
ファイル名: src/main.rs
fn main() { let width1 = 30; let height1 = 50; println!( // 長方形の面積は、{}平方ピクセルです "The area of the rectangle is {} square pixels.", area(width1, height1) ); } fn area(width: u32, height: u32) -> u32 { width * height }
リスト5-8: 個別の幅と高さ変数を指定して長方形の面積を求める
では、cargo run
でこのプログラムを走らせてください:
The area of the rectangle is 1500 square pixels.
(長方形の面積は、1500平方ピクセルです)
タプルでリファクタリングする
リスト5-8のコードはうまく動き、各寸法を与えてarea
関数を呼び出すことで長方形の面積を割り出しますが、
改善点があります。幅と高さは、組み合わせると一つの長方形を表すので、相互に関係があるわけです。
このコードの問題点は、area
のシグニチャから明らかです:
fn area(width: u32, height: u32) -> u32 {
area
関数は、1長方形の面積を求めるものと考えられますが、今書いた関数には、引数が2つあります。
引数は関連性があるのに、このプログラム内のどこにもそのことは表現されていません。
幅と高さを一緒にグループ化する方が、より読みやすく、扱いやすくなるでしょう。
それをする一つの方法については、第3章の「タプル型」節ですでに議論しました: タプルを使うのです。
タプルでリファクタリングする
リスト5-9は、タプルを使う別バージョンのプログラムを示しています。
ファイル名: src/main.rs
fn main() { let rect1 = (30, 50); println!( "The area of the rectangle is {} square pixels.", area(rect1) ); } fn area(dimensions: (u32, u32)) -> u32 { dimensions.0 * dimensions.1 }
リスト5-9: タプルで長方形の幅と高さを指定する
ある意味では、このプログラムはマシです。タプルのおかげで少し構造的になり、一引数を渡すだけになりました。 しかし別の意味では、このバージョンは明確性を失っています: タプルは要素に名前を付けないので、 計算が不明瞭になったのです。なぜなら、タプルの一部に添え字アクセスする必要があるからです。
面積計算で幅と高さを混在させるのなら問題はないのですが、長方形を画面に描画したいとなると、問題になるのです!
タプルの添え字0
が幅
で、添え字1
が高さ
であることを肝に銘じておかなければなりません。
他人がこのコードをいじることになったら、このことを割り出し、同様に肝に銘じなければならないでしょう。
容易く、このことを忘れたり、これらの値を混ぜこぜにしたりしてエラーを発生させてしまうでしょう。
データの意味をコードに載せていないからです。
構造体でリファクタリングする: より意味付けする
データのラベル付けで意味を付与するために構造体を使います。現在使用しているタプルを全体と一部に名前のあるデータ型に、 変形することができます。そう、リスト5-10に示したように。
ファイル名: src/main.rs
struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; println!( "The area of the rectangle is {} square pixels.", area(&rect1) ); } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height }
リスト5-10: Rectangle
構造体を定義する
ここでは、構造体を定義し、Rectangle
という名前にしています。波括弧の中でwidth
とheight
というフィールドを定義し、
u32
という型にしました。それからmain
内でRectangle
の特定のインスタンスを生成し、
幅を30、高さを50にしました。
これでarea
関数は引数が一つになり、この引数は名前がrectangle
、型はRectangle
構造体インスタンスへの不変借用になりました。
第4章で触れたように、構造体の所有権を奪うよりも借用する必要があります。こうすることでmain
は所有権を保って、
rect1
を使用し続けることができ、そのために関数シグニチャと関数呼び出し時に&
を使っているわけです。
area
関数は、Rectangle
インスタンスのwidth
とheight
フィールドにアクセスしています。
これで、area
の関数シグニチャは、我々の意図をズバリ示すようになりました: width
とheight
フィールドを使って、
Rectangle
の面積を計算します。これにより、幅と高さが相互に関係していることが伝わり、
タプルの0
や1
という添え字を使うよりも、これらの値に説明的な名前を与えられるのです。プログラムの意図が明瞭になりました。
トレイトの導出で有用な機能を追加する
プログラムのデバッグをしている間に、Rectangle
のインスタンスを出力し、フィールドの値を確認できると、
素晴らしいわけです。リスト5-11では、以前の章のように、println!
マクロを試しに使用しようとしていますが、動きません。
ファイル名: src/main.rs
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
// rect1は{}です
println!("rect1 is {}", rect1);
}
リスト5-11: Rectangle
のインスタンスを出力しようとする
このコードを走らせると、こんな感じのエラーが出ます:
error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied
(エラー: トレイト境界`Rectangle: std::fmt::Display`が満たされていません)
println!
マクロには、様々な整形があり、標準では、波括弧はDisplay
として知られる整形をするよう、
println!
に指示するのです: 直接エンドユーザ向けの出力です。これまでに見てきた基本型は、
標準でDisplay
を実装しています。というのも、1
や他の基本型をユーザに見せる方法は一つしかないからです。
しかし構造体では、println!
が出力を整形する方法は自明ではなくなります。出力方法がいくつもあるからです:
カンマは必要なの?波かっこを出力する必要はある?全フィールドが見えるべき?この曖昧性のため、
Rustは必要なものを推測しようとせず、構造体にはDisplay
実装が提供されないのです。
エラーを読み下すと、こんな有益な注意書きがあります:
`Rectangle` cannot be formatted with the default formatter; try using
`:?` instead if you are using a format string
(注釈: `Rectangle`は、デフォルト整形機では、整形できません; フォーマット文字列を使うのなら
代わりに`:?`を試してみてください)
試してみましょう!pritnln!
マクロ呼び出しは、println!("rect1 is {:?}", rect1);
という見た目になるでしょう。
波括弧内に:?
という指定子を書くと、println!
にDebug
と呼ばれる出力整形を使いたいと指示するのです。
Debug
トレイトは、開発者にとって有用な方法で構造体を出力させてくれるので、
コードをデバッグしている最中に、値を確認することができます。
変更してコードを走らせてください。なに!まだエラーが出ます:
error[E0277]: the trait bound `Rectangle: std::fmt::Debug` is not satisfied
(エラー: トレイト境界`Rectangle: std::fmt::Debug`が満たされていません)
しかし今回も、コンパイラは有益な注意書きを残してくれています:
`Rectangle` cannot be formatted using `:?`; if it is defined in your
crate, add `#[derive(Debug)]` or manually implement it
(注釈: `Rectangle`は`:?`を使って整形できません; 自分のクレートで定義しているのなら
`#[derive(Debug)]`を追加するか、手動で実装してください)
確かにRustにはデバッグ用の情報を出力する機能が備わっていますが、この機能を構造体で使えるようにするには、
明示的な選択をしなければならないのです。そうするには、構造体定義の直前に#[derive(Debug)]
という注釈を追加します。
そう、リスト5-12で示されている通りです。
ファイル名: src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; println!("rect1 is {:?}", rect1); }
リスト5-12: Debug
トレイトを導出する注釈を追加し、
Rectangle
インスタンスをデバッグ用整形機で出力する
これでプログラムを実行すれば、エラーは出ず、以下のような出力が得られるでしょう:
rect1 is Rectangle { width: 30, height: 50 }
素晴らしい!最善の出力ではないものの、このインスタンスの全フィールドの値を出力しているので、
デバッグ中には間違いなく役に立つでしょう。より大きな構造体があるなら、もう少し読みやすい出力の方が有用です;
そのような場合には、println!
文字列中の{:?}
の代わりに{:#?}
を使うことができます。
この例で{:#?}
というスタイルを使用したら、出力は以下のようになるでしょう:
rect1 is Rectangle {
width: 30,
height: 50
}
Rustには、derive
注釈で使えるトレイトが多く提供されており、独自の型に有用な振る舞いを追加することができます。
そのようなトレイトとその振る舞いは、付録Cで一覧になっています。
これらのトレイトを独自の動作とともに実装する方法だけでなく、独自のトレイトを生成する方法については、第10章で解説します。
area
関数は、非常に特殊です: 長方形の面積を算出するだけです。Rectangle
構造体とこの動作をより緊密に結び付けられると、
役に立つでしょう。なぜなら、他のどんな型でもうまく動作しなくなるからです。
area
関数をRectangle
型に定義されたarea
メソッドに変形することで、
このコードをリファクタリングし続けられる方法について見ていきましょう。
メソッド記法
メソッドは関数に似ています: fn
キーワードと名前で宣言されるし、引数と返り値があるし、
どこか別の場所で呼び出された時に実行されるコードを含みます。ところが、
メソッドは構造体の文脈(あるいはenumかトレイトオブジェクトの。これらについては各々第6章と17章で解説します)で定義されるという点で、
関数とは異なり、最初の引数は必ずself
になり、これはメソッドが呼び出されている構造体インスタンスを表します。
メソッドを定義する
Rectangle
インスタンスを引数に取るarea
関数を変え、代わりにRectangle
構造体上にarea
メソッドを作りましょう。
リスト5-13に示した通りですね。
ファイル名: src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; println!( "The area of the rectangle is {} square pixels.", rect1.area() ); }
リスト5-13: Rectangle
構造体上にarea
メソッドを定義する
Rectangle
の文脈内で関数を定義するには、impl
(implementation; 実装)ブロックを始めます。
それからarea
関数をimpl
の波かっこ内に移動させ、最初の(今回は唯一の)引数をシグニチャ内と本体内全てでself
に変えます。
area
関数を呼び出し、rect1
を引数として渡すmain
では、代替としてメソッド記法を使用して、
Rectangle
インスタンスのarea
メソッドを呼び出せます。メソッド記法は、インスタンスの後に続きます:
ドット、メソッド名、かっこ、そして引数と続くわけです。
area
のシグニチャでは、rectangle: &Rectangle
の代わりに&self
を使用しています。
というのも、コンパイラは、このメソッドがimpl Rectangle
という文脈内に存在するために、
self
の型がRectangle
であると把握しているからです。&Rectangle
と同様に、
self
の直前に&
を使用していることに注意してください。メソッドは、self
の所有権を奪ったり、
ここでしているように不変でself
を借用したり、可変でself
を借用したりできるのです。
他の引数と全く同じですね。
ここで&self
を選んでいるのは、関数バージョンで&Rectangle
を使用していたのと同様の理由です:
所有権はいらず、構造体のデータを読み込みたいだけで、書き込む必要はないわけです。
メソッドの一部でメソッドを呼び出したインスタンスを変更したかったら、第1引数に&mut self
を使用するでしょう。
self
だけを第1引数にしてインスタンスの所有権を奪うメソッドを定義することは稀です; このテクニックは通常、
メソッドがself
を何か別のものに変形し、変形後に呼び出し元が元のインスタンスを使用できないようにしたい場合に使用されます。
関数の代替としてメソッドを使う主な利点は、メソッド記法を使用して全メソッドのシグニチャでself
の型を繰り返す必要がなくなる以外だと、
体系化です。コードの将来的な利用者にRectangle
の機能を提供しているライブラリ内の各所でその機能を探させるのではなく、
この型のインスタンスでできることを一つのimpl
ブロックにまとめあげています。
->
演算子はどこに行ったの?CとC++では、メソッド呼び出しには2種類の異なる演算子が使用されます: オブジェクトに対して直接メソッドを呼び出すのなら、
.
を使用するし、オブジェクトのポインタに対してメソッドを呼び出し、 先にポインタを参照外しする必要があるなら、->
を使用するわけです。 言い換えると、object
がポインタなら、object->something()
は、(*object).something()
と同等なのです。Rustには
->
演算子の代わりとなるようなものはありません; その代わり、Rustには、 自動参照および参照外しという機能があります。Rustにおいてメソッド呼び出しは、 この動作が行われる数少ない箇所なのです。動作方法はこうです:
object.something()
とメソッドを呼び出すと、 コンパイラはobject
がメソッドのシグニチャと合致するように、自動で&
か&mut
、*
を付与するのです。 要するに、以下のコードは同じものです:#![allow(unused)] fn main() { #[derive(Debug,Copy,Clone)] struct Point { x: f64, y: f64, } impl Point { fn distance(&self, other: &Point) -> f64 { let x_squared = f64::powi(other.x - self.x, 2); let y_squared = f64::powi(other.y - self.y, 2); f64::sqrt(x_squared + y_squared) } } let p1 = Point { x: 0.0, y: 0.0 }; let p2 = Point { x: 5.0, y: 6.5 }; p1.distance(&p2); (&p1).distance(&p2); }
前者の方がずっと明確です。メソッドには自明な受け手(
self
の型)がいるので、この自動参照機能は動作するのです。 受け手とメソッド名が与えられれば、コンパイラは確実にメソッドが読み込み専用(&self
)か、書き込みもする(&mut self
)のか、 所有権を奪う(self
)のか判断できるわけです。メソッドの受け手に関して借用が明示されないというのが、 所有権を実際に使うのがRustにおいて簡単である大きな理由です。
より引数の多いメソッド
Rectangle
構造体に2番目のメソッドを実装して、メソッドを使う鍛錬をしましょう。今回は、Rectangle
のインスタンスに、
別のRectangle
のインスタンスを取らせ、2番目のRectangle
がself
に完全にはめ込まれたら、true
を返すようにしたいのです;
そうでなければ、false
を返すべきです。つまり、一旦can_hold
メソッドを定義したら、
リスト5-14のようなプログラムを書けるようになりたいのです。
ファイル名: src/main.rs
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 10, height: 40 };
let rect3 = Rectangle { width: 60, height: 45 };
// rect1にrect2ははまり込む?
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
リスト5-14: まだ書いていないcan_hold
メソッドを使用する
そして、予期される出力は以下のようになります。なぜなら、rect2
の各寸法はrect1
よりも小さいものの、
rect3
はrect1
より幅が広いからです:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
メソッドを定義したいことはわかっているので、impl Rectangle
ブロック内での話になります。
メソッド名は、can_hold
になり、引数として別のRectangle
を不変借用で取るでしょう。
メソッドを呼び出すコードを見れば、引数の型が何になるかわかります: rect1.can_hold(&rect2)
は、
&rect2
、Rectangle
のインスタンスであるrect2
への不変借用を渡しています。
これは道理が通っています。なぜなら、rect2
を読み込む(書き込みではなく。この場合、可変借用が必要になります)だけでよく、
can_hold
メソッドを呼び出した後にもrect2
が使えるよう、所有権をmain
に残したままにしたいからです。
can_hold
の返り値は、booleanになり、メソッドの中身は、self
の幅と高さがもう一つのRectangle
の幅と高さよりも、
それぞれ大きいことを確認します。リスト5-13のimpl
ブロックに新しいcan_hold
メソッドを追記しましょう。
リスト5-15に示した通りです。
ファイル名: src/main.rs
#![allow(unused)] fn main() { #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } }
リスト5-15: 別のRectangle
のインスタンスを引数として取るcan_hold
メソッドを、
Rectangle
に実装する
このコードをリスト5-14のmain
関数と合わせて実行すると、望み通りの出力が得られます。
メソッドは、self
引数の後にシグニチャに追加した引数を複数取ることができ、
その引数は、関数の引数と同様に動作するのです。
関連関数
impl
ブロックの別の有益な機能は、impl
ブロック内にself
を引数に取らない関数を定義できることです。
これは、構造体に関連付けられているので、関連関数と呼ばれます。それでも、関連関数は関数であり、メソッドではありません。
というのも、対象となる構造体のインスタンスが存在しないからです。もうString::from
という関連関数を使用したことがありますね。
関連関数は、構造体の新規インスタンスを返すコンストラクタによく使用されます。例えば、一つの寸法を引数に取り、
長さと幅両方に使用する関連関数を提供することができ、その結果、同じ値を2回指定する必要なく、
正方形のRectangle
を生成しやすくすることができます。
ファイル名: src/main.rs
#![allow(unused)] fn main() { #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size } } } }
この関連関数を呼び出すために、構造体名と一緒に::
記法を使用します; 一例はlet sq = Rectangle::square(3);
です。
この関数は、構造体によって名前空間分けされています: ::
という記法は、関連関数とモジュールによって作り出される名前空間両方に使用されます。
モジュールについては第7章で議論します。
複数のimpl
ブロック
各構造体には、複数のimpl
ブロックを存在させることができます。例えば、リスト5-15はリスト5-16に示したコードと等価で、
リスト5-16では、各メソッドごとにimpl
ブロックを用意しています。
#![allow(unused)] fn main() { #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } }
リスト5-16: 複数のimpl
ブロックを使用してリスト5-15を書き直す
ここでこれらのメソッドを個々のimpl
ブロックに分ける理由はないのですが、合法な書き方です。
複数のimpl
ブロックが有用になるケースは第10章で見ますが、そこではジェネリック型と、トレイトについて議論します。
まとめ
構造体により、自分の領域で意味のある独自の型を作成することができます。構造体を使用することで、 関連のあるデータ片を相互に結合させたままにし、各部品に名前を付け、コードを明確にすることができます。 メソッドにより、構造体のインスタンスが行う動作を指定することができ、関連関数により、 構造体に特有の機能をインスタンスを利用することなく、名前空間分けすることができます。
しかし、構造体だけが独自の型を作成する手段ではありません: Rustのenum機能に目を向けて、 別の道具を道具箱に追加しましょう。
Enumとパターンマッチング
この章では、列挙型について見ていきます。列挙型は、enumとも称されます。enumは、取りうる値を列挙することで、
型を定義させてくれます。最初に、enumを定義し、使用して、enumがデータとともに意味をコード化する方法を示します。
次に、特別に有用なenumであるOption
について掘り下げていきましょう。この型は、
値が何かかなんでもないかを表現します。それから、match
式のパターンマッチングにより、
どうenumの色々な値に対して異なるコードを走らせやすくなるかを見ます。最後に、if let
文法要素も、
如何にenumをコードで扱う際に使用可能な便利で簡潔な慣用句であるかを解説します。
enumは多くの言語に存在する機能ですが、その能力は言語ごとに異なります。Rustのenumは、F#、OCaml、Haskellなどの、 関数型言語に存在する代数的データ型に最も酷似しています。
Enumを定義する
コードで表現したくなるかもしれない場面に目を向けて、enumが有用でこの場合、構造体よりも適切である理由を確認しましょう。 IPアドレスを扱う必要が出たとしましょう。現在、IPアドレスの規格は二つあります: バージョン4とバージョン6です。 これらは、プログラムが遭遇するIPアドレスのすべての可能性です: 列挙型は、取りうる値をすべて列挙でき、 これが列挙型の名前の由来です。
どんなIPアドレスも、バージョン4かバージョン6のどちらかになりますが、同時に両方にはなり得ません。 IPアドレスのその特性により、enumデータ構造が適切なものになります。というのも、 enumの値は、その列挙子のいずれか一つにしかなり得ないからです。バージョン4とバージョン6のアドレスは、 どちらも根源的にはIPアドレスですから、コードがいかなる種類のIPアドレスにも適用される場面を扱う際には、 同じ型として扱われるべきです。
この概念をコードでは、IpAddrKind
列挙型を定義し、IPアドレスがなりうる種類、V4
とV6
を列挙することで、
表現できます。これらは、enumの列挙子として知られています:
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } }
これで、IpAddrKind
はコードの他の場所で使用できる独自のデータ型になります。
Enumの値
以下のようにして、IpAddrKind
の各列挙子のインスタンスは生成できます:
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } let four = IpAddrKind::V4; let six = IpAddrKind::V6; }
enumの列挙子は、その識別子の元に名前空間分けされていることと、
2連コロンを使ってその二つを区別していることに注意してください。
これが有効な理由は、こうすることで、値IpAddrKind::V4
とIpAddrKind::V6
という値は両方とも、
同じ型IpAddrKind
になったからです。そうしたら、例えば、どんなIpAddrKind
を取る関数も定義できるようになります。
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } fn route(ip_type: IpAddrKind) { } }
そして、この関数をどちらの列挙子に対しても呼び出せます:
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } fn route(ip_type: IpAddrKind) { } route(IpAddrKind::V4); route(IpAddrKind::V6); }
enumの利用には、さらなる利点さえもあります。このIPアドレス型についてもっと考えてみると、現状では、 実際のIPアドレスのデータを保持する方法がありません。つまり、どんな種類であるかを知っているだけです。 構造体について第5章で学んだばっかりとすると、この問題に対して、あなたはリスト6-1のように対処するかもしれません。
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } struct IpAddr { kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1"), }; let loopback = IpAddr { kind: IpAddrKind::V6, address: String::from("::1"), }; }
リスト6-1: IPアドレスのデータとIpAddrKind
の列挙子をstruct
を使って保持する
ここでは、二つのフィールドを持つIpAddr
という構造体を定義しています: IpAddrKind
型(先ほど定義したenumですね)のkind
フィールドと、
String
型のaddress
フィールドです。この構造体のインスタンスが2つあります。最初のインスタンス、
home
にはkind
としてIpAddrKind::V4
があり、紐付けられたアドレスデータは127.0.0.1
です。
2番目のインスタンス、loopback
には、kind
の値として、IpAddrKind
のもう一つの列挙子、V6
があり、
アドレス::1
が紐付いています。構造体を使ってkind
とaddress
値を一緒に包んだので、
もう列挙子は値と紐付けられています。
各enumの列挙子に直接データを格納して、enumを構造体内に使うというよりもenumだけを使って、
同じ概念をもっと簡潔な方法で表現することができます。この新しいIpAddr
の定義は、
V4
とV6
列挙子両方にString
値が紐付けられていることを述べています。
#![allow(unused)] fn main() { enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V6(String::from("::1")); }
enumの各列挙子にデータを直接添付できるので、余計な構造体を作る必要は全くありません。
構造体よりもenumを使うことには、別の利点もあります: 各列挙子に紐付けるデータの型と量は、異なってもいいのです。
バージョン4のIPアドレスには、常に0から255の値を持つ4つの数値があります。V4
のアドレスは、4つのu8
型の値として格納するけれども、
V6
のアドレスは引き続き、単独のString
型の値で格納したかったとしても、構造体では不可能です。
enumなら、こんな場合も容易に対応できます:
#![allow(unused)] fn main() { enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1")); }
バージョン4とバージョン6のIPアドレスを格納するデータ構造を定義する複数の異なる方法を示してきました。
しかしながら、蓋を開けてみれば、IPアドレスを格納してその種類をコード化したくなるということは一般的なので、
標準ライブラリに使用可能な定義があります! 標準ライブラリでのIpAddr
の定義のされ方を見てみましょう:
私たちが定義し、使用したのと全く同じenumと列挙子がありますが、アドレスデータを二種の異なる構造体の形で列挙子に埋め込み、
この構造体は各列挙子用に異なる形で定義されています。
#![allow(unused)] fn main() { struct Ipv4Addr { // 省略 } struct Ipv6Addr { // 省略 } enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr), } }
このコードは、enum列挙子内にいかなる種類のデータでも格納できることを描き出しています: 例を挙げれば、文字列、数値型、構造体などです。他のenumを含むことさえできます!また、 標準ライブラリの型は、あなたの想像するよりも複雑ではないことがしばしばあります。
標準ライブラリにIpAddr
に対する定義は含まれるものの、標準ライブラリの定義をまだ我々のスコープに導入していないので、
干渉することなく自分自身の定義を生成して使用できることに注意してください。型をスコープに導入することについては、
第7章でもっと詳しく言及します。
リスト6-2でenumの別の例を見てみましょう: 今回のコードは、幅広い種類の型が列挙子に埋め込まれています。
#![allow(unused)] fn main() { enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } }
リスト6-2: 列挙子各々が異なる型と量の値を格納するMessage
enum
このenumには、異なる型の列挙子が4つあります:
Quit
には紐付けられたデータは全くなし。Move
は、中に匿名構造体を含む。Write
は、単独のString
オブジェクトを含む。ChangeColor
は、3つのi32
値を含む。
リスト6-2のような列挙子を含むenumを定義することは、enumの場合、struct
キーワードを使わず、
全部の列挙子がMessage
型の元に分類される点を除いて、異なる種類の構造体定義を定義するのと類似しています。
以下の構造体も、先ほどのenumの列挙子が保持しているのと同じデータを格納することができるでしょう:
#![allow(unused)] fn main() { struct QuitMessage; // ユニット構造体 struct MoveMessage { x: i32, y: i32, } struct WriteMessage(String); // タプル構造体 struct ChangeColorMessage(i32, i32, i32); // タプル構造体 }
ですが、異なる構造体を使っていたら、各々、それ自身の型があるので、単独の型になるリスト6-2で定義したMessage
enumほど、
これらの種のメッセージいずれもとる関数を簡単に定義することはできないでしょう。
enumと構造体にはもう1点似通っているところがあります: impl
を使って構造体にメソッドを定義できるのと全く同様に、
enumにもメソッドを定義することができるのです。こちらは、Message
enum上に定義できるcall
という名前のメソッドです:
#![allow(unused)] fn main() { enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } impl Message { fn call(&self) { // method body would be defined here // メソッド本体はここに定義される } } let m = Message::Write(String::from("hello")); m.call(); }
メソッドの本体では、self
を使用して、メソッドを呼び出した相手の値を取得できるでしょう。この例では、
Message::Write(String::from("hello"))
という値を持つ、変数m
を生成したので、これがm.call()
を走らせた時に、
call
メソッドの本体内でself
が表す値になります。
非常に一般的で有用な別の標準ライブラリのenumを見てみましょう: Option
です。
Option
enumとNull値に勝る利点
前節で、IpAddr
enumがRustの型システムを使用して、プログラムにデータ以上の情報をコード化できる方法を目撃しました。
この節では、Option
のケーススタディを掘り下げていきます。この型も標準ライブラリにより定義されているenumです。
このOption
型はいろんな箇所で使用されます。なぜなら、値が何かかそうでないかという非常に一般的な筋書きをコード化するからです。
この概念を型システムの観点で表現することは、コンパイラが、プログラマが処理すべき場面全てを処理していることをチェックできることを意味します;
この機能は、他の言語において、究極的にありふれたバグを阻止することができます。
プログラミング言語のデザインは、しばしばどの機能を入れるかという観点で考えられるが、 除いた機能も重要なのです。Rustには、他の多くの言語にはあるnull機能がありません。 nullとはそこに何も値がないことを意味する値です。nullのある言語において、 変数は常に二者択一どちらかの状態になります: nullかそうでないかです。
nullの開発者であるトニー・ホーア(Tony Hoare)の2009年のプレゼンテーション、 "Null References: The Billion Dollar Mistake"(Null参照: 10億ドルの間違い)では、こんなことが語られています。
私はそれを10億ドルの失敗と呼んでいます。その頃、私は、オブジェクト指向言語の参照に対する、 最初のわかりやすい型システムを設計していました。私の目標は、 どんな参照の使用も全て完全に安全であるべきことを、コンパイラにそのチェックを自動で行ってもらって保証することだったのです。 しかし、null参照を入れるという誘惑に打ち勝つことができませんでした。それは、単純に実装が非常に容易だったからです。 これが無数のエラーや脆弱性、システムクラッシュにつながり、過去40年で10億ドルの苦痛や損害を引き起こしたであろうということなのです。
null値の問題は、nullの値をnullでない値のように使用しようとしたら、何らかの種類のエラーが出ることです。 このnullかそうでないかという特性は広く存在するので、この種の間違いを大変犯しやすいのです。
しかしながら、nullが表現しようとしている概念は、それでも役に立つものです: nullは、 何らかの理由で現在無効、または存在しない値のことなのです。
問題は、全く概念にあるのではなく、特定の実装にあるのです。そんな感じなので、Rustにはnullがありませんが、
値が存在するか不在かという概念をコード化するenumならあります。このenumがOption<T>
で、
以下のように標準ライブラリに定義されています。
#![allow(unused)] fn main() { enum Option<T> { Some(T), None, } }
Option<T>
は有益すぎて、初期化処理(prelude)にさえ含まれています。つまり、明示的にスコープに導入する必要がないのです。
さらに、列挙子もそうなっています: Some
とNone
をOption::
の接頭辞なしに直接使えるわけです。
ただ、Option<T>
はそうは言っても、普通のenumであり、Some(T)
とNone
もOption<T>
型のただの列挙子です。
<T>
という記法は、まだ語っていないRustの機能です。これは、ジェネリック型引数であり、ジェネリクスについて詳しくは、
第10章で解説します。とりあえず、知っておく必要があることは、<T>
は、Option
enumのSome
列挙子が、
あらゆる型のデータを1つだけ持つことができることを意味していることだけです。こちらは、
Option
値を使って、数値型や文字列型を保持する例です。
#![allow(unused)] fn main() { let some_number = Some(5); let some_string = Some("a string"); let absent_number: Option<i32> = None; }
Some
ではなく、None
を使ったら、コンパイラにOption<T>
の型が何になるかを教えなければいけません。
というのも、None
値を見ただけでは、Some
列挙子が保持する型をコンパイラが推論できないからです。
Some
値がある時、値が存在するとわかり、その値は、Some
に保持されています。None
値がある場合、
ある意味、nullと同じことを意図します: 有効な値がないのです。では、なぜOption<T>
の方が、
nullよりも少しでも好ましいのでしょうか?
簡潔に述べると、Option<T>
とT
(ここでT
はどんな型でもいい)は異なる型なので、
コンパイラがOption<T>
値を確実に有効な値かのようには使用させてくれません。
例えば、このコードはi8
をOption<i8>
に足そうとしているので、コンパイルできません。
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
このコードを動かしたら、以下のようなエラーメッセージが出ます。
error[E0277]: the trait bound `i8: std::ops::Add<std::option::Option<i8>>` is
not satisfied
(エラー: `i8: std::ops::Add<std::option::Option<i8>>`というトレイト境界が満たされていません)
-->
|
5 | let sum = x + y;
| ^ no implementation for `i8 + std::option::Option<i8>`
|
なんて強烈な!実際に、このエラーメッセージは、i8
とOption<i8>
が異なる型なので、
足し合わせる方法がコンパイラにはわからないことを意味します。Rustにおいて、i8
のような型の値がある場合、
コンパイラが常に有効な値であることを確認してくれます。この値を使う前にnullであることをチェックする必要なく、
自信を持って先に進むことができるのです。Option<i8>
がある時(あるいはどんな型を扱おうとしていても)のみ、
値を保持していない可能性を心配する必要があるわけであり、
コンパイラはプログラマが値を使用する前にそのような場面を扱っているか確かめてくれます。
言い換えると、T
型の処理を行う前には、Option<T>
をT
に変換する必要があるわけです。一般的に、
これにより、nullの最もありふれた問題の一つを捕捉する一助になります: 実際にはnullなのに、
そうでないかのように想定することです。
不正確にnullでない値を想定する心配をしなくてもよいということは、コード内でより自信を持てることになります。
nullになる可能性のある値を保持するには、その値の型をOption<T>
にすることで明示的に同意しなければなりません。
それからその値を使用する際には、値がnullである場合を明示的に処理する必要があります。
値がOption<T>
以外の型であるとこ全てにおいて、値がnullでないと安全に想定することができます。
これは、Rustにとって、意図的な設計上の決定であり、nullの普遍性を制限し、Rustコードの安全性を向上させます。
では、Option<T>
型の値がある時、その値を使えるようにするには、どのようにSome
列挙子からT
型の値を取り出せばいいのでしょうか?
Option<T>
には様々な場面で有効に活用できる非常に多くのメソッドが用意されています;
ドキュメントでそれらを確認できます。Option<T>
のメソッドに馴染むと、
Rustの旅が極めて有益になるでしょう。
一般的に、Option<T>
値を使うには、各列挙子を処理するコードが欲しくなります。
Some(T)
値がある時だけ走る何らかのコードが欲しくなり、このコードが内部のT
を使用できます。
None
値があった場合に走る別のコードが欲しくなり、そちらのコードはT
値は使用できない状態になります。
match
式が、enumとともに使用した時にこれだけの動作をする制御フロー文法要素になります:
enumの列挙子によって、違うコードが走り、そのコードがマッチした値の中のデータを使用できるのです。
match
制御フロー演算子
Rustには、一連のパターンに対して値を比較し、マッチしたパターンに応じてコードを実行させてくれるmatch
と呼ばれる、
非常に強力な制御フロー演算子があります。パターンは、リテラル値、変数名、ワイルドカードやその他多数のもので構成することができます;
第18章で、全ての種類のパターンと、その目的については解説します。match
のパワーは、
パターンの表現力とコンパイラが全てのありうるパターンを処理しているかを確認してくれるという事実に由来します。
match
式をコイン並べ替え装置のようなものと考えてください: コインは、様々なサイズの穴が空いた通路を流れ落ち、
各コインは、サイズのあった最初の穴に落ちます。同様に、値はmatch
の各パターンを通り抜け、値が「適合する」最初のパターンで、
値は紐付けられたコードブロックに落ち、実行中に使用されるわけです。
コインについて話したので、それをmatch
を使用する例にとってみましょう!数え上げ装置と同じ要領で未知のアメリカコインを一枚取り、
どの種類のコインなのか決定し、その価値をセントで返す関数をリスト6-3で示したように記述することができます。
#![allow(unused)] fn main() { enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } }
リスト6-3: enumとそのenumの列挙子をパターンにしたmatch
式
value_in_cents
関数内のmatch
を噛み砕きましょう。まず、match
キーワードに続けて式を並べています。
この式は今回の場合、値coin
です。if
で使用した式と非常に酷似しているみたいですね。しかし、大きな違いがあります:
if
では、式は論理値を返す必要がありますが、ここでは、どんな型でも構いません。この例におけるcoin
の型は、
1行目で定義したCoin
enumです。
次は、match
アームです。一本のアームには2つの部品があります: パターンと何らかのコードです。
今回の最初のアームはCoin::Penny
という値のパターンであり、パターンと動作するコードを区別する=>
演算子が続きます。
この場合のコードは、ただの値1
です。各アームは次のアームとカンマで区切られています。
このmatch
式が実行されると、結果の値を各アームのパターンと順番に比較します。パターンに値がマッチしたら、
そのコードに紐付けられたコードが実行されます。パターンが値にマッチしなければ、コイン並べ替え装置と全く同じように、
次のアームが継続して実行されます。必要なだけパターンは存在できます: リスト6-3では、match
には4本のアームがあります。
各アームに紐付けられるコードは式であり、マッチしたアームの式の結果がmatch
式全体の戻り値になります。
典型的に、アームのコードが短い場合、波かっこは使用されません。リスト6-3では、各アームが値を返すだけなので、
これに倣っています。マッチのアームで複数行のコードを走らせたいのなら、波かっこを使用することができます。
例えば、以下のコードは、メソッドがCoin::Penny
とともに呼び出されるたびに「Lucky penny!」と表示しつつ、
ブロックの最後の値、1
を返すでしょう。
#![allow(unused)] fn main() { enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 }, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } }
値に束縛されるパターン
マッチのアームの別の有益な機能は、パターンにマッチした値の一部に束縛できる点です。こうして、 enumの列挙子から値を取り出すことができます。
例として、enumの列挙子の一つを中にデータを保持するように変えましょう。1999年から2008年まで、
アメリカは、片側に50の州それぞれで異なるデザインをしたクォーターコインを鋳造していました。
他のコインは州のデザインがなされることはなかったので、クォーターだけがこのおまけの値を保持します。
Quarter
列挙子を変更して、UsState
値が中に保持されるようにすることでenum
にこの情報を追加でき、
それをしたのがリスト6-4のコードになります。
#![allow(unused)] fn main() { #[derive(Debug)] // すぐに州を点検できるように enum UsState { Alabama, Alaska, // ... などなど } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } }
リスト6-4: Quarter
列挙子がUsState
の値も保持するCoin
enum
友人の一人が50州全部のクォーターコインを収集しようとしているところを想像しましょう。コインの種類で小銭を並べ替えつつ、 友人が持っていない種類だったら、コレクションに追加できるように、各クォーターに関連した州の名前を出力します。
このコードのmatch式では、Coin::Quarter
列挙子の値にマッチするstate
という名の変数をパターンに追加します。
Coin::Quarter
がマッチすると、state
変数はそのクォーターのstateの値に束縛されます。それから、
state
をそのアームのコードで使用できます。以下のようにですね:
#![allow(unused)] fn main() { #[derive(Debug)] enum UsState { Alabama, Alaska, } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {:?}!", state); 25 }, } } }
value_in_cents(Coin::Quarter(UsState::Alaska))
と呼び出すつもりだったなら、coin
は
Coin::Quarter(UsState::Alaska)
になります。その値をmatchの各アームと比較すると、
Coin::Quarter(state)
に到達するまで、どれにもマッチしません。その時に、state
に束縛されるのは、
UsState::Alaska
という値です。そして、println!
式でその束縛を使用することができ、
そのため、Coin
enumの列挙子からQuarter
に対する中身のstateの値を取得できたわけです。
Option<T>
とのマッチ
前節では、Option<T>
を使用する際に、Some
ケースから中身のT
の値を取得したくなりました。要するに、
Coin
enumに対して行ったように、match
を使ってOption<T>
を扱うこともできるというわけです!
コインを比較する代わりに、Option<T>
の列挙子を比較するのですが、match
式の動作の仕方は同じままです。
Option<i32>
を取る関数を書きたくなったとし、中に値があったら、その値に1を足すことにしましょう。
中に値がなければ、関数はNone
値を返し、何も処理を試みるべきではありません。
match
のおかげで、この関数は大変書きやすく、リスト6-5のような見た目になります。
#![allow(unused)] fn main() { fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), } } let five = Some(5); let six = plus_one(five); let none = plus_one(None); }
リスト6-5: Option<i32>
にmatch
式を使う関数
plus_one
の最初の実行についてもっと詳しく検証しましょう。plus_one(five)
と呼び出した時、
plus_one
の本体の変数x
はSome(5)
になります。そして、これをマッチの各アームと比較します。
None => None,
Some(5)
という値は、None
というパターンにはマッチしませんので、次のアームに処理が移ります。
Some(i) => Some(i + 1),
Some(5)
はSome(i)
にマッチしますか?なんと、します!列挙子が同じです。i
はSome
に含まれる値に束縛されるので、
i
は値5
になります。それから、このマッチのアームのコードが実行されるので、i
の値に1を足し、
合計の6
を中身にした新しいSome
値を生成します。
さて、x
がNone
になるリスト6-5の2回目のplus_one
の呼び出しを考えましょう。match
に入り、
最初のアームと比較します。
None => None,
マッチします!足し算する値がないので、プログラムは停止し、=>
の右辺にあるNone
値が返ります。
最初のアームがマッチしたため、他のアームは比較されません。
match
とenumの組み合わせは、多くの場面で有効です。Rustコードにおいて、このパターンはよく見かけるでしょう:
enumに対しmatch
し、内部のデータに変数を束縛させ、それに基づいたコードを実行します。最初はちょっと巧妙ですが、
一旦慣れてしまえば、全ての言語にあってほしいと願うことになるでしょう。一貫してユーザのお気に入りなのです。
マッチは包括的
もう一つ議論する必要のあるmatch
の観点があります。一点バグがありコンパイルできないこんなバージョンのplus_one
関数を考えてください:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
None
の場合を扱っていないため、このコードはバグを生みます。幸い、コンパイラが捕捉できるバグです。
このコードのコンパイルを試みると、こんなエラーが出ます:
error[E0004]: non-exhaustive patterns: `None` not covered
(エラー: 包括的でないパターン: `None`がカバーされてません)
-->
|
6 | match x {
| ^ pattern `None` not covered
全可能性を網羅していないことをコンパイラは検知しています。もっと言えば、どのパターンを忘れているかさえ知っているのです。
Rustにおけるマッチは、包括的です: 全てのあらゆる可能性を網羅し尽くさなければ、コードは有効にならないのです。
特にOption<T>
の場合には、私達が明示的にNone
の場合を処理するのを忘れないようにしてくれます。
nullになるかもしれないのに値があると思い込まないよう、すなわち前に議論した10億ドルの失敗を犯さないよう、
コンパイラが保護してくれるわけです。
_
というプレースホルダー
Rustには、全ての可能性を列挙したくない時に使用できるパターンもあります。例えば、u8
は、有効な値として、
0から255までを取ります。1、3、5、7の値にだけ興味があったら、0、2、4、6、8、9と255までの数値を列挙する必要に迫られたくはないです。
幸運なことに、する必要はありません: 代わりに特別なパターンの_
を使用できます:
#![allow(unused)] fn main() { let some_u8_value = 0u8; match some_u8_value { 1 => println!("one"), 3 => println!("three"), 5 => println!("five"), 7 => println!("seven"), _ => (), } }
_
というパターンは、どんな値にもマッチします。他のアームの後に記述することで、_
は、
それまでに指定されていない全ての可能性にマッチします。()
は、ただのユニット値なので、_
の場合には、
何も起こりません。結果として、_
プレースホルダーの前に列挙していない可能性全てに対しては、
何もしたくないと言えるわけです。
ですが、一つのケースにしか興味がないような場面では、match
式はちょっと長ったらしすぎます。
このような場面用に、Rustには、if let
が用意されています。
if let
で簡潔な制御フロー
if let
記法でif
とlet
をより冗長性の少ない方法で組み合わせ、残りを無視しつつ、一つのパターンにマッチする値を扱うことができます。
Option<u8>
にマッチするけれど、値が3の時にだけコードを実行したい、リスト6-6のプログラムを考えてください。
#![allow(unused)] fn main() { let some_u8_value = Some(0u8); match some_u8_value { Some(3) => println!("three"), _ => (), } }
リスト6-6: 値がSome(3)
の時だけコードを実行するmatch
Some(3)
にマッチした時だけ何かをし、他のSome<u8>
値やNone
値の時には何もしたくありません。
match
式を満たすためには、列挙子を一つだけ処理した後に_ => ()
を追加しなければなりません。
これでは、追加すべき定型コードが多すぎます。
その代わり、if let
を使用してもっと短く書くことができます。以下のコードは、
リスト6-6のmatch
と同じように振る舞います:
#![allow(unused)] fn main() { let some_u8_value = Some(0u8); if let Some(3) = some_u8_value { println!("three"); } }
if let
という記法は等号記号で区切られたパターンと式を取り、式がmatch
に与えられ、パターンが最初のアームになったmatch
と、
同じ動作をします。
if let
を使うと、タイプ数が減り、インデントも少なくなり、定型コードも減ります。しかしながら、
match
では強制された包括性チェックを失ってしまいます。match
かif let
かの選択は、
特定の場面でどんなことをしたいかと簡潔性を得ることが包括性チェックを失うのに適切な代償となるかによります。
言い換えると、if let
は値が一つのパターンにマッチした時にコードを走らせ、
他は無視するmatch
への糖衣構文と考えることができます。
if let
では、else
を含むこともできます。else
に入るコードブロックは、
if let
とelse
に等価なmatch
式の_
の場合に入るコードブロックと同じになります。
リスト6-4のCoin
enum定義を思い出してください。ここでは、Quarter
列挙子は、
UsState
の値も保持していましたね。クォーターコインの状態を告げつつ、
見かけたクォーター以外のコインの枚数を数えたいなら、以下のようにmatch
式で実現することができるでしょう:
#![allow(unused)] fn main() { #[derive(Debug)] enum UsState { Alabama, Alaska, } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } let coin = Coin::Penny; let mut count = 0; match coin { // {:?}州のクォーターコイン Coin::Quarter(state) => println!("State quarter from {:?}!", state), _ => count += 1, } }
または、以下のようにif let
とelse
を使うこともできるでしょう:
#![allow(unused)] fn main() { #[derive(Debug)] enum UsState { Alabama, Alaska, } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } let coin = Coin::Penny; let mut count = 0; if let Coin::Quarter(state) = coin { println!("State quarter from {:?}!", state); } else { count += 1; } }
match
を使って表現するには冗長的すぎるロジックがプログラムにあるようなシチュエーションに遭遇したら、
if let
もRust道具箱にあることを思い出してください。
まとめ
これで、enumを使用してワンセットの列挙された値のどれかになりうる独自の型を生成する方法を講義しました。
標準ライブラリのOption<T>
が型システムを使用して、エラーを回避する際に役立つ方法についても示しました。
enumの値がデータを内部に含む場合、処理すべきケースの数に応じて、match
かif let
を使用して値を取り出し、
使用できます。
もうRustプログラムで構造体とenumを使用して、自分の領域の概念を表現できます。API内で使用するために独自の型を生成することで、 型安全性を保証することができます: コンパイラが、各関数の予期する型の値のみを関数が得ることを確かめてくれるのです。
使用するのに率直な整理整頓されたAPIをユーザに提供し、ユーザが必要とするものだけを公開するために、 今度は、Rustのモジュールに目を向けてみましょう。
肥大化していくプロジェクトをパッケージ、クレート、モジュールを利用して管理する
大きなプログラムを書く時、そのすべてを頭の中に入れておくのは不可能になるため、コードのまとまりを良くすることが重要になります。 関係した機能をまとめ、異なる特徴を持つコードを分割することにより、特定の機能を実装しているコードを見つけたり、機能を変更したりするためにどこを探せば良いのかを明確にできます。
私達がこれまでに書いてきたプログラムは、一つのファイル内の一つのモジュール内にありました。 プロジェクトが大きくなるにつれて、これを複数のモジュールに、ついで複数のファイルに分割することで、プログラムを整理することができます。 パッケージは複数のバイナリクレートからなり、またライブラリクレートを1つもつこともできます。 パッケージが大きくなるにつれて、その一部を抜き出して分離したクレートにし、外部依存とするのもよいでしょう。 この章ではそれらのテクニックすべてを学びます。 相互に関係し合い、同時に成長するパッケージの集まりからなる巨大なプロジェクトには、 Cargoがワークスペースという機能を提供します。これは14章のCargoワークスペースで解説します。
機能をグループにまとめられることに加え、実装の詳細がカプセル化されることにより、コードをより高いレベルで再利用できるようになります: 手続きを実装し終えてしまえば、他のコードはそのコードの公開されたインターフェースを通じて、実装の詳細を知ることなくそのコードを呼び出すことができるのです。 コードをどう書くかによって、どの部分が他のコードにも使える公開のものになるのか、それとも自分だけが変更できる非公開のものになるのかが決定されます。 これもまた、記憶しておくべき細部を制限してくれる方法のひとつです。
関係する概念にスコープがあります: コードが記述されているネストされた文脈には、「スコープ内」として定義される名前の集合があります。 コードを読んだり書いたりコンパイルしたりする時には、プログラマーやコンパイラは特定の場所にある特定の名前が、変数・関数・構造体・enum・モジュール・定数・その他のどの要素を表すのか、そしてその要素は何を意味するのかを知る必要があります。 そこでスコープを作り、どの名前がスコープ内/スコープ外にあるのかを変更することができます。 同じ名前のものを2つ同じスコープ内に持つことはできません。そこで、名前の衝突を解決するための方法があります。
Rustには、どの詳細を公開するか、どの詳細を非公開にするか、どの名前がプログラムのそれぞれのスコープにあるか、といったコードのまとまりを保つためのたくさんの機能があります。 これらの機能は、まとめて「モジュールシステム」と呼ばれることがあり、以下のようなものが含まれます。
- パッケージ: クレートをビルドし、テストし、共有することができるCargoの機能
- クレート: ライブラリか実行可能ファイルを生成する、木構造をしたモジュール群
- モジュール と use: これを使うことで、パスの構成、スコープ、公開するか否かを決定できます
- パス: 要素(例えば構造体や関数やモジュール)に名前をつける方法
この章では、これらの機能をすべて学び、これらがどう相互作用するかについて議論し、これらをどう使ってスコープを制御するのかについて説明します。 この章を読み終わる頃には、モジュールシステムをしっかりと理解し、熟練者のごとくスコープを扱うことができるようになっているでしょう!
パッケージとクレート
最初に学ぶモジュールシステムの要素は、パッケージとクレートです。 クレートはバイナリかライブラリのどちらかです。 クレートルート (crate root) とは、Rustコンパイラの開始点となり、クレートのルートモジュールを作るソースファイルのことです(モジュールについて詳しくは「モジュールを定義して、スコープとプライバシーを制御する」のセクションで説明します)。 パッケージ はある機能群を提供する1つ以上のクレートです。 パッケージは Cargo.toml という、それらのクレートをどのようにビルドするかを説明するファイルを持っています。
パッケージが何を持ってよいかはいくつかのルールで決まっています。 パッケージは0個か1個のライブラリクレートを持っていないといけません。それ以上は駄目です。 バイナリクレートはいくらでも持って良いですが、少なくとも(ライブラリでもバイナリでも良いですが)1つのクレートを持っていないといけません。
パッケージを作る時に何が起こるか見てみましょう。
まず、cargo new
というコマンドを入力します:
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
このコマンドを入力したとき、Cargoは Cargo.toml ファイルを作り、パッケージを作ってくれました。
Cargo.toml の中身を見ても、src/main.rs については何も書いてありません。これは、Cargoは src/main.rs が、パッケージと同じ名前を持つバイナリクレートのクレートルートであるという慣習に従っているためです。
同じように、Cargoはパッケージディレクトリに src/lib.rs が含まれていたら、パッケージにはパッケージと同じ名前のライブラリクレートが含まれており、src/lib.rs がそのクレートルートなのだと判断します。
Cargoはクレートルートファイルを rustc
に渡し、ライブラリやバイナリをビルドします。
今、このパッケージには src/main.rs しか含まれておらず、つまりこのパッケージはmy-project
という名前のバイナリクレートのみを持っているということです。
もしパッケージが src/main.rs と src/lib.rs を持っていたら、クレートは2つになります:どちらもパッケージと同じ名前を持つ、ライブラリクレートとバイナリクレートです。
ファイルを src/bin ディレクトリに置くことで、パッケージは複数のバイナリクレートを持つことができます。それぞれのファイルが別々のバイナリクレートになります。
クレートは、関連した機能を一つのスコープにまとめることで、その機能が複数のプロジェクト間で共有しやすいようにします。
例えば、2章で使ったrand
クレートは、乱数を生成する機能を提供します。
rand
クレートを私達のプロジェクトのスコープに持ち込むことで、この機能を私達のプロジェクトで使うことができます。
rand
クレートが提供する機能にはすべて、クレートの名前rand
を使ってアクセスできます。
クレートの機能をそれ自身のスコープの中に入れたままにしておくことは、ある機能が私達のクレートで定義されたのかrand
クレートで定義されたのかを明確にし、名前の衝突を予防してくれます。
例えば、rand
クレートはRng
という名前のトレイトを提供しています。
更に、私達のクレートでRng
という名前のstruct
を定義することもできます。
クレートの機能はそのスコープ内の名前空間に位置づけられているので、rand
を依存先として追加しても、コンパイラはRng
という名前が何を意味するのかについて混乱することはないのです。
私達のクレートでは、私達の定義したstruct Rng
のことであり、rand
クレートのRng
トレイトにはrand::Rng
でアクセスするというわけです。
では、モジュールシステムの話に移りましょう!
モジュールを定義して、スコープとプライバシーを制御する
この節では、モジュールと、その他のモジュールシステムの要素
――すなわち、要素に名前をつけるための パス 、パスをスコープに持ち込むuse
キーワード、要素を公開するpub
キーワード――
について学びます。
また、as
キーワード、外部パッケージ、glob演算子についても話します。
とりあえず、今はモジュールに集中しましょう!
モジュール はクレート内のコードをグループ化し、可読性と再利用性を上げるのに役に立ちます。 モジュールは要素の プライバシー も制御できます。プライバシーとは、要素がコードの外側で使える (公開 public) のか、内部の実装の詳細であり外部では使えない (非公開 private) のかです。
例えば、レストランの機能を提供するライブラリクレートを書いてみましょう。 実際にレストランを実装することではなく、コードの関係性に注目したいので、関数にシグネチャをつけますが中身は空白のままにします。
レストラン業界では、レストランの一部を 接客部門 (front of house) といい、その他を 後方部門 (back of house) といいます。 接客部門とはお客さんがいるところです。接客係がお客様を席に案内し、給仕係が注文と支払いを受け付け、バーテンダーが飲み物を作ります。 後方部門とはシェフや料理人がキッチンで働き、皿洗い係が食器を片付け、マネージャが管理業務をする場所です。
私達のクレートを現実のレストランと同じような構造にするために、関数をネストしたモジュールにまとめましょう。
restaurant
という名前の新しいライブラリをcargo new --lib restaurant
と実行することで作成し、Listing 7-1 のコードを src/lib.rs に書き込み、モジュールと関数のシグネチャを定義してください。
ファイル名: src/lib.rs
mod front_of_house { mod hosting { fn add_to_waitlist() {} fn seat_at_table() {} } mod serving { fn take_order() {} fn serve_order() {} fn take_payment() {} } } fn main() {}
Listing 7-1: front_of_house
モジュールにその他のモジュールが含まれ、さらにそれらが関数を含んでいる
モジュールは、mod
キーワードを書き、次にモジュールの名前(今回の場合、front_of_house
)を指定することで定義されます。
モジュールの中には、今回だとhosting
とserving
のように、他のモジュールをおくこともできます。
モジュールにはその他の要素の定義も置くことができます。例えば、構造体、enum、定数、トレイト、そして(Listing 7-1のように)関数です。
モジュールを使うことで、関連する定義を一つにまとめ、関連する理由を名前で示せます。 このコードを使うプログラマーは、定義を全部読むことなく、グループ単位でコードを読み進められるので、欲しい定義を見つけ出すのが簡単になるでしょう。 このコードに新しい機能を付け加えるプログラマーは、プログラムのまとまりを保つために、どこにその機能のコードを置けば良いのかがわかるでしょう。
以前、 src/main.rs と src/lib.rs はクレートルートと呼ばれていると言いました。
この名前のわけは、 モジュールツリー と呼ばれるクレートのモジュール構造の根っこ (ルート)にこれら2つのファイルの中身がcrate
というモジュールを形成するからです。
Listing 7-2は、Listing 7-1の構造のモジュールツリーを示しています。
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
Listing 7-2: Listing 7-1 のコードのモジュールツリー
このツリーを見ると、どのモジュールがどのモジュールの中にネストしているのかがわかります(例えば、hosting
はfront_of_house
の中にネストしています)。
また、いくつかのモジュールはお互いに 兄弟 の関係にある、つまり、同じモジュール内で定義されていることもわかります(例えばhosting
とserving
はfront_of_house
で定義されています)。
他にも、家族関係の比喩を使って、モジュールAがモジュールBの中に入っている時、AはBの 子 であるといい、BはAの 親 であるといいます。
モジュールツリー全体が、暗黙のうちに作られたcrate
というモジュールの下にあることにも注目してください。
モジュールツリーを見ていると、コンピュータのファイルシステムのディレクトリツリーを思い出すかもしれません。その喩えはとても適切です! ファイルシステムのディレクトリのように、モジュールはコードをまとめるのに使われるのです。 そしてディレクトリからファイルを見つけるように、目的のモジュールを見つけ出す方法が必要になります。
モジュールツリーの要素を示すためのパス
ファイルシステムの中を移動する時と同じように、Rustにモジュールツリー内の要素を見つけるためにはどこを探せばいいのか教えるためにパスを使います。 関数を呼び出したいなら、そのパスを知っていなければなりません。
パスは2つの形を取ることができます:
- 絶対パス は、クレートの名前か
crate
という文字列を使うことで、クレートルートからスタートします。 - 相対パス は、
self
、super
または今のモジュール内の識別子を使うことで、現在のモジュールからスタートします。
絶対パスも相対パスも、その後に一つ以上の識別子がダブルコロン(::
)で仕切られて続きます。
Listing 7-1の例に戻ってみましょう。
add_to_waitlist
関数をどうやって呼べばいいでしょうか?
すなわち、add_to_waitlist
のパスは何でしょうか?
Listing 7-3 は、モジュールと関数をいくつか取り除いてコードをやや簡潔にしています。
これを使って、クレートルートに定義された新しいeat_at_restaurant
という関数から、add_to_waitlist
関数を呼びだす2つの方法を示しましょう。
eat_at_restaurant
関数はこのライブラリクレートの公開 (public) APIの1つなので、pub
キーワードをつけておきます。
pub
については、パスをpub
キーワードで公開するの節でより詳しく学びます。
この例はまだコンパイルできないことに注意してください。理由はすぐに説明します。
ファイル名: src/lib.rs
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
// 絶対パス
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
// 相対パス
front_of_house::hosting::add_to_waitlist();
}
Listing 7-3: add_to_waitlist
関数を絶対パスと相対パスで呼び出す
eat_at_restaurant
で最初にadd_to_waitlist
関数を呼び出す時、絶対パスを使っています。
add_to_waitlist
関数はeat_at_restaurant
と同じクレートで定義されているので、crate
キーワードで絶対パスを始めることができます。
crate
の後は、add_to_waitlist
にたどり着くまで、後に続くモジュールを書き込んでいます。
同じ構造のファイルシステムを想像すれば、/front_of_house/hosting/add_to_waitlist
とパスを指定してadd_to_waitlist
を実行していることに相当します。
crate
という名前を使ってクレートルートからスタートするというのは、/
を使ってファイルシステムのルートからスタートするようなものです。
eat_at_restaurant
で2回目にadd_to_waitlist
関数を呼び出す時、相対パスを使っています。
パスは、モジュールツリーにおいてeat_at_restaurant
と同じ階層で定義されているモジュールであるfront_of_house
からスタートします。
これはファイルシステムでfront_of_house/hosting/add_to_waitlist
というパスを使っているのに相当します。
名前から始めるのは、パスが相対パスであることを意味します。
相対パスを使うか絶対パスを使うかは、プロジェクトによって決めましょう。
要素を定義するコードを、その要素を使うコードと別々に動かすか一緒に動かすか、どちらが起こりそうかによって決めるのが良いです。
例えば、front_of_house
モジュールとeat_at_restaurant
関数をcustomer_experience
というモジュールに移動させると、add_to_waitlist
への絶対パスを更新しないといけませんが、相対パスは有効なままです。
しかし、eat_at_restaurant
関数だけをdining
というモジュールに移動させると、add_to_waitlist
への絶対パスは同じままですが、相対パスは更新しないといけないでしょう。
コードの定義と、その要素の呼び出しは独立に動かしそうなので、絶対パスのほうが好ましいです。
では、Listing 7-3 をコンパイルしてみて、どうしてこれはまだコンパイルできないのか考えてみましょう! エラーをListing 7-4 に示しています。
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^
error[E0603]: module `hosting` is private
--> src/lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant`.
To learn more, run the command again with --verbose.
Listing 7-4: Listing 7-3のコードをビルドしたときのコンパイルエラー
エラーメッセージは、hosting
は非公開 (private) だ、と言っています。
言い換えるなら、hosting
モジュールとadd_to_waitlist
関数へのパスは正しいが、非公開な部分へのアクセスは許可されていないので、Rustがそれを使わせてくれないということです。
モジュールはコードの整理に役立つだけではありません。 モジュールはRustの プライバシー境界 も定義します。これは、外部のコードが知ったり、呼び出したり、依存したりしてはいけない実装の詳細をカプセル化する線引きです。 なので、関数や構造体といった要素を非公開にしたければ、モジュールに入れればよいのです。
Rustにおけるプライバシーは、「あらゆる要素(関数、メソッド、構造体、enum、モジュールおよび定数)は標準では非公開」というやり方で動いています。 親モジュールの要素は子モジュールの非公開要素を使えませんが、子モジュールの要素はその祖先モジュールの要素を使えます。 これは、子モジュールは実装の詳細を覆い隠しますが、子モジュールは自分の定義された文脈を見ることができるためです。 レストランの喩えを続けるなら、レストランの後方部門になったつもりでプライバシーのルールを考えてみてください。レストランの顧客にはそこで何が起こっているのかは非公開ですが、そこで働くオフィスマネージャには、レストランのことは何でも見えるし何でもできるのです。
Rustは、内部実装の詳細を隠すことが標準であるようにモジュールシステムを機能させることを選択しました。
こうすることで、内部のコードのどの部分が、外部のコードを壊すことなく変更できるのかを知ることができます。
しかし、pub
キーワードを使って要素を公開することで、子モジュールの内部部品を外部の祖先モジュールに見せることができます。
パスをpub
キーワードで公開する
Listing 7-4の、hosting
モジュールが非公開だと言ってきていたエラーに戻りましょう。
親モジュールのeat_at_restaurant
関数が子モジュールのadd_to_waitlist
関数にアクセスできるようにしたいので、hosting
モジュールにpub
キーワードをつけます。Listing 7-5のようになります。
ファイル名: src/lib.rs
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
// 絶対パス
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
// 相対パス
front_of_house::hosting::add_to_waitlist();
}
Listing 7-5: hosting
モジュールを pub
として宣言することでeat_at_restaurant
から使う
残念ながら、Listing 7-5 のコードもListing 7-6 に示されるようにエラーとなります。
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:9:37
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:12:30
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant`.
To learn more, run the command again with --verbose.
Listing 7-6: Listing 7-5 のコードをビルドしたときのコンパイルエラー
何が起きたのでしょう?pub
キーワードをmod hosting
の前に追加したことで、このモジュールは公開されました。
この変更によって、front_of_house
にアクセスできるなら、hosting
にもアクセスできるようになりました。
しかしhosting
の 中身 はまだ非公開です。モジュールを公開してもその中身は公開されないのです。
モジュールにpub
キーワードがついていても、祖先モジュールのコードはモジュールを参照できるようになるだけです。
Listing 7-6 のエラーはadd_to_waitlist
関数が非公開だと言っています。
プライバシーのルールは、モジュール同様、構造体、enum、関数、メソッドにも適用されるのです。
add_to_waitlist
の定義の前にpub
キーワードを追加して、これも公開しましょう。
ファイル名: src/lib.rs
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } pub fn eat_at_restaurant() { // Absolute path // 絶対パス crate::front_of_house::hosting::add_to_waitlist(); // Relative path // 相対パス front_of_house::hosting::add_to_waitlist(); } fn main() {}
Listing 7-7: pub
キーワードをmod hosting
とfn add_to_waitlist
に追加することで、eat_at_restaurant
からこの関数を呼べるようになる
これでこのコードはコンパイルできます!
絶対パスと相対パスをもう一度確認して、どうしてpub
キーワードを追加することでadd_to_waitlist
のそれらのパスを使えるようになるのか、プライバシールールの観点からもう一度確認してみてみましょう。
絶対パスは、クレートのモジュールツリーのルートであるcrate
から始まります。
クレートルートの中にfront_of_house
が定義されています。
front_of_house
は公開されていませんが、eat_at_restaurant
関数はfront_of_house
と同じモジュール内で定義されている(つまり、eat_at_restaurant
とfront_of_house
は兄弟な)ので、eat_at_restaurant
からfront_of_house
を参照することができます。
次はpub
の付いたhosting
モジュールです。
hosting
の親モジュールにアクセスできるので、hosting
にもアクセスできます。
最後に、add_to_waitlist
関数はpub
が付いており、私達はその親モジュールにアクセスできるので、この関数呼び出しはうまく行くというわけです。
相対パスについても、最初のステップを除けば同じ理屈です。パスをクレートルートから始めるのではなくて、front_of_house
から始めるのです。
front_of_house
モジュールはeat_at_restaurant
と同じモジュールで定義されているので、eat_at_restaurant
が定義されている場所からの相対パスが使えます。
そして、hosting
とadd_to_waitlist
はpub
が付いていますから、残りのパスについても問題はなく、この関数呼び出しは有効というわけです。
相対パスをsuper
で始める
親モジュールから始まる相対パスなら、super
を最初につけることで構成できます。
ファイルシステムパスを..
構文で始めるのに似ています。
どのようなときのこの機能が使いたくなるのでしょう?
シェフが間違った注文を修正し、自分でお客さんに持っていくという状況をモデル化している、Listing 7-8 を考えてみてください。
fix_incorrect_order
関数はserve_order
関数を呼び出すために、super
から始まるserve_order
関数へのパスを使っています。
ファイル名: src/lib.rs
fn serve_order() {} mod back_of_house { fn fix_incorrect_order() { cook_order(); super::serve_order(); } fn cook_order() {} } fn main() {}
Listing 7-8: super
で始まる相対パスを使って関数を呼び出す
fix_incorrect_order
関数はback_of_house
モジュールの中にあるので、super
を使ってback_of_house
の親モジュールにいけます。親モジュールは、今回の場合ルートであるcrate
です。
そこから、serve_order
を探し、見つけ出します。
成功!
もしクレートのモジュールツリーを再編成することにした場合でも、back_of_house
モジュールとserve_order
関数は同じ関係性で有り続け、一緒に動くように思われます。
そのため、super
を使うことで、将来このコードが別のモジュールに移動するとしても、更新する場所が少なくて済むようにしました。
構造体とenumを公開する
構造体やenumもpub
を使って公開するよう指定できますが、追加の細目がいくつかあります。
構造体定義の前にpub
を使うと、構造体は公開されますが、構造体のフィールドは非公開のままなのです。
それぞれのフィールドを公開するか否かを個々に決められます。
Listing 7-9 では、公開のtoast
フィールドと、非公開のseasonal_fruit
フィールドをもつ公開のback_of_house::Breakfast
構造体を定義しました。
これは、例えば、レストランで、お客さんが食事についてくるパンの種類は選べるけれど、食事についてくるフルーツは季節と在庫に合わせてシェフが決める、という状況をモデル化しています。
提供できるフルーツはすぐに変わるので、お客さんはフルーツを選ぶどころかどんなフルーツが提供されるのか知ることもできません。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { mod back_of_house { pub struct Breakfast { pub toast: String, seasonal_fruit: String, } impl Breakfast { pub fn summer(toast: &str) -> Breakfast { Breakfast { toast: String::from(toast), seasonal_fruit: String::from("peaches"), } } } } pub fn eat_at_restaurant() { // Order a breakfast in the summer with Rye toast // 夏 (summer) にライ麦 (Rye) パン付き朝食を注文 let mut meal = back_of_house::Breakfast::summer("Rye"); // Change our mind about what bread we'd like // やっぱり別のパンにする meal.toast = String::from("Wheat"); println!("I'd like {} toast please", meal.toast); // The next line won't compile if we uncomment it; we're not allowed // to see or modify the seasonal fruit that comes with the meal // 下の行のコメントを外すとコンパイルできない。食事についてくる // 季節のフルーツを知ることも修正することも許されていないので // meal.seasonal_fruit = String::from("blueberries"); } }
Listing 7-9: 公開のフィールドと非公開のフィールドとを持つ構造体
back_of_house::Breakfast
のtoast
フィールドは公開されているので、eat_at_restaurant
においてtoast
をドット記法を使って読み書きできます。
seasonal_fruit
は非公開なので、eat_at_restaurant
においてseasonal_fruit
は使えないということに注意してください。
seasonal_fruit
を修正している行のコメントを外して、どのようなエラーが得られるか試してみてください!
また、back_of_house::Breakfast
は非公開のフィールドを持っているので、Breakfast
のインスタンスを作成 (construct) する公開された関連関数が構造体によって提供されている必要があります(ここではsummer
と名付けました)。
もしBreakfast
にそのような関数がなかったら、eat_at_restaurant
において非公開であるseasonal_fruit
の値を設定できないので、Breakfast
のインスタンスを作成できません。
一方で、enumを公開すると、そのヴァリアントはすべて公開されます。
Listing 7-10 に示されているように、pub
はenum
キーワードの前にだけおけばよいのです。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { mod back_of_house { pub enum Appetizer { Soup, Salad, } } pub fn eat_at_restaurant() { let order1 = back_of_house::Appetizer::Soup; let order2 = back_of_house::Appetizer::Salad; } }
Listing 7-10: enumを公開に指定することはそのヴァリアントをすべて公開にする
Appetizer
というenumを公開したので、Soup
とSalad
というヴァリアントもeat_at_restaurant
で使えます。
enumはヴァリアントが公開されてないとあまり便利ではないのですが、毎回enumのすべてのヴァリアントにpub
をつけるのは面倒なので、enumのヴァリアントは標準で公開されるようになっているのです。
構造体はフィールドが公開されていなくても便利なことが多いので、構造体のフィールドは、pub
がついてない限り標準で非公開という通常のルールに従うわけです。
まだ勉強していない、pub
の関わるシチュエーションがもう一つあります。モジュールシステムの最後の機能、use
キーワードです。
use
自体の勉強をした後、pub
とuse
を組み合わせる方法についてお見せします。
use
キーワードでパスをスコープに持ち込む
これまで関数呼び出しのために書いてきたパスは、長く、繰り返しも多くて不便なものでした。
例えば、Listing 7-7 においては、絶対パスを使うか相対パスを使うかにかかわらず、add_to_waitlist
関数を呼ぼうと思うたびにfront_of_house
とhosting
も指定しないといけませんでした。
ありがたいことに、この手続きを簡単化する方法があります。
use
キーワードを使うことで、パスを一度スコープに持ち込んでしまえば、それ以降はパス内の要素がローカルにあるかのように呼び出すことができるのです。
Listing 7-11 では、crate::front_of_house::hosting
モジュールをeat_at_restaurant
関数のスコープに持ち込むことで、eat_at_restaurant
において、hosting::add_to_waitlist
と指定するだけでadd_to_waitlist
関数を呼び出せるようにしています。
ファイル名: src/lib.rs
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); } fn main() {}
Listing 7-11: use
でモジュールをスコープに持ち込む
use
とパスをスコープに追加することは、ファイルシステムにおいてシンボリックリンクを張ることに似ています。
use crate::front_of_house::hosting
をクレートルートに追加することで、hosting
はこのスコープで有効な名前となり、まるでhosting
はクレートルートで定義されていたかのようになります。
スコープにuse
で持ち込まれたパスも、他のパスと同じようにプライバシーがチェックされます。
use
と相対パスで要素をスコープに持ち込むこともできます。
Listing 7-12 はListing 7-11 と同じふるまいを得るためにどう相対パスを書けば良いかを示しています。
ファイル名: src/lib.rs
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use self::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); } fn main() {}
Listing 7-12: モジュールをuse
と相対パスを使ってスコープに持ち込む
慣例に従ったuse
パスを作る
Listing 7-11 を見て、なぜuse crate::front_of_house::hosting
と書いてeat_at_restaurant
内でhosting::add_to_waitlist
と呼び出したのか不思議に思っているかもしれません。Listing 7-13 のように、use
でadd_to_waitlist
までのパスをすべて指定しても同じ結果が得られるのに、と。
ファイル名: src/lib.rs
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use crate::front_of_house::hosting::add_to_waitlist; pub fn eat_at_restaurant() { add_to_waitlist(); add_to_waitlist(); add_to_waitlist(); } fn main() {}
Listing 7-13: add_to_waitlist
関数をuse
でスコープに持ち込む。このやりかたは慣例的ではない
Listing 7-11 も 7-13 もおなじ仕事をしてくれますが、関数をスコープにuse
で持ち込む場合、Listing 7-11 のほうが慣例的なやり方です。
関数の親モジュールをuse
で持ち込むことで、関数を呼び出す際、毎回親モジュールを指定しなければならないようにすれば、フルパスを繰り返して書くことを抑えつつ、関数がローカルで定義されていないことを明らかにできます。
Listing 7-13 のコードではどこでadd_to_waitlist
が定義されたのかが不明瞭です。
一方で、構造体やenumその他の要素をuse
で持ち込むときは、フルパスを書くのが慣例的です。
Listing 7-14 は標準ライブラリのHashMap
構造体をバイナリクレートのスコープに持ち込む慣例的なやり方を示しています。
ファイル名: src/main.rs
use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }
Listing 7-14: HashMap
を慣例的なやり方でスコープに持ち込む
こちらの慣例の背後には、はっきりとした理由はありません。自然に発生した慣習であり、みんなRustのコードをこのやり方で読み書きするのに慣れてしまったというだけです。
同じ名前の2つの要素をuse
でスコープに持ち込むのはRustでは許されないので、そのときこの慣例は例外的に不可能です。
Listing 7-15は、同じ名前を持つけれど異なる親モジュールを持つ2つのResult
型をスコープに持ち込み、それらを参照するやり方を示しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::fmt; use std::io; fn function1() -> fmt::Result { // --snip-- // (略) Ok(()) } fn function2() -> io::Result<()> { // --snip-- // (略) Ok(()) } }
Listing 7-15: 同じ名前を持つ2つの型を同じスコープに持ち込むには親モジュールを使わないといけない。
このように、親モジュールを使うことで2つのResult
型を区別できます。
もしuse std::fmt::Result
と use std::io::Result
と書いていたとしたら、2つのResult
型が同じスコープに存在することになり、私達がResult
を使ったときにどちらのことを意味しているのかRustはわからなくなってしまいます。
新しい名前をas
キーワードで与える
同じ名前の2つの型をuse
を使って同じスコープに持ち込むという問題には、もう一つ解決策があります。パスの後に、as
と型の新しいローカル名、即ちエイリアスを指定すればよいのです。
Listing 7-16 は、Listing 7-15 のコードを、2つのResult
型のうち一つをas
を使ってリネームするという別のやり方で書いたものを表しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::fmt::Result; use std::io::Result as IoResult; fn function1() -> Result { // --snip-- Ok(()) } fn function2() -> IoResult<()> { // --snip-- Ok(()) } }
Listing 7-16: 型がスコープに持ち込まれた時、as
キーワードを使ってその名前を変えている
2つめのuse
文では、std::io::Result
に、IoResult
という新たな名前を選んでやります。std::fmt
のResult
もスコープに持ち込んでいますが、この名前はこれとは衝突しません。
Listing 7-15もListing 7-16も慣例的とみなされているので、どちらを使っても構いませんよ!
pub use
を使って名前を再公開する
use
キーワードで名前をスコープに持ちこんだ時、新しいスコープで使用できるその名前は非公開です。
私達のコードを呼び出すコードが、まるでその名前が私達のコードのスコープで定義されていたかのように参照できるようにするためには、pub
とuse
を組み合わせればいいです。
このテクニックは、要素を自分たちのスコープに持ち込むだけでなく、他の人がその要素をその人のスコープに持ち込むことも可能にすることから、再公開 (re-exporting) と呼ばれています。
Listing 7-17 は Listing 7-11 のコードのルートモジュールでのuse
をpub use
に変更したものを示しています。
ファイル名: src/lib.rs
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } pub use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); } fn main() {}
Listing 7-17: pub use
で、新たなスコープのコードがその名前を使えるようにする
pub use
を使うことで、外部のコードがhosting::add_to_waitlist
を使ってadd_to_waitlist
関数を呼び出せるようになりました。
pub use
を使っていなければ、eat_at_restaurant
関数はhosting::add_to_waitlist
を自らのスコープ内で使えるものの、外部のコードはこの新しいパスを利用することはできないでしょう。
再公開は、あなたのコードの内部構造と、あなたのコードを呼び出すプログラマーたちのその領域に関しての見方が異なるときに有用です。
例えば、レストランの比喩では、レストランを経営している人は「接客部門 (front of house)」と「後方部門 (back of house)」のことについて考えるでしょう。
しかし、レストランを訪れるお客さんは、そのような観点からレストランの部門について考えることはありません。
pub use
を使うことで、ある構造でコードを書きつつも、別の構造で公開するということが可能になります。
こうすることで、私達のライブラリを、ライブラリを開発するプログラマにとっても、ライブラリを呼び出すプログラマにとっても、よく整理されたものとすることができます。
外部のパッケージを使う
2章で、乱数を得るためにrand
という外部パッケージを使って、数当てゲームをプログラムしました。
rand
を私達のプロジェクトで使うために、次の行を Cargo.toml に書き加えましたね:
ファイル名: Cargo.toml
rand = "0.8.3"
rand
を依存 (dependency) として Cargo.toml に追加すると、rand
パッケージとそのすべての依存をcrates.ioからダウンロードして、私達のプロジェクトでrand
が使えるようにするようCargoに命令します。
そして、rand
の定義を私達のパッケージのスコープに持ち込むために、クレートの名前であるrand
から始まるuse
の行を追加し、そこにスコープに持ち込みたい要素を並べました。
2章の乱数を生成するの節で、Rng
トレイトをスコープに持ち込みrand::thread_rng
関数を呼び出したことを思い出してください。
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
println!("The secret number is: {}", secret_number); //秘密の数字は次の通り: {}
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
Rustコミュニティに所属する人々がcrates.ioでたくさんのパッケージを利用できるようにしてくれており、上と同じステップを踏めばそれらをあなたのパッケージに取り込むことができます:あなたのパッケージの Cargo.toml ファイルにそれらを書き並べ、use
を使って要素をクレートからスコープへと持ち込めばよいのです。
標準ライブラリ (std
) も、私達のパッケージの外部にあるクレートだということに注意してください。
標準ライブラリはRust言語に同梱されているので、 Cargo.toml を std
を含むように変更する必要はありません。
しかし、その要素をそこから私達のパッケージのスコープに持ち込むためには、use
を使って参照する必要はあります。
例えば、HashMap
には次の行を使います。
#![allow(unused)] fn main() { use std::collections::HashMap; }
これは標準ライブラリクレートの名前std
から始まる絶対パスです。
巨大なuse
のリストをネストしたパスを使って整理する
同じクレートか同じモジュールで定義された複数の要素を使おうとする時、それぞれの要素を一行一行並べると、縦に大量のスペースを取ってしまいます。
例えば、Listing 2-4の数当てゲームで使った次の2つのuse
文がstd
からスコープへ要素を持ち込みました。
ファイル名: src/main.rs
use rand::Rng;
// --snip--
// (略)
use std::cmp::Ordering;
use std::io;
// --snip--
// (略)
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("The secret number is: {}", secret_number);
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
代わりに、ネストしたパスを使うことで、同じ一連の要素を1行でスコープに持ち込めます。 これをするには、Listing 7-18 に示されるように、パスの共通部分を書き、2つのコロンを続け、そこで波括弧で互いに異なる部分のパスのリストを囲みます。
ファイル名: src/main.rs
use rand::Rng;
// --snip--
// (略)
use std::{cmp::Ordering, io};
// --snip--
// (略)
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("The secret number is: {}", secret_number);
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
Listing 7-18: 同じプレフィックスをもつ複数の要素をスコープに持ち込むためにネストしたパスを指定する
大きなプログラムにおいては、同じクレートやモジュールからのたくさんの要素をネストしたパスで持ち込むようにすれば、独立したuse
文の数を大きく減らすことができます!
ネストしたパスはパスのどの階層においても使うことができます。これはサブパスを共有する2つのuse
文を合体させるときに有用です。
例えば、Listing 7-19 は2つのuse
文を示しています:1つはstd::io
をスコープに持ち込み、もう一つはstd::io::Write
をスコープに持ち込んでいます。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::io; use std::io::Write; }
Listing 7-19: 片方がもう片方のサブパスである2つのuse
文
これらの2つのパスの共通部分はstd::io
であり、そしてこれは最初のパスにほかなりません。これらの2つのパスを1つのuse
文へと合体させるには、Listing 7-20 に示されるように、ネストしたパスにself
を使いましょう。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::io::{self, Write}; }
Listing 7-20: Listing 7-19 のパスを一つの use
文に合体させる
この行は std::io
とstd::io::Write
をスコープに持ち込みます。
glob演算子
パスにおいて定義されているすべての公開要素をスコープに持ち込みたいときは、glob演算子 *
をそのパスの後ろに続けて書きましょう:
#![allow(unused)] fn main() { use std::collections::*; }
このuse
文はstd::collections
のすべての公開要素を現在のスコープに持ち込みます。
glob演算子を使う際にはご注意を!
globをすると、どの名前がスコープ内にあり、プログラムで使われている名前がどこで定義されたのか分かりづらくなります。
glob演算子はしばしば、テストの際、テストされるあらゆるものをtests
モジュールに持ち込むために使われます。これについては11章テストの書き方の節で話します。
glob演算子はプレリュードパターンの一部としても使われることがあります:そのようなパターンについて、より詳しくは標準ライブラリのドキュメントをご覧ください。
モジュールを複数のファイルに分割する
この章のすべての例において、今までのところ、複数のモジュールを一つのファイルに定義していました。 モジュールが大きくなる時、コードを読み進めやすくするため、それらの定義を別のファイルへ移動させたくなるかもしれません。
例えば、Listing 7-17 のコードからはじめましょう。クレートルートのファイルをListing 7-21 のコードを持つように変更して、front_of_house
モジュールをそれ専用のファイルsrc/front_of_house.rs
に動かしましょう。
今回、クレートルートファイルはsrc/lib.rs
ですが、この手続きはクレートルートファイルがsrc/main.rs
であるバイナリクレートでもうまく行きます。
ファイル名: src/lib.rs
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
Listing 7-21: front_of_house
モジュールを宣言する。その中身はsrc/front_of_house.rs
内にある
そして、 Listing 7-22 のように、src/front_of_house.rs にはfront_of_house
モジュールの中身の定義を与えます。
ファイル名: src/front_of_house.rs
pub mod hosting {
pub fn add_to_waitlist() {}
}
Listing 7-22: src/front_of_house.rsにおける、front_of_house
モジュール内部の定義
mod front_of_house
の後にブロックではなくセミコロンを使うと、Rustにモジュールの中身をモジュールと同じ名前をした別のファイルから読み込むように命令します。
私達の例で、つづけてhosting
モジュールをそれ専用のファイルに抽出するには、src/front_of_house.rs
がhosting
モジュールの宣言のみを含むように変更します:
ファイル名: src/front_of_house.rs
pub mod hosting;
さらにsrc/front_of_house ディレクトリとsrc/front_of_house/hosting.rs ファイルを作って、hosting
モジュール内でなされていた定義を持つようにします。
ファイル名: src/front_of_house/hosting.rs
#![allow(unused)] fn main() { pub fn add_to_waitlist() {} }
定義は別のファイルにあるにもかかわらず、モジュールツリーは同じままであり、eat_at_restaurant
内での関数呼び出しもなんの変更もなくうまく行きます。
このテクニックのおかげで、モジュールが大きくなってきた段階で新しいファイルへ動かす、ということができます。
src/lib.rs におけるpub use crate::front_of_house::hosting
という文も変わっていないし、use
はどのファイルがクレートの一部としてコンパイルされるかになんの影響も与えないということに注意してください。
mod
キーワードがモジュールを宣言したなら、Rustはそのモジュールに挿入するためのコードを求めて、モジュールと同じ名前のファイルの中を探すというわけです。
まとめ
Rustでは、パッケージを複数のクレートに、そしてクレートを複数のモジュールに分割して、あるモジュールで定義された要素を他のモジュールから参照することができます。
これは絶対パスか相対パスを指定することで行なえます。
これらのパスはuse
文でスコープに持ち込むことができ、こうすると、そのスコープで要素を複数回使う時に、より短いパスで済むようになります。
モジュールのコードは標準では非公開ですが、pub
キーワードを追加することで定義を公開することができます。
次の章では、きちんと整理されたあなたのコードで使うことができる、標準ライブラリのいくつかのコレクションデータ構造を見ていきます。
一般的なコレクション
Rustの標準ライブラリは、コレクションと呼ばれる多くの非常に有益なデータ構造を含んでいます。他の多くのデータ型は、 ある一つの値を表しますが、コレクションは複数の値を含むことができます。組み込みの配列とタプル型とは異なり、 これらのコレクションが指すデータはヒープに確保され、データ量はコンパイル時にわかる必要はなく、 プログラムの実行にあわせて、伸縮可能であることになります。各種のコレクションには異なる能力とコストが存在し、 自分の現在の状況に最適なものを選び取るスキルは、時間とともに育っていきます。この章では、 Rustのプログラムにおいて、非常に頻繁に使用される3つのコレクションについて議論しましょう。
- ベクタ型は、可変長の値を並べて保持できる。
- 文字列は、文字のコレクションである。以前、
String
型について触れたが、 この章ではより掘り下げていく。 - ハッシュマップは、値を特定のキーと紐付けさせてくれる。より一般的なデータ構造である、 マップの特定の実装である。
標準ライブラリで提供されている他の種のコレクションについて学ぶには、 ドキュメントを参照されたし。
ベクタ型、文字列、ハッシュマップの生成と更新方法や、各々が特別な点について議論していきましょう。
ベクタで値のリストを保持する
最初に見るコレクション型はVec<T>
であり、これはベクタとしても知られています。
ベクタは単体のデータ構造でありながら複数の値を保持でき、それらの値をメモリ上に隣り合わせに並べます。
ベクタには同じ型の値しか保持できません。
要素のリストがある場合にベクタは有用です。
例えば、テキストファイルの各行とか、ショッピングカートのアイテムの価格などです。
新しいベクタを生成する
空のベクタを新たに作るには、リスト8-1に示すようにVec::new
関数を呼びます。
fn main() { let v: Vec<i32> = Vec::new(); }
リスト8-1:新しい空のベクタを生成してi32
型の値を保持する
ここで、型注釈を付けていることに注目してください。
なぜなら、このベクタに対して何も値を挿入していないので、コンパイラには私たちがどんなデータを保持させるつもりか推測できないからです。
これは重要な点です。
ベクタはジェネリクスを使用して実装されています。
あなた自身の型でどうジェネリクスを使用するかついては第10章で解説します。
現時点では標準ライブラリで提供されるVec<T>
型は、どんな型でも保持でき、ある特定のベクタがある型を保持するとき、その型は山かっこ内に指定されることを知っておいてください。
リスト8-1では、コンパイラにv
のVec<T>
はi32
型の要素を保持すると指示しました。
いったん値を挿入すると、多くの場合、コンパイラは保持させたい値の型を推論できるようになります。
ですから、より現実的なコードでは、型注釈を付ける必要はあまりないでしょう。
また、初期値を持つVec<T>
を生成する方が一般的ですし、Rustにはvec!
という便利なマクロも用意されています。
このマクロは与えた値を保持する新しいベクタを生成します。
リスト8-2では、1
、2
、3
という値を持つ新しいVec<i32>
を生成しています。
整数型をi32
にしているのは、3章の「データ型」節で学んだように、これが標準の整数型だからです。
fn main() { let v = vec![1, 2, 3]; }
リスト8-2: 値を含む新しいベクタを生成する
初期値のi32
値を与えたので、コンパイラはv
の型がVec<i32>
であると推論でき、型注釈は不要になりました。
次はベクタを変更する方法を見ましょう。
ベクタを更新する
ベクタを生成し、それから要素を追加するには、リスト8-3に示すようにpush
メソッドを使います。
fn main() { let mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8); }
リスト8-3:push
メソッドを使用してベクタに値を追加する
第3章で説明したとおり、どんな変数でも、その値を変更したかったらmut
キーワードで可変にする必要があります。
中に配置する数値は全てi32
型であり、Rustはこのことをデータから推論するので、Vec<i32>
という注釈は不要です。
ベクタをドロップすれば、要素もドロップする
他のあらゆるstruct
(構造体)と同様に、ベクタもスコープを抜ければ解放されます。
その様子をリスト8-4に示します。
fn main() { { let v = vec![1, 2, 3, 4]; // vで作業をする } // <- vはここでスコープを抜け、解放される }
リスト8-4:ベクタとその要素がドロップされる箇所を示す
ベクタがドロップされると、その中身もドロップされます。 つまり、保持されていた整数値が片付けられるということです。 これは一見単純そうですが、ベクタの要素に対する参照を使い始めると少し複雑になり得ます。 次はそれに挑戦しましょう!
ベクタの要素を読む
ベクタを生成し、更新し、破棄する方法がわかったので、次のステップでは中身を読む方法について学ぶのが良いでしょう。 ベクタに保持された値を参照する方法は2つあります。 これから示す例では、理解を助けるために、それらの関数からの戻り値型を注釈しています。
リスト8-5はベクタの値にアクセスする両方の方法として、添え字記法とget
メソッドが示されています。
fn main() { let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; println!("The third element is {}", third); match v.get(2) { // "3つ目の要素は{}です" Some(third) => println!("The third element is {}", third), // "3つ目の要素はありません。" None => println!("There is no third element."), } }
リスト8-5:添え字記法かget
メソッドを使用してベクタの要素にアクセスする
ここでは2つのことに注目してください。
1つ目は、3番目の要素を得るのに2
という添え字の値を使用していることです。
ベクタは番号で索引化されますが、その番号は0から始まります。
2つ目は、3番目の要素を得る2つの方法とは、&
と[]
を使用して参照を得るものと、get
メソッドに引数として添え字を渡してOption<&T>
を得るものだということです。
Rustのベクタには要素を参照する方法が2通りあるので、ベクタに含まれない要素の添え字を使おうとしたときのプログラムの振る舞いを選択できます。 例として、ベクタに5つ要素があるとして、添え字100の要素にアクセスを試みた場合、プログラムがどうなるのか確認しましょう。 リスト8-6に示します。
fn main() { let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100]; let does_not_exist = v.get(100); }
リスト8-6:5つの要素を含むベクタの添え字100の要素にアクセスしようとする
このコードを走らせると、最初の[]
メソッドはプログラムをパニックさせます。
なぜなら存在しない要素を参照しているからです。
このメソッドは、ベクタの終端を超えて要素にアクセスしようとしたときにプログラムをクラッシュさせたい場合に最適です。
get
メソッドにベクタ外の添え字を渡すと、パニックすることなくNone
を返します。
普通の状況でもベクタの範囲外にアクセスする可能性があるなら、このメソッドを使用することになるでしょう。
その場合、第6章で説明したように、コードはSome(&element)
かNone
を扱うロジックを持つことになります。
例えば、誰かが入力した数値が添え字になるかもしれません。
もし誤って大きすぎる値を入力し、プログラムがNone
値を得たなら、いまベクタに何要素あるかをユーザに教え、正しい値を再入力してもらうこともできます。
その方が、ただのタイプミスでプログラムをクラッシュさせるより、ユーザに優しいといえそうです。
プログラムに有効な参照がある場合、借用チェッカー (borrow checker) は、(第4章で解説しましたが)所有権と借用規則を強制し、ベクタの中身へのこの参照や他のいかなる参照も有効であり続けることを保証してくれます。
同一スコープ上では、可変と不変な参照を同時には存在させられないというルールを思い出してください。
このルールはリスト8-7でも適用されています。
リスト8-7ではベクタの最初の要素への不変参照を保持しつつ、終端に要素を追加しようとしています。
関数内のここ以降で、この要素(訳注:first
のこと)を参照しようとすると失敗します。
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);
}
リスト8-7:要素への参照を保持しつつ、ベクタに要素を追加しようとする
このコードをコンパイルすると、こんなエラーになります。
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
(エラー: 不変としても借用されているので、`v`を可変で借用できません)
--> src/main.rs:6:5
|
4 | let first = &v[0];
| - immutable borrow occurs here
| (不変借用はここで発生しています)
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
| (可変借用はここで発生しています)
7 |
8 | println!("The first element is: {}", first);
| ----- immutable borrow later used here
| (その後、不変借用はここで使われています)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections`.
To learn more, run the command again with --verbose.
リスト8-7のコードは、一見動きそうに見えるかもしれません。 なぜ最初の要素への参照が、ベクタの終端への変更を気にかける必要があるのでしょうか? このエラーはベクタが動作するしくみによるものです。 新たな要素をベクタの終端に追加するとき、いまベクタのある場所に全要素を隣り合わせに配置するだけのスペースがないなら、新しいメモリを割り当て、古い要素を新しいスペースにコピーする必要があります。 その場合、最初の要素を指す参照は、解放されたメモリを指すことになるでしょう。 借用規則がそのような状況に陥らないよう防いでくれるのです。
注釈:
Vec<T>
の実装に関する詳細については、“The Rustonomicon”を参照してください (訳注:日本語版はこちらです)。
ベクタ内の値を順に処理する
ベクタの要素に順番にアクセスしたいなら、添え字で1要素ごとにアクセスするのではなく、全要素を走査することができます。
リスト8-8でfor
ループを使い、i32
のベクタの各要素に対する不変な参照を得て、それらを表示する方法を示します。
fn main() { let v = vec![100, 32, 57]; for i in &v { println!("{}", i); } }
リスト8-8:for
ループで要素を走査し、ベクタの各要素を表示する
また、全要素に変更を加えるために、可変なベクタの各要素への可変な参照を走査することもできます。
リスト8-9のfor
ループでは各要素に50
を足しています。
fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } }
リスト8-9:ベクタの要素への可変な参照を走査する
可変参照が参照している値を変更するには、+=
演算子を使用する前に、参照外し演算子(*
)を使用してi
の値にたどり着かないといけません。
参照外し演算子については、第15章の「参照外し演算子で値までポインタを追いかける」節でより詳しく扱います。
Enumを使って複数の型を保持する
この章の冒頭で、ベクタは同じ型の値しか保持できないと述べました。 これは不便なこともあります。 異なる型の要素を保持する必要のあるユースケースは必ず存在します。 幸運なことに、enumの列挙子は同じenumの型の中に定義されるので、ベクタに異なる型の要素を保持する必要が出たら、enumを定義して使用すればよいのです!
例えば、スプレッドシートのある行から値を得ることを考えます。 ここで、その行の中の列には、整数を含むもの、浮動小数点数を含むもの、文字列を含むものがあるとします。 列挙子ごとに異なる値の型を保持するenumが定義できます。 そして、このenumの列挙子は全て同じ型、つまりenumの型、と考えられるわけです。 ですから、そのenumを保持するベクタを作成でき、結果的に異なる型を保持できるようになるわけです。 リスト8-10でこれを実演しています。
fn main() { enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from("blue")), SpreadsheetCell::Float(10.12), ]; }
リスト8-10:enum
を定義して、一つのベクタに異なる型の値を保持する
個々の要素を格納するのにヒープ上で必要となるメモリの量を正確に把握するめに、Rustコンパイラはコンパイル時にベクタに入る型を知る必要があります。
また、このベクタではどんな型が許容されるのか明示できるという副次的な利点があります。
もしRustが、ベクタにどんな型でも保持できることを許していたら、ベクタの要素に対して行われる処理に対して、いくつかの型がエラーを引き起こすかもしれません。
enumに加えてmatch
式を使うことで、第6章で説明したとおり、あらゆるケースが処理できることを、Rustがコンパイル時に保証することになります。
プログラムを書いている時点で、プログラムが実行時に取得し、ベクタに格納し得る全ての型を網羅できない場合には、このenumを使ったテクニックはうまくいかないでしょう。 代わりにトレイトオブジェクトを使用できます。 こちらは第17章で取り上げます。
これまでにベクタの代表的な使い方をいくつか紹介しました。
標準ライブラリでVec<T>
に定義されている多くの有益なメソッドについて、APIドキュメントを必ず確認するようにしてください。
例えば、push
に加えて、pop
というメソッドがあり、これは最後の要素を削除して返します。
それでは次のコレクション型であるString
に移りましょう!
文字列でUTF-8でエンコードされたテキストを保持する
第4章で文字列について語りましたが、今度はより掘り下げていきましょう。新参者のRustaceanは、 3つの概念の組み合わせにより、文字列でよく行き詰まります: Rustのありうるエラーを晒す性質、 多くのプログラマが思っている以上に文字列が複雑なデータ構造であること、そしてUTF-8です。 これらの要因が、他のプログラミング言語から移ってきた場合、一見困難に見えるように絡み合うわけです。
コレクションの文脈で文字列を議論することは、有用なことです。なぜなら、文字列はテキストとして解釈された時に有用になる機能を提供するメソッドと、
バイトのコレクションで実装されているからです。この節では、生成、更新、読み込みのような全コレクションが持つString
の処理について語ります。
また、String
が他のコレクションと異なる点についても議論します。具体的には、人間とコンピュータがString
データを解釈する方法の差異により、
String
に添え字アクセスする方法がどう複雑なのかということです。
文字列とは?
まずは、文字列という用語の意味を定義しましょう。Rustには、言語の核として1種類しか文字列型が存在しません。
文字列スライスのstr
で、通常借用された形態&str
で見かけます。第4章で、文字列スライスについて語りました。
これは、別の場所に格納されたUTF-8エンコードされた文字列データへの参照です。例えば、文字列リテラルは、
プログラムのバイナリ出力に格納されるので、文字列スライスになります。
String
型は、言語の核として組み込まれるのではなく、Rustの標準ライブラリで提供されますが、伸長可能、
可変、所有権のあるUTF-8エンコードされた文字列型です。RustaceanがRustにおいて「文字列」を指したら、
どちらかではなく、String
と文字列スライスの&str
のことを通常意味します。この節は、大方、
String
についてですが、どちらの型もRustの標準ライブラリで重宝されており、
どちらもUTF-8エンコードされています。
また、Rustの標準ライブラリには、他の文字列型も含まれています。OsString
、OsStr
、CString
、CStr
などです。
ライブラリクレートにより、文字列データを格納する選択肢はさらに増えます。
それらの名前が全てString
かStr
で終わっているのがわかりますか?所有権ありと借用されたバージョンを指しているのです。
ちょうど以前見かけたString
と&str
のようですね。例えば、これらの文字列型は、異なるエンコード方法でテキストを格納していたり、
メモリ上の表現が異なったりします。この章では、これらの他の種類の文字列については議論しません;
使用方法やどれが最適かについては、APIドキュメントを参照してください。
新規文字列を生成する
Vec<T>
で使用可能な処理の多くがString
でも使用できます。文字列を生成するnew
関数から始めましょうか。
リスト8-11に示したようにですね。
#![allow(unused)] fn main() { let mut s = String::new(); }
リスト8-11: 新しい空のString
を生成する
この行は、新しい空のs
という文字列を生成しています。それからここにデータを読み込むことができるわけです。
だいたい、文字列の初期値を決めるデータがあるでしょう。そのために、to_string
メソッドを使用します。
このメソッドは、文字列リテラルのように、Display
トレイトを実装する型ならなんでも使用できます。
リスト8-12に2例、示しています。
#![allow(unused)] fn main() { let data = "initial contents"; let s = data.to_string(); // the method also works on a literal directly: let s = "initial contents".to_string(); }
リスト8-12: to_string
メソッドを使用して文字列リテラルからString
を生成する
このコードは、initial contents
(初期値)を含む文字列を生成します。
さらに、String::from
関数を使っても、文字列リテラルからString
を生成することができます。
リスト8-13のコードは、to_string
を使用するリスト8-12のコードと等価です。
#![allow(unused)] fn main() { let s = String::from("initial contents"); }
リスト8-13: String::from
関数を使って文字列リテラルからString
を作る
文字列は、非常に多くのものに使用されるので、多くの異なる一般的なAPIを使用でき、たくさんの選択肢があるわけです。
冗長に思われるものもありますが、適材適所です!今回の場合、String::from
とto_string
は全く同じことをします。
従って、どちらを選ぶかは、スタイル次第です。
文字列はUTF-8エンコードされていることを覚えていますか?要するに文字列には、適切にエンコードされていればどんなものでも含めます。 リスト8-14に示したように。
#![allow(unused)] fn main() { let hello = String::from("السلام عليكم"); let hello = String::from("Dobrý den"); let hello = String::from("Hello"); let hello = String::from("שָׁלוֹם"); let hello = String::from("नमस्ते"); let hello = String::from("こんにちは"); let hello = String::from("안녕하세요"); let hello = String::from("你好"); let hello = String::from("Olá"); let hello = String::from("Здравствуйте"); let hello = String::from("Hola"); }
リスト8-14: いろんな言語の挨拶を文字列に保持する
これらは全て、有効なString
の値です。
文字列を更新する
String
は、サイズを伸ばすことができ、Vec<T>
の中身のように、追加のデータをプッシュすれば、中身も変化します。
付け加えると、String
値を連結する+
演算子や、format!
マクロを便利に使用することができます。
push_str
とpush
で文字列に追加する
push_str
メソッドで文字列スライスを追記することで、String
を伸ばすことができます。
リスト8-15の通りです。
#![allow(unused)] fn main() { let mut s = String::from("foo"); s.push_str("bar"); }
リスト8-15: push_str
メソッドでString
に文字列スライスを追記する
この2行の後、s
はfoobar
を含むことになります。push_str
メソッドは、必ずしも引数の所有権を得なくていいので、
文字列スライスを取ります。例えば、リスト8-16のコードは、中身をs1
に追加した後、
s2
を使えなかったら残念だということを示しています。
#![allow(unused)] fn main() { let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(s2); println!("s2 is {}", s2); }
リスト8-16: 中身をString
に追加した後に、文字列スライスを使用する
もし、push_str
メソッドがs2
の所有権を奪っていたら、最後の行でその値を出力することは不可能でしょう。
ところが、このコードは予想通りに動きます!
push
メソッドは、1文字を引数として取り、String
に追加します。リスト8-15は、
push
メソッドでlをString
に追加するコードを呈示しています。
#![allow(unused)] fn main() { let mut s = String::from("lo"); s.push('l'); }
リスト8-17: push
でString
値に1文字を追加する
このコードの結果、s
はlol
を含むことになるでしょう。
編者注:
lol
はlaughing out loud
(大笑いする)の頭文字からできたスラングです。 日本語のwww
みたいなものですね。
+
演算子、またはformat!
マクロで連結
2つのすでにある文字列を組み合わせたくなることがよくあります。リスト8-18に示したように、
一つ目の方法は、+
演算子を使用することです。
#![allow(unused)] fn main() { let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // s1はムーブされ、もう使用できないことに注意 }
リスト8-18: +
演算子を使用して二つのString
値を新しいString
値にする
このコードの結果、s3
という文字列は、Hello, world!
を含むことになるでしょう。
追記の後、s1
がもう有効でなくなった理由と、s2
への参照を使用した理由は、
+
演算子を使用した時に呼ばれるメソッドのシグニチャと関係があります。+
演算子は、add
メソッドを使用し、
そのシグニチャは以下のような感じです:
fn add(self, s: &str) -> String {
これは、標準ライブラリにあるシグニチャそのものではありません: 標準ライブラリでは、add
はジェネリクスで定義されています。
ここでは、ジェネリックな型を具体的な型に置き換えたadd
のシグニチャを見ており、これは、
このメソッドをString
値とともに呼び出した時に起こることです。ジェネリクスについては、第10章で議論します。
このシグニチャが、+
演算子の巧妙な部分を理解するのに必要な手がかりになるのです。
まず、s2
には&
がついてます。つまり、add
関数のs
引数のために最初の文字列に2番目の文字列の参照を追加するということです:
String
には&str
を追加することしかできません。要するに2つのString
値を追加することはできないのです。
でも待ってください。add
の第2引数で指定されているように、&s2
の型は、&str
ではなく、
&String
ではないですか。では、なぜ、リスト8-18は、コンパイルできるのでしょうか?
add
呼び出しで&s2
を使える理由は、コンパイラが&String
引数を&str
に型強制してくれるためです。
add
メソッド呼び出しの際、コンパイラは、参照外し型強制というものを使用し、ここでは、
&s2
を&s2[..]
に変えるものと考えることができます。参照外し型強制について詳しくは、第15章で議論します。
add
がs
引数の所有権を奪わないので、この処理後もs2
が有効なString
になるわけです。
2番目に、シグニチャからadd
はself
の所有権をもらうことがわかります。self
には&
がついていないからです。
これはつまり、リスト8-18においてs1
はadd
呼び出しにムーブされ、その後は有効ではなくなるということです。
故に、s3 = s1 + &s2;
は両文字列をコピーして新しいものを作るように見えますが、
この文は実際にはs1
の所有権を奪い、s2
の中身のコピーを追記し、結果の所有権を返すのです。言い換えると、
たくさんのコピーをしているように見えますが、違います; 実装は、コピーよりも効率的です。
複数の文字列を連結する必要が出ると、+
演算子の振る舞いは扱いにくくなります:
#![allow(unused)] fn main() { let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = s1 + "-" + &s2 + "-" + &s3; }
ここで、s
はtic-tac-toe
になるでしょう。+
と"
文字のせいで何が起きているのかわかりにくいです。
もっと複雑な文字列の連結には、format!
マクロを使用することができます:
#![allow(unused)] fn main() { let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = format!("{}-{}-{}", s1, s2, s3); }
このコードでも、s
はtic-tac-toe
になります。format!
マクロは、println!
と同様の動作をしますが、
出力をスクリーンに行う代わりに、中身をString
で返すのです。format!
を使用したコードの方がはるかに読みやすく、
引数の所有権を奪いません。
文字列に添え字アクセスする
他の多くのプログラミング言語では、文字列中の文字に、添え字で参照してアクセスすることは、有効なコードであり、
一般的な処理です。しかしながら、Rustにおいて、添え字記法でString
の一部にアクセスしようとすると、
エラーが発生するでしょう。リスト8-19の非合法なコードを考えてください。
let s1 = String::from("hello");
let h = s1[0];
リスト8-19: 文字列に対して添え字記法を試みる
このコードは、以下のようなエラーに落ち着きます:
error[E0277]: the trait bound `std::string::String: std::ops::Index<{Integer}>` is not satisfied
(エラー: トレイト境界`std::string::String: std::ops::Index<{Integer}>`が満たされていません)
|>
3 |> let h = s1[0];
|> ^^^^^ the type `std::string::String` cannot be indexed by `{Integer}`
|> (型`std::string::String`は`{Integer}`で添え字アクセスできません)
= help: the trait `std::ops::Index<{Integer}>` is not implemented for `std::string::String`
(ヘルプ: `std::ops::Index<{Integer}>`というトレイトが`std::string::String`に対して実装されていません)
エラーと注釈が全てを物語っています: Rustの文字列は、添え字アクセスをサポートしていないのです。 でも、なぜでしょうか?その疑問に答えるには、Rustがメモリにどのように文字列を保持しているかについて議論する必要があります。
内部表現
String
はVec<u8>
のラッパです。リスト8-14から適切にUTF-8でエンコードされた文字列の例をご覧ください。
まずは、これ:
#![allow(unused)] fn main() { let len = String::from("Hola").len(); }
この場合、len
は4になり、これは、文字列"Hola"を保持するベクタの長さが4バイトであることを意味します。
これらの各文字は、UTF-8でエンコードすると、1バイトになるのです。しかし、以下の行ではどうでしょうか?
(この文字列は大文字のキリル文字Zeで始まり、アラビア数字の3では始まっていないことに注意してください)
#![allow(unused)] fn main() { let len = String::from("Здравствуйте").len(); }
文字列の長さはと問われたら、あなたは12と答えるかもしれません。ところが、Rustの答えは、24です: “Здравствуйте”をUTF-8でエンコードすると、この長さになります。各Unicodeスカラー値は、2バイトの領域を取るからです。 それ故に、文字列のバイトの添え字は、必ずしも有効なUnicodeのスカラー値とは相互に関係しないのです。 デモ用に、こんな非合法なRustコードを考えてください:
let hello = "Здравствуйте";
let answer = &hello[0];
answer
の値は何になるべきでしょうか?最初の文字のЗ
になるべきでしょうか?UTF-8エンコードされた時、
З
の最初のバイトは208
、2番目は151
になるので、answer
は実際、208
になるべきですが、
208
は単独では有効な文字ではありません。この文字列の最初の文字を求めている場合、208
を返すことは、
ユーザの望んでいるものではないでしょう; しかしながら、Rustには、バイト添え字0の位置には、そのデータしかないのです。
文字列がラテン文字のみを含む場合でも、ユーザは一般的にバイト値が返ることを望みません:
&"hello"[0]
がバイト値を返す有効なコードだったら、h
ではなく、104
を返すでしょう。
予期しない値を返し、すぐには判明しないバグを引き起こさないために、Rustはこのコードを全くコンパイルせず、
開発過程の早い段階で誤解を防いでくれるのです。
バイトとスカラー値と書記素クラスタ!なんてこった!
UTF-8について別の要点は、実際Rustの観点から文字列を見るには3つの関連した方法があるということです: バイトとして、スカラー値として、そして、書記素クラスタ(人間が文字と呼ぶものに一番近い)としてです。
ヒンディー語の単語、“नमस्ते”をデーヴァナーガリー(訳注
: サンスクリット語とヒンディー語を書くときに使われる書記法)で表記したものを見たら、
以下のような見た目のu8
値のベクタとして保持されます:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
18バイトになり、このようにしてコンピュータは最終的にこのデータを保持しているわけです。これをUnicodeスカラー値として見たら
(Rustのchar
型はこれなのですが)このバイトは以下のような見た目になります:
['न', 'म', 'स', '्', 'त', 'े']
ここでは、6つchar
値がありますが、4番目と6番目は文字ではありません: 単独では意味をなさないダイアクリティックです。
最後に、書記素クラスタとして見たら、このヒンディー語の単語を作り上げる人間が4文字と呼ぶであろうものが得られます:
["न", "म", "स्", "ते"]
Rustには、データが表す自然言語に関わらず、各プログラムが必要な解釈方法を選択できるように、 コンピュータが保持する生の文字列データを解釈する方法がいろいろ用意されています。
Rustで文字を得るのにString
に添え字アクセスすることが許されない最後の理由は、
添え字アクセスという処理が常に定数時間(O(1))になると期待されるからです。
しかし、String
でそのパフォーマンスを保証することはできません。というのも、
合法な文字がいくつあるか決定するのに、最初から添え字まで中身を走査する必要があるからです。
文字列をスライスする
文字列に添え字アクセスするのは、しばしば悪い考えです。文字列添え字処理の戻り値の型が明瞭ではないからです:
バイト値、文字、書記素クラスタ、あるいは文字列スライスにもなります。故に、文字列スライスを生成するのに、
添え字を使う必要が本当に出た場合にコンパイラは、もっと特定するよう求めてきます。添え字アクセスを特定し、
文字列スライスが欲しいと示唆するためには、[]
で1つの数値により添え字アクセスするのではなく、
範囲とともに[]
を使って、特定のバイトを含む文字列スライスを作ることができます:
#![allow(unused)] fn main() { let hello = "Здравствуйте"; let s = &hello[0..4]; }
ここで、s
は文字列の最初の4バイトを含む&str
になります。先ほど、これらの文字は各々2バイトになると指摘しましたから、
s
はЗд
になります。
&hello[0..1]
と使用したら、何が起きるでしょうか?答え: Rustはベクタの非合法な添え字にアクセスしたかのように、
実行時にパニックするでしょう:
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/libcore/str/mod.rs:2188:4
('main'スレッドは「バイト添え字1は文字の境界ではありません; `Здравствуйте`の'З'(バイト番号0から2)の中です」でパニックしました)
範囲を使用して文字列スライスを作る際にはプログラムをクラッシュさせることがあるので、気をつけるべきです。
文字列を走査するメソッド群
幸いなことに、他の方法でも文字列の要素にアクセスすることができます。
もし、個々のUnicodeスカラー値に対して処理を行う必要があったら、最適な方法はchars
メソッドを使用するものです。
“नमस्ते”に対してchars
を呼び出したら、分解して6つのchar
型の値を返すので、各要素にアクセスするには、
その結果を走査すればいいわけです:
#![allow(unused)] fn main() { for c in "नमस्ते".chars() { println!("{}", c); } }
このコードは、以下のように出力します:
न
म
स
्
त
े
bytes
メソッドは、各バイトをそのまま返すので、最適になることもあるかもしれません:
#![allow(unused)] fn main() { for b in "नमस्ते".bytes() { println!("{}", b); } }
このコードは、String
をなす18バイトを出力します:
224
164
// --snip--
165
135
ですが、合法なUnicodeスカラー値は、2バイト以上からなる場合もあることは心得ておいてください。
書記素クラスタを文字列から得る方法は複雑なので、この機能は標準ライブラリでは提供されていません。 この機能が必要なら、crates.ioでクレートを入手可能です。
文字列はそう単純じゃない
まとめると、文字列は込み入っています。プログラミング言語ごとにこの複雑性をプログラマに提示する方法は違います。
Rustでは、String
データを正しく扱うことが、全てのRustプログラムにとっての既定動作になっているわけであり、
これは、プログラマがUTF-8データを素直に扱う際に、よりしっかり考えないといけないことを意味します。
このトレードオフにより、他のプログラミング言語で見えるよりも文字列の複雑性がより露出していますが、
ASCII以外の文字に関するエラーを開発の後半で扱わなければならない可能性が排除されているのです。
もう少し複雑でないものに切り替えていきましょう: ハッシュマップです!
キーとそれに紐づいた値をハッシュマップに格納する
一般的なコレクションのトリを飾るのは、ハッシュマップです。型HashMap<K, V>
は、
K
型のキーとV
型の値の対応関係を保持します。これをハッシュ関数を介して行います。
ハッシュ関数は、キーと値のメモリ配置方法を決めるものです。多くのプログラミング言語でもこの種のデータ構造はサポートされていますが、
しばしば名前が違います。hash、map、object、ハッシュテーブル、連想配列など、枚挙に暇はありません。
ハッシュマップは、ベクタのように番号ではなく、どんな型にもなりうるキーを使ってデータを参照したいときに有用です。 例えば、ゲームにおいて、各チームのスコアをハッシュマップで追いかけることができます。ここで、各キーはチーム名、 値が各チームのスコアになります。チーム名が与えられれば、スコアを扱うことができるわけです。
この節でハッシュマップの基礎的なAPIを見ていきますが、より多くのグッズが標準ライブラリにより、
HashMap<K, V>
上に定義された関数に隠されています。いつものように、
もっと情報が欲しければ、標準ライブラリのドキュメントをチェックしてください。
新規ハッシュマップを生成する
空のハッシュマップをnew
で作り、要素をinsert
で追加することができます。リスト8-20では、
名前がブルーとイエローの2チームのスコアを追いかけています。ブルーチームは10点から、イエローチームは50点から始まります。
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); }
リスト8-20: ハッシュマップを生成してキーと値を挿入する
最初に標準ライブラリのコレクション部分からHashMap
をuse
する必要があることに注意してください。
今までの3つの一般的なコレクションの内、これが最も使用頻度が低いので、初期化処理で自動的にスコープに導入される機能には含まれていません。
また、標準ライブラリからのサポートもハッシュマップは少ないです; 例えば、生成するための組み込みマクロがありません。
ベクタと全く同様に、ハッシュマップはデータをヒープに保持します。このHashMap
はキーがString
型、
値はi32
型です。ベクタのように、ハッシュマップは均質です: キーは全て同じ型でなければならず、
値も全て同じ型でなければなりません。
ハッシュマップを生成する別の方法は、タプルのベクタに対してcollect
メソッドを使用するものです。
ここで、各タプルは、キーと値から構成されています。collect
メソッドはいろんなコレクション型にデータをまとめ上げ、
そこにはHashMap
も含まれています。例として、チーム名と初期スコアが別々のベクタに含まれていたら、
zip
メソッドを使ってタプルのベクタを作り上げることができ、そこでは「ブルー」は10とペアになるなどします。
リスト8-21に示したように、それからcollect
メソッドを使って、そのタプルのベクタをハッシュマップに変換することができるわけです。
#![allow(unused)] fn main() { use std::collections::HashMap; let teams = vec![String::from("Blue"), String::from("Yellow")]; let initial_scores = vec![10, 50]; let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect(); }
リスト8-21: チームのリストとスコアのリストからハッシュマップを作る
ここでは、HashMap<_, _>
という型注釈が必要になります。なぜなら、いろんなデータ構造にまとめ上げる
ことができ、
コンパイラは指定しない限り、どれを所望なのかわからないからです。ところが、キーと値の型引数については、
アンダースコアを使用しており、コンパイラはベクタのデータ型に基づいてハッシュマップが含む型を推論することができるのです。
ハッシュマップと所有権
i32
のようなCopy
トレイトを実装する型について、値はハッシュマップにコピーされます。
String
のような所有権のある値なら、値はムーブされ、リスト8-22でデモされているように、
ハッシュマップはそれらの値の所有者になるでしょう。
#![allow(unused)] fn main() { use std::collections::HashMap; let field_name = String::from("Favorite color"); let field_value = String::from("Blue"); let mut map = HashMap::new(); map.insert(field_name, field_value); // field_name and field_value are invalid at this point, try using them and // see what compiler error you get! // field_nameとfield_valueはこの時点で無効になる。試しに使ってみて // どんなコンパイルエラーが出るか確認してみて! }
リスト8-22: 一旦挿入されたら、キーと値はハッシュマップに所有されることを示す
insert
を呼び出してfield_name
とfield_value
がハッシュマップにムーブされた後は、
これらの変数を使用することは叶いません。
値への参照をハッシュマップに挿入したら、値はハッシュマップにムーブされません。参照が指している値は、 最低でもハッシュマップが有効な間は、有効でなければなりません。これらの問題について詳細には、 第10章の「ライフタイムで参照を有効化する」節で語ります。
ハッシュマップの値にアクセスする
リスト8-23に示したように、キーをget
メソッドに提供することで、ハッシュマップから値を取り出すことができます。
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); let team_name = String::from("Blue"); let score = scores.get(&team_name); }
リスト8-23: ハッシュマップに保持されたブルーチームのスコアにアクセスする
ここで、score
はブルーチームに紐づけられた値になり、結果はSome(&10)
となるでしょう。
結果はSome
に包まれます。というのも、get
はOption<&V>
を返すからです; キーに対応する値がハッシュマップになかったら、
get
はNone
を返すでしょう。プログラムは、このOption
を第6章で講義した方法のどれかで扱う必要があるでしょう。
ベクタのように、for
ループでハッシュマップのキーと値のペアを走査することができます:
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); for (key, value) in &scores { println!("{}: {}", key, value); } }
このコードは、各ペアを任意の順番で出力します:
Yellow: 50
Blue: 10
ハッシュマップを更新する
キーと値の数は伸長可能なものの、各キーには1回に1つの値しか紐づけることができません。 ハッシュマップ内のデータを変えたい時は、すでにキーに値が紐づいている場合の扱い方を決めなければなりません。 古い値を新しい値で置き換えて、古い値を完全に無視することもできます。古い値を保持して、 新しい値を無視し、キーにまだ値がない場合に新しい値を追加するだけにすることもできます。 あるいは、古い値と新しい値を組み合わせることもできます。各方法について見ていきましょう!
値を上書きする
キーと値をハッシュマップに挿入し、同じキーを異なる値で挿入したら、そのキーに紐づけられている値は置換されます。
リスト8-24のコードは、insert
を二度呼んでいるものの、ハッシュマップには一つのキーと値の組しか含まれません。
なぜなら、ブルーチームキーに対する値を2回とも挿入しているからです。
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Blue"), 25); println!("{:?}", scores); }
リスト8-24: 特定のキーで保持された値を置き換える
このコードは、{"Blue": 25}
と出力するでしょう。10
という元の値は上書きされたのです。
キーに値がなかった時のみ値を挿入する
特定のキーに値があるか確認することは一般的であり、存在しない時に値を挿入することも一般的です。
ハッシュマップには、これを行うentry
と呼ばれる特別なAPIがあり、これは、引数としてチェックしたいキーを取ります。
このentry
メソッドの戻り値は、Entry
と呼ばれるenumであり、これは存在したりしなかったりする可能性のある値を表します。
イエローチームに対するキーに値が紐づけられているか否か確認したくなったとしましょう。存在しなかったら、
50という値を挿入したく、ブルーチームに対しても同様です。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); }
リスト8-25: entry
メソッドを使ってキーに値がない場合だけ挿入する
Entry
上のor_insert
メソッドは、対応するEntry
キーが存在した時にそのキーに対する値への可変参照を返すために定義されており、
もしなかったら、引数をこのキーの新しい値として挿入し、新しい値への可変参照を返します。このテクニックの方が、
そのロジックを自分で書くよりもはるかに綺麗な上に、borrow checkerとも親和性が高くなります。
リスト8-25のコードを実行すると、{"Yellow": 50, "Blue": 10}
と出力するでしょう。
最初のentry
呼び出しは、まだイエローチームに対する値がないので、値50でイエローチームのキーを挿入します。
entry
の2回目の呼び出しはハッシュマップを変更しません。なぜなら、ブルーチームには既に10という値があるからです。
古い値に基づいて値を更新する
ハッシュマップの別の一般的なユースケースは、キーの値を探し、古い値に基づいてそれを更新することです。 例えば、リスト8-26は、各単語があるテキストに何回出現するかを数え上げるコードを示しています。 キーに単語を入れたハッシュマップを使用し、その単語を何回見かけたか追いかけるために値を増やします。 ある単語を見かけたのが最初だったら、まず0という値を挿入します:
#![allow(unused)] fn main() { use std::collections::HashMap; let text = "hello world wonderful world"; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(0); *count += 1; } println!("{:?}", map); }
リスト8-26: 単語とカウントを保持するハッシュマップを使って単語の出現数をカウントする
このコードは、{"world": 2, "hello": 1, "wonderful": 1}
と出力するでしょう。
or_insert
関数は実際、このキーに対する値への可変参照(&mut V
)を返すのです。
ここでその可変参照をcount
変数に保持しているので、その値に代入するには、
まずアスタリスク(*
)でcount
を参照外ししなければならないのです。この可変参照は、
for
ループの終端でスコープを抜けるので、これらの変更は全て安全であり、借用規則により許可されるのです。
ハッシュ関数
標準では、HashMap
はサービス拒否(DoS)アタックに対して抵抗を示す暗号学的に安全なハッシュ関数を使用します。
これは、利用可能な最速のハッシュアルゴリズムではありませんが、パフォーマンスの欠落と引き換えに安全性を得るというトレードオフは、
価値があります。自分のコードをプロファイリングして、自分の目的では標準のハッシュ関数は遅すぎると判明したら、
異なるhasherを指定することで別の関数に切り替えることができます。hasherとは、
BuildHasher
トレイトを実装する型のことです。トレイトについてとその実装方法については、第10章で語ります。
必ずしも独自のhasherを1から作り上げる必要はありません; crates.ioには、
他のRustユーザによって共有された多くの一般的なハッシュアルゴリズムを実装したhasherを提供するライブラリがあります。
まとめ
ベクタ、文字列、ハッシュマップはデータを保持し、アクセスし、変更する必要のあるプログラムで必要になる、 多くの機能を提供してくれるでしょう。今なら解決可能なはずの練習問題を用意しました:
- 整数のリストが与えられ、ベクタを使ってmean(平均値)、median(ソートされた時に真ん中に来る値)、 mode(最も頻繁に出現する値; ハッシュマップがここでは有効活用できるでしょう)を返してください。
- 文字列をピッグ・ラテン(
訳注
: 英語の言葉遊びの一つ)に変換してください。各単語の最初の子音は、 単語の終端に移り、"ay"が足されます。従って、"first"は"irst-fay"になります。ただし、 母音で始まる単語には、お尻に"hay"が付け足されます("apple"は"apple-hay"になります)。 UTF-8エンコードに関する詳細を心に留めておいてください! - ハッシュマップとベクタを使用して、ユーザに会社の部署に雇用者の名前を追加させられるテキストインターフェイスを作ってください。 例えば、"Add Sally to Engineering"(開発部門にサリーを追加)や"Add Amir to Sales"(販売部門にアミールを追加)などです。 それからユーザに、ある部署にいる人間の一覧や部署ごとにアルファベット順で並べ替えられた会社の全人間の一覧を扱わせてあげてください。
標準ライブラリのAPIドキュメントには、この練習問題に有用な、ベクタ、文字列、ハッシュマップのメソッドが解説されています。
処理が失敗することもあるような、より複雑なプログラムに入り込んできています; ということは、 エラーの処理法について議論するのにぴったりということです。次はそれをします!
エラー処理
Rustの信頼性への傾倒は、エラー処理にも及びます。ソフトウェアにおいて、エラーは生きている証しです。 従って、Rustには何かがおかしくなる場面を扱う機能がたくさんあります。多くの場面で、 コンパイラは、プログラマにエラーの可能性を知り、コードのコンパイルが通るまでに何かしら対応を行うことを要求してきます。 この要求により、エラーを発見し、コードを実用に供する前に適切に対処していることを確認することでプログラムを頑健なものにしてくれるのです!
Rustでは、エラーは大きく二つに分類されます: 回復可能と回復不能なエラーです。 ファイルが見つからないなどの回復可能なエラーには、問題をユーザに報告し、処理を再試行することが合理的になります。 回復不能なエラーは、常にバグの兆候です。例えば、配列の境界を超えた箇所にアクセスしようとすることなどです。
多くの言語では、この2種のエラーを区別することはなく、例外などの機構を使用して同様に扱います。
Rustには例外が存在しません。代わりに、回復可能なエラーにはResult<T, E>
値があり、
プログラムが回復不能なエラーに遭遇した時には、実行を中止するpanic!
マクロがあります。
この章では、まずpanic!
の呼び出しを講義し、それからResult<T, E>
を戻り値にする話をします。
加えて、エラーからの回復を試みるか、実行を中止するか決定する際に考慮すべき事項についても、探究しましょう。
panic!
で回復不能なエラー
時として、コードで悪いことが起きるものです。そして、それに対してできることは何もありません。
このような場面で、Rustにはpanic!
マクロが用意されています。panic!
マクロが実行されると、
プログラムは失敗のメッセージを表示し、スタックを巻き戻し掃除して、終了します。これが最もありふれて起こるのは、
何らかのバグが検出された時であり、プログラマには、どうエラーを処理すればいいか明確ではありません。
パニックに対してスタックを巻き戻すか異常終了するか
標準では、パニックが発生すると、プログラムは巻き戻しを始めます。つまり、言語がスタックを遡り、 遭遇した各関数のデータを片付けるということです。しかし、この遡りと片付けはすべきことが多くなります。 対立案は、即座に異常終了し、片付けをせずにプログラムを終了させることです。そうなると、プログラムが使用していたメモリは、 OSが片付ける必要があります。プロジェクトにおいて、実行可能ファイルを極力小さくする必要があれば、 Cargo.tomlファイルの適切な
[profile]
欄にpanic = 'abort'
を追記することで、 パニック時に巻き戻しから異常終了するように切り替えることができます。例として、 リリースモード時に異常終了するようにしたければ、以下を追記してください:[profile.release] panic = 'abort'
単純なプログラムでpanic!
の呼び出しを試してみましょう:
ファイル名: src/main.rs
fn main() { panic!("crash and burn"); //クラッシュして炎上 }
このプログラムを実行すると、以下のような出力を目の当たりにするでしょう:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.25 secs
Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2:4
('main'スレッドはsrc/main.rs:2:4の「クラッシュして炎上」でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.
panic!
の呼び出しが、最後の2行に含まれるエラーメッセージを発生させているのです。
1行目にパニックメッセージとソースコード中でパニックが発生した箇所を示唆しています:
src/main.rs:2:4は、src/main.rsファイルの2行目4文字目であることを示しています。
この場合、示唆される行は、自分のコードの一部で、その箇所を見に行けば、panic!
マクロ呼び出しがあるわけです。
それ以外では、panic!
呼び出しが、自分のコードが呼び出しているコードの一部になっている可能性もあるわけです。
エラーメッセージで報告されるファイル名と行番号が、結果的にpanic!
呼び出しに導いた自分のコードの行ではなく、
panic!
マクロが呼び出されている他人のコードになるでしょう。panic!
呼び出しの発生元である関数のバックトレースを使用して、
問題を起こしている自分のコードの箇所を割り出すことができます。バックトレースがどんなものか、次に議論しましょう。
panic!
バックトレースを使用する
別の例を眺めて、自分のコードでマクロを直接呼び出す代わりに、コードに存在するバグにより、
ライブラリでpanic!
呼び出しが発生するとどんな感じなのか確かめてみましょう。リスト9-1は、
添え字でベクタの要素にアクセスを試みる何らかのコードです。
ファイル名: src/main.rs
fn main() { let v = vec![1, 2, 3]; v[99]; }
リスト9-1: ベクタの境界を超えて要素へのアクセスを試み、panic!
の呼び出しを発生させる
ここでは、ベクタの100番目の要素(添え字は0始まりなので添え字99)にアクセスを試みていますが、ベクタには3つしか要素がありません。
この場面では、Rustはパニックします。[]
の使用は、要素を返すと想定されるものの、
無効な添え字を渡せば、ここでRustが返せて正しいと思われる要素は何もないわけです。
他の言語(Cなど)では、この場面で欲しいものではないにもかかわらず、まさしく要求したものを返そうとしてきます:
メモリがベクタに属していないにもかかわらず、ベクタ内のその要素に対応するメモリ上の箇所にあるものを何か返してくるのです。
これは、バッファー外読み出し(buffer overread; 訳注
: バッファー読みすぎとも解釈できるか)と呼ばれ、
攻撃者が、配列の後に格納された読めるべきでないデータを読み出せるように添え字を操作できたら、
セキュリティ脆弱性につながる可能性があります。
この種の脆弱性からプログラムを保護するために、存在しない添え字の要素を読もうとしたら、 Rustは実行を中止し、継続を拒みます。試して確認してみましょう:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.27 secs
Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is
99', /checkout/src/liballoc/vec.rs:1555:10
('main'スレッドは、/checkout/src/liballoc/vec.rs:1555:10の
「境界外番号: 長さは3なのに、添え字は99です」でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.
このエラーは、自分のファイルではないvec.rsファイルを指しています。
標準ライブラリのVec<T>
の実装です。ベクタv
に対して[]
を使った時に走るコードは、
vec.rsに存在し、ここで実際にpanic!
が発生しているのです。
その次の注釈行は、RUST_BACKTRACE
環境変数をセットして、まさしく何が起き、
エラーが発生したのかのバックトレースを得られることを教えてくれています。
バックトレースとは、ここに至るまでに呼び出された全関数の一覧です。Rustのバックトレースも、
他の言語同様に動作します: バックトレースを読むコツは、頭からスタートして自分のファイルを見つけるまで読むことです。
そこが、問題の根源になるのです。自分のファイルを言及している箇所以前は、自分のコードで呼び出したコードになります;
以後は、自分のコードを呼び出しているコードになります。これらの行には、Rustの核となるコード、標準ライブラリのコード、
使用しているクレートなどが含まれるかもしれません。RUST_BACKTRACE
環境変数を0以外の値にセットして、
バックトレースを出力してみましょう。リスト9-2のような出力が得られるでしょう。
$ RUST_BACKTRACE=1 cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', /checkout/src/liballoc/vec.rs:1555:10
stack backtrace:
0: std::sys::imp::backtrace::tracing::imp::unwind_backtrace
at /checkout/src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
1: std::sys_common::backtrace::_print
at /checkout/src/libstd/sys_common/backtrace.rs:71
2: std::panicking::default_hook::{{closure}}
at /checkout/src/libstd/sys_common/backtrace.rs:60
at /checkout/src/libstd/panicking.rs:381
3: std::panicking::default_hook
at /checkout/src/libstd/panicking.rs:397
4: std::panicking::rust_panic_with_hook
at /checkout/src/libstd/panicking.rs:611
5: std::panicking::begin_panic
at /checkout/src/libstd/panicking.rs:572
6: std::panicking::begin_panic_fmt
at /checkout/src/libstd/panicking.rs:522
7: rust_begin_unwind
at /checkout/src/libstd/panicking.rs:498
8: core::panicking::panic_fmt
at /checkout/src/libcore/panicking.rs:71
9: core::panicking::panic_bounds_check
at /checkout/src/libcore/panicking.rs:58
10: <alloc::vec::Vec<T> as core::ops::index::Index<usize>>::index
at /checkout/src/liballoc/vec.rs:1555
11: panic::main
at src/main.rs:4
12: __rust_maybe_catch_panic
at /checkout/src/libpanic_unwind/lib.rs:99
13: std::rt::lang_start
at /checkout/src/libstd/panicking.rs:459
at /checkout/src/libstd/panic.rs:361
at /checkout/src/libstd/rt.rs:61
14: main
15: __libc_start_main
16: <unknown>
リスト9-2: RUST_BACKTRACE
環境変数をセットした時に表示される、
panic!
呼び出しが生成するバックトレース
出力が多いですね!OSやRustのバージョンによって、出力の詳細は変わる可能性があります。この情報とともに、
バックトレースを得るには、デバッグシンボルを有効にしなければなりません。デバッグシンボルは、
--release
オプションなしでcargo build
やcargo run
を使用していれば、標準で有効になり、
ここではそうなっています。
リスト9-2の出力で、バックトレースの11行目が問題発生箇所を指し示しています: src/main.rsの4行目です。 プログラムにパニックしてほしくなければ、自分のファイルについて言及している最初の行で示されている箇所が、 どのようにパニックを引き起こす値でこの箇所にたどり着いたか割り出すために調査を開始すべき箇所になります。 バックトレースの使用法を模擬するためにわざとパニックするコードを書いたリスト9-1において、 パニックを解消する方法は、3つしか要素のないベクタの添え字99の要素を要求しないことです。 将来コードがパニックしたら、パニックを引き起こすどんな値でコードがどんな動作をしているのかと、 代わりにコードは何をすべきなのかを算出する必要があるでしょう。
この章の後ほど、「panic!
するかpanic!
するまいか」節でpanic!
とエラー状態を扱うのにpanic!
を使うべき時と使わぬべき時に戻ってきます。
次は、Result
を使用してエラーから回復する方法を見ましょう。
Result
で回復可能なエラー
多くのエラーは、プログラムを完全にストップさせるほど深刻ではありません。時々、関数が失敗した時に、 容易に解釈し、対応できる理由によることがあります。例えば、ファイルを開こうとして、 ファイルが存在しないために処理が失敗したら、プロセスを停止するのではなく、ファイルを作成したいことがあります。
第2章の「Result
型で失敗する可能性に対処する」でResult
enumが以下のように、
Ok
とErr
の2列挙子からなるよう定義されていることを思い出してください:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
T
とE
は、ジェネリックな型引数です: ジェネリクスについて詳しくは、第10章で議論します。
たった今知っておく必要があることは、T
が成功した時にOk
列挙子に含まれて返される値の型を表すことと、
E
が失敗した時にErr
列挙子に含まれて返されるエラーの型を表すことです。Result
はこのようなジェネリックな型引数を含むので、
標準ライブラリ上に定義されているResult
型や関数などを、成功した時とエラーの時に返したい値が異なるような様々な場面で使用できるのです。
関数が失敗する可能性があるためにResult
値を返す関数を呼び出しましょう: リスト9-3では、
ファイルを開こうとしています。
ファイル名: src/main.rs
use std::fs::File; fn main() { let f = File::open("hello.txt"); }
リスト9-3: ファイルを開く
File::open
がResult
を返すとどう知るのでしょうか?標準ライブラリのAPIドキュメントを参照することもできますし、
コンパイラに尋ねることもできます!f
に関数の戻り値ではないと判明している型注釈を与えて、
コードのコンパイルを試みれば、コンパイラは型が合わないと教えてくれるでしょう。そして、エラーメッセージは、
f
の実際の型を教えてくれるでしょう。試してみましょう!File::open
の戻り値の型はu32
ではないと判明しているので、
let f
文を以下のように変更しましょう:
let f: u32 = File::open("hello.txt");
これでコンパイルしようとすると、以下のような出力が得られます:
error[E0308]: mismatched types
(エラー: 型が合いません)
--> src/main.rs:4:18
|
4 | let f: u32 = File::open("hello.txt");
| ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
`std::result::Result`
|
= note: expected type `u32`
(注釈: 予期した型は`u32`です)
found type `std::result::Result<std::fs::File, std::io::Error>`
(実際の型は`std::result::Result<std::fs::File, std::io::Error>`です)
これにより、File::open
関数の戻り値の型は、Result<T, E>
であることがわかります。ジェネリック引数のT
は、
ここでは成功値の型std::fs::File
で埋められていて、これはファイルハンドルです。
エラー値で使用されているE
の型は、std::io::Error
です。
この戻り値型は、File::open
の呼び出しが成功し、読み込みと書き込みを行えるファイルハンドルを返す可能性があることを意味します。
また、関数呼び出しは失敗もする可能性があります: 例えば、ファイルが存在しない可能性、ファイルへのアクセス権限がない可能性です。
File::open
には成功したか失敗したかを知らせる方法とファイルハンドルまたは、エラー情報を与える方法が必要なのです。
この情報こそがResult
enumが伝達するものなのです。
File::open
が成功した場合、変数f
の値はファイルハンドルを含むOk
インスタンスになります。
失敗した場合には、発生したエラーの種類に関する情報をより多く含むErr
インスタンスがf
の値になります。
リスト9-3のコードに追記をしてFile::open
が返す値に応じて異なる動作をする必要があります。
リスト9-4に基礎的な道具を使ってResult
を扱う方法を一つ示しています。第6章で議論したmatch
式です。
ファイル名: src/main.rs
use std::fs::File; fn main() { let f = File::open("hello.txt"); let f = match f { Ok(file) => file, Err(error) => { // ファイルを開く際に問題がありました panic!("There was a problem opening the file: {:?}", error) }, }; }
リスト9-4: match
式を使用して返却される可能性のあるResult
列挙子を処理する
Option
enumのように、Result
enumとその列挙子は、初期化処理でインポートされているので、
match
アーム内でOk
とErr
列挙子の前にResult::
を指定する必要がないことに注目してください。
ここでは、結果がOk
の時に、Ok
列挙子から中身のfile
値を返すように指示し、
それからそのファイルハンドル値を変数f
に代入しています。match
の後には、
ファイルハンドルを使用して読み込んだり書き込むことができるわけです。
match
のもう一つのアームは、File::open
からErr
値が得られたケースを処理しています。
この例では、panic!
マクロを呼び出すことを選択しています。カレントディレクトリにhello.txtというファイルがなく、
このコードを走らせたら、panic!
マクロからの以下のような出力を目の当たりにするでしょう:
thread 'main' panicked at 'There was a problem opening the file: Error { repr:
Os { code: 2, message: "No such file or directory" } }', src/main.rs:9:12
('main'スレッドは、src/main.rs:9:12の「ファイルを開く際に問題がありました: Error{ repr:
Os { code: 2, message: "そのような名前のファイルまたはディレクトリはありません"}}」でパニックしました)
通常通り、この出力は、一体何がおかしくなったのかを物語っています。
色々なエラーにマッチする
リスト9-4のコードは、File::open
が失敗した理由にかかわらずpanic!
します。代わりにしたいことは、
失敗理由によって動作を変えることです: ファイルが存在しないためにFile::open
が失敗したら、
ファイルを作成し、その新しいファイルへのハンドルを返したいです。他の理由(例えばファイルを開く権限がなかったなど)で、
File::open
が失敗したら、リスト9-4のようにコードにはpanic!
してほしいのです。
リスト9-5を眺めてください。ここではmatch
に別のアームを追加しています。
ファイル名: src/main.rs
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(ref error) if error.kind() == ErrorKind::NotFound => {
match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => {
panic!(
//ファイルを作成しようとしましたが、問題がありました
"Tried to create file but there was a problem: {:?}",
e
)
},
}
},
Err(error) => {
panic!(
"There was a problem opening the file: {:?}",
error
)
},
};
}
リスト9-5: 色々な種類のエラーを異なる方法で扱う
File::open
がErr
列挙子に含めて返す値の型は、io::Error
であり、これは標準ライブラリで提供されている構造体です。
この構造体には、呼び出すとio::ErrorKind
値が得られるkind
メソッドがあります。io::ErrorKind
というenumは、
標準ライブラリで提供されていて、io
処理の結果発生する可能性のある色々な種類のエラーを表す列挙子があります。
使用したい列挙子は、ErrorKind::NotFound
で、これは開こうとしているファイルがまだ存在しないことを示唆します。
if error.kind() == ErrorKind::Notfound
という条件式は、マッチガードと呼ばれます:
アームのパターンをさらに洗練するmatch
アーム上のおまけの条件式です。この条件式は、
そのアームのコードが実行されるには真でなければいけないのです; そうでなければ、
パターンマッチングは継続し、match
の次のアームを考慮します。パターンのref
は、
error
がガード条件式にムーブされないように必要ですが、ただ単にガード式に参照されます。
ref
を使用して&
の代わりにパターン内で参照を作っている理由は、第18章で詳しく講義します。
手短に言えば、パターンの文脈において、&
は参照にマッチし、その値を返しますが、
ref
は値にマッチし、それへの参照を返すということなのです。
マッチガードで精査したい条件は、error.kind()
により返る値が、ErrorKind
enumのNotFound
列挙子であるかということです。
もしそうなら、File::create
でファイル作成を試みます。ところが、File::create
も失敗する可能性があるので、
内部にもmatch
式を追加する必要があるのです。ファイルが開けないなら、異なるエラーメッセージが出力されるでしょう。
外側のmatch
の最後のアームは同じままなので、ファイルが存在しないエラー以外ならプログラムはパニックします。
エラー時にパニックするショートカット: unwrap
とexpect
match
の使用は、十分に仕事をしてくれますが、いささか冗長になり得る上、必ずしも意図をよく伝えるとは限りません。
Result<T, E>
型には、色々な作業をするヘルパーメソッドが多く定義されています。それらの関数の一つは、
unwrap
と呼ばれますが、リスト9-4で書いたmatch
式と同じように実装された短絡メソッドです。
Result
値がOk
列挙子なら、unwrap
はOk
の中身を返します。Result
がErr
列挙子なら、
unwrap
はpanic!
マクロを呼んでくれます。こちらが実際に動作しているunwrap
の例です:
ファイル名: src/main.rs
use std::fs::File; fn main() { let f = File::open("hello.txt").unwrap(); }
このコードをhello.txtファイルなしで走らせたら、unwrap
メソッドが行うpanic!
呼び出しからのエラーメッセージを目の当たりにするでしょう:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
repr: Os { code: 2, message: "No such file or directory" } }',
src/libcore/result.rs:906:4
('main'スレッドは、src/libcore/result.rs:906:4の
「`Err`値に対して`Result::unwrap()`が呼び出されました: Error{
repr: Os { code: 2, message: "そのようなファイルまたはディレクトリはありません" } }」でパニックしました)
別のメソッドexpect
は、unwrap
に似ていますが、panic!
のエラーメッセージも選択させてくれます。
unwrap
の代わりにexpect
を使用して、いいエラーメッセージを提供すると、意図を伝え、
パニックの原因をたどりやすくしてくれます。expect
の表記はこんな感じです:
ファイル名: src/main.rs
use std::fs::File; fn main() { // hello.txtを開くのに失敗しました let f = File::open("hello.txt").expect("Failed to open hello.txt"); }
expect
をunwrap
と同じように使用してます: ファイルハンドルを返したり、panic!
マクロを呼び出しています。
expect
がpanic!
呼び出しで使用するエラーメッセージは、unwrap
が使用するデフォルトのpanic!
メッセージではなく、
expect
に渡した引数になります。以下のようになります:
thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
2, message: "No such file or directory" } }', src/libcore/result.rs:906:4
このエラーメッセージは、指定したテキストのhello.txtを開くのに失敗しました
で始まっているので、
コード内のどこでエラーメッセージが出力されたのかより見つけやすくなるでしょう。複数箇所でunwrap
を使用していたら、
ズバリどのunwrap
がパニックを引き起こしているのか理解するのは、より時間がかかる可能性があります。
パニックするunwrap
呼び出しは全て、同じメッセージを出力するからです。
エラーを委譲する
失敗する可能性のある何かを呼び出す実装をした関数を書く際、関数内でエラーを処理する代わりに、 呼び出し元がどうするかを決められるようにエラーを返すことができます。これはエラーの委譲として認知され、 自分のコードの文脈で利用可能なものよりも、 エラーの処理法を規定する情報やロジックがより多くある呼び出し元のコードに制御を明け渡します。
例えば、リスト9-6の関数は、ファイルからユーザ名を読み取ります。ファイルが存在しなかったり、読み込みできなければ、 この関数はそのようなエラーを呼び出し元のコードに返します。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::io; use std::io::Read; use std::fs::File; fn read_username_from_file() -> Result<String, io::Error> { let f = File::open("hello.txt"); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(e) => Err(e), } } }
リスト9-6: match
でエラーを呼び出し元のコードに返す関数
まずは、関数の戻り値型に注目してください: Result<String, io::Error>
です。つまり、この関数は、
Result<T, E>
型の値を返しているということです。ここでジェネリック引数のT
は、具体型String
で埋められ、
ジェネリック引数のE
は具体型io::Error
で埋められています。この関数が何の問題もなく成功すれば、
この関数を呼び出したコードは、String
(関数がファイルから読み取ったユーザ名)を保持するOk
値を受け取ります。
この関数が何か問題に行き当たったら、呼び出し元のコードはio::Error
のインスタンスを保持するErr
値を受け取り、
このio::Error
は問題の内容に関する情報をより多く含んでいます。関数の戻り値の型にio::Error
を選んだのは、
この関数本体で呼び出している失敗する可能性のある処理が両方とも偶然この型をエラー値として返すからです:
File::open
関数とread_to_string
メソッドです。
関数の本体は、File::open
関数を呼び出すところから始まります。そして、リスト9-4のmatch
に似たmatch
で返ってくるResult
値を扱い、
Err
ケースにpanic!
を呼び出すだけの代わりに、この関数から早期リターンしてこの関数のエラー値として、
File::open
から得たエラー値を呼び出し元に渡し戻します。File::open
が成功すれば、
ファイルハンドルを変数f
に保管して継続します。
さらに、変数s
に新規String
を生成し、f
のファイルハンドルに対してread_to_string
を呼び出して、
ファイルの中身をs
に読み出します。File::open
が成功しても、失敗する可能性があるので、read_to_string
メソッドも、
Result
を返却します。そのResult
を処理するために別のmatch
が必要になります: read_to_string
が成功したら、
関数は成功し、今はOk
に包まれたs
に入っているファイルのユーザ名を返却します。read_to_string
が失敗したら、
File::open
の戻り値を扱ったmatch
でエラー値を返したように、エラー値を返します。
しかし、明示的にreturn
を述べる必要はありません。これが関数の最後の式だからです。
そうしたら、呼び出し元のコードは、ユーザ名を含むOk
値か、io::Error
を含むErr
値を得て扱います。
呼び出し元のコードがそれらの値をどうするかはわかりません。呼び出しコードがErr
値を得たら、
例えば、panic!
を呼び出してプログラムをクラッシュさせたり、デフォルトのユーザ名を使ったり、
ファイル以外の場所からユーザ名を検索したりできるでしょう。呼び出し元のコードが実際に何をしようとするかについて、
十分な情報がないので、成功や失敗情報を全て委譲して適切に扱えるようにするのです。
Rustにおいて、この種のエラー委譲は非常に一般的なので、Rustにはこれをしやすくする?
演算子が用意されています。
エラー委譲のショートカット: ?
演算子
リスト9-7もリスト9-6と同じ機能を有するread_username_from_file
の実装ですが、
こちらは?
演算子を使用しています:
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::io; use std::io::Read; use std::fs::File; fn read_username_from_file() -> Result<String, io::Error> { let mut f = File::open("hello.txt")?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s) } }
リスト9-7: ?
演算子でエラーを呼び出し元に返す関数
Result
値の直後に置かれた?
は、リスト9-6でResult
値を処理するために定義したmatch
式とほぼ同じように動作します。
Result
の値がOk
なら、Ok
の中身がこの式から返ってきて、プログラムは継続します。値がErr
なら、
return
キーワードを使ったかのように関数全体からErr
の中身が返ってくるので、
エラー値は呼び出し元のコードに委譲されます。
リスト9-6のmatch
式と?
演算子には違いがあります: ?
を使ったエラー値は、
標準ライブラリのFrom
トレイトで定義され、エラーの型を別のものに変換するfrom
関数を通ることです。
?
演算子がfrom
関数を呼び出すと、受け取ったエラー型が現在の関数の戻り値型で定義されているエラー型に変換されます。これは、
個々がいろんな理由で失敗する可能性があるのにも関わらず、関数が失敗する可能性を全て一つのエラー型で表現して返す時に有用です。
各エラー型がfrom
関数を実装して返り値のエラー型への変換を定義している限り、
?
演算子が変換の面倒を自動的に見てくれます。
リスト9-7の文脈では、File::open
呼び出し末尾の?
はOk
の中身を変数f
に返します。
エラーが発生したら、?
演算子により関数全体から早期リターンし、あらゆるErr
値を呼び出し元に与えます。
同じ法則がread_to_string
呼び出し末尾の?
にも適用されます。
?
演算子により定型コードの多くが排除され、この関数の実装を単純にしてくれます。
リスト9-8で示したように、?
の直後のメソッド呼び出しを連結することでさらにこのコードを短くすることさえもできます。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::io; use std::io::Read; use std::fs::File; fn read_username_from_file() -> Result<String, io::Error> { let mut s = String::new(); File::open("hello.txt")?.read_to_string(&mut s)?; Ok(s) } }
リスト9-8: ?
演算子の後のメソッド呼び出しを連結する
s
の新規String
の生成を関数の冒頭に移動しました; その部分は変化していません。変数f
を生成する代わりに、
read_to_string
の呼び出しを直接File::open("hello.txt")?
の結果に連結させました。
それでも、read_to_string
呼び出しの末尾には?
があり、File::open
とread_to_string
両方が成功したら、
エラーを返すというよりもそれでも、s
にユーザ名を含むOk
値を返します。機能もまたリスト9-6及び、9-7と同じです;
ただ単に異なるバージョンのよりエルゴノミックな書き方なのです。
?
演算子は、Result
を返す関数でしか使用できない
?
演算子は戻り値にResult
を持つ関数でしか使用できません。というのも、リスト9-6で定義したmatch
式と同様に動作するよう、
定義されているからです。Result
の戻り値型を要求するmatch
の部品は、return Err(e)
なので、
関数の戻り値はこのreturn
と互換性を保つためにResult
でなければならないのです。
main
関数で?
演算子を使用したらどうなるか見てみましょう。main
関数は、戻り値が()
でしたね:
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?;
}
このコードをコンパイルすると、以下のようなエラーメッセージが得られます:
error[E0277]: the trait bound `(): std::ops::Try` is not satisfied
(エラー: `(): std::ops::Try`というトレイト境界が満たされていません)
--> src/main.rs:4:13
|
4 | let f = File::open("hello.txt")?;
| ------------------------
| |
| the `?` operator can only be used in a function that returns
`Result` (or another type that implements `std::ops::Try`)
| in this macro invocation
| (このマクロ呼び出しの`Result`(かまたは`std::ops::Try`を実装する他の型)を返す関数でしか`?`演算子は使用できません)
|
= help: the trait `std::ops::Try` is not implemented for `()`
(助言: `std::ops::Try`トレイトは`()`には実装されていません)
= note: required by `std::ops::Try::from_error`
(注釈: `std::ops::Try::from_error`で要求されています)
このエラーは、?
演算子はResult
を返す関数でしか使用が許可されないと指摘しています。
Result
を返さない関数では、Result
を返す別の関数を呼び出した時、
?
演算子を使用してエラーを呼び出し元に委譲する可能性を生み出す代わりに、match
かResult
のメソッドのどれかを使う必要があるでしょう。
さて、panic!
呼び出しやResult
を返す詳細について議論し終えたので、
どんな場合にどちらを使うのが適切か決める方法についての話に戻りましょう。
panic!
すべきかするまいか
では、panic!
すべき時とResult
を返すべき時はどう決定すればいいのでしょうか?コードがパニックしたら、
回復する手段はありません。回復する可能性のある手段の有る無しに関わらず、どんなエラー場面でもpanic!
を呼ぶことはできますが、
そうすると、呼び出す側のコードの立場に立ってこの場面は回復不能だという決定を下すことになります。
Result
値を返す決定をすると、決断を下すのではなく、呼び出し側に選択肢を与えることになります。
呼び出し側は、場面に合わせて回復を試みることを決定したり、この場合のErr
値は回復不能と断定して、
panic!
を呼び出し、回復可能だったエラーを回復不能に変換することもできます。故に、Result
を返却することは、
失敗する可能性のある関数を定義する際には、いい第一選択肢になります。
稀な場面では、Result
を返すよりもパニックするコードを書く方がより適切になることもあります。
例やプロトタイプコード、テストでパニックするのが適切な理由を探ってみましょう。
それからコンパイラではありえない失敗だと気づけなくとも、人間なら気づける場面を議論しましょう。
そして、ライブラリコードでパニックするか決定する方法についての一般的なガイドラインで結論づけましょう。
例、プロトタイプコード、テスト
例を記述して何らかの概念を具体化している時、頑健なエラー処理コードも例に含むことは、例の明瞭さを欠くことになりかねません。
例において、unwrap
などのパニックする可能性のあるメソッド呼び出しは、
アプリケーションにエラーを処理してほしい方法へのプレースホルダーを意味していると理解され、
これは残りのコードがしていることによって異なる可能性があります。
同様に、unwrap
やexpect
メソッドは、エラーの処理法を決定する準備ができる前、プロトタイプの段階では、
非常に便利です。それらにより、コードにプログラムをより頑健にする時の明らかなマーカーが残されるわけです。
メソッド呼び出しがテスト内で失敗したら、そのメソッドがテスト下に置かれた機能ではなかったとしても、
テスト全体が失敗してほしいでしょう。panic!
が、テストが失敗と印づけられる手段なので、
unwrap
やexpect
の呼び出しはスバリ起こるべきことです。
コンパイラよりもプログラマがより情報を持っている場合
Result
がOk
値であると確認する何らかの別のロジックがある場合、unwrap
を呼び出すことは適切でしょうが、
コンパイラは、そのロジックを理解はしません。それでも、処理する必要のあるResult
は存在するでしょう:
呼び出している処理が何であれ、自分の特定の場面では論理的に起こり得なくても、一般的にまだ失敗する可能性はあるわけです。
手動でコードを調査してErr
列挙子は存在しないと確認できたら、unwrap
を呼び出すことは完全に受容できることです。
こちらが例です:
#![allow(unused)] fn main() { use std::net::IpAddr; let home: IpAddr = "127.0.0.1".parse().unwrap(); }
ハードコードされた文字列を構文解析することでIpAddr
インスタンスを生成しています。
プログラマには127.0.0.1
が合法なIPアドレスであることがわかるので、ここでunwrap
を使用することは、
受容可能なことです。しかしながら、ハードコードされた合法な文字列が存在することは、
parse
メソッドの戻り値型を変えることにはなりません: それでも得られるのは、Result
値であり、
コンパイラはまだErr
列挙子になる可能性があるかのようにResult
を処理することを強制してきます。
コンパイラは、この文字列が常に合法なIPアドレスであると把握できるほど利口ではないからです。
プログラムにハードコードされるのではなく、IPアドレス文字列がユーザ起源でそれ故に確かに失敗する可能性がある場合、
Result
をもっと頑健な方法で処理したほうが絶対にいいでしょう。
エラー処理のガイドライン
コードが悪い状態に陥る可能性があるときにパニックさせるのは、推奨されることです。この文脈において、 悪い状態とは、何らかの前提、保証、契約、不変性が破られたことを言い、例を挙げれば、無効な値、 矛盾する値、行方不明な値がコードに渡されることと、さらに以下のいずれか一つ以上の状態であります:
- 悪い状態がときに起こるとは予想されないとき。
- この時点以降、この悪い状態にないことを頼りにコードが書かれているとき。
- 使用している型にこの情報をコード化するいい手段がないとき。
誰かが自分のコードを呼び出して筋の通らない値を渡してきたら、最善の選択肢はpanic!
し、
開発段階で修正できるように自分たちのコードにバグがあることをライブラリ使用者に通知することかもしれません。
同様に自分の制御下にない外部コードを呼び出し、修正しようのない無効な状態を返すときにpanic!
はしばしば適切です。
しかし、どんなにコードをうまく書いても起こると予想されますが、悪い状態に達したとき、それでもpanic!
呼び出しをするよりも、
Result
を返すほうがより適切です。例には、不正なデータを渡されたパーサとか、
訪問制限に引っかかったことを示唆するステータスを返すHTTPリクエストなどが挙げられます。
このような場合には、呼び出し側が問題の処理方法を決定できるようにResult
を返してこの悪い状態を委譲して、
失敗が予想される可能性であることを示唆するべきです。panic!
を呼び出すことは、
これらのケースでは最善策ではないでしょう。
コードが値に対して処理を行う場合、コードはまず値が合法であることを確認し、
値が合法でなければパニックするべきです。これはほぼ安全性上の理由によるものです: 不正なデータの処理を試みると、
コードを脆弱性に晒す可能性があります。これが、境界外へのメモリアクセスを試みたときに標準ライブラリがpanic!
を呼び出す主な理由です:
現在のデータ構造に属しないメモリにアクセスを試みることは、ありふれたセキュリティ問題なのです。
関数にはしばしば契約が伴います: 入力が特定の条件を満たすときのみ、振る舞いが保証されるのです。
契約が侵されたときにパニックすることは、道理が通っています。なぜなら、契約侵害は常に呼び出し側のバグを示唆し、
呼び出し側に明示的に処理してもらう必要のある種類のエラーではないからです。実際に、
呼び出し側が回復する合理的な手段はありません; 呼び出し側のプログラマがコードを修正する必要があるのです。
関数の契約は、特に侵害がパニックを引き起こす際には、関数のAPIドキュメント内で説明されているべきです。
ですが、全ての関数でたくさんのエラーチェックを行うことは冗長で煩わしいことでしょう。幸運にも、
Rustの型システム(故にコンパイラが行う型精査)を使用して多くの検査を行ってもらうことができます。
関数の引数に特定の型があるなら、合法な値があるとコンパイラがすでに確認していることを把握して、
コードのロジックに進むことができます。例えば、Option
以外の型がある場合、プログラムは、
何もないではなく何かあると想定します。そうしたらコードは、
Some
とNone
列挙子の2つの場合を処理する必要がなくなるわけです:
確実に値があるという可能性しかありません。関数に何もないことを渡そうとしてくるコードは、
コンパイルが通りもしませんので、その場合を実行時に検査する必要はないわけです。
別の例は、u32
のような符号なし整数を使うことであり、この場合、引数は負には絶対にならないことが確認されます。
検証のために独自の型を作る
Rustの型システムを使用して合法な値があると確認するというアイディアを一歩先に進め、 検証のために独自の型を作ることに目を向けましょう。第2章の数当てゲームで、 コードがユーザに1から100までの数字を推測するよう求めたことを思い出してください。 秘密の数字と照合する前にユーザの推測がそれらの値の範囲にあることを全く確認しませんでした; 推測が正であることしか確認しませんでした。この場合、結果はそれほど悲惨なものではありませんでした: 「大きすぎ」、「小さすぎ」という出力は、それでも正しかったでしょう。ユーザを合法な推測に導き、 ユーザが範囲外の数字を推測したり、例えばユーザが文字を代わりに入力したりしたときに別の挙動をするようにしたら、 有益な改善になるでしょう。
これをする一つの方法は、ただのu32
の代わりにi32
として推測をパースし、負の数になる可能性を許可し、
それから数字が範囲に収まっているというチェックを追加することでしょう。そう、以下のように:
loop {
// --snip--
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
}
このif
式が、値が範囲外かどうかをチェックし、ユーザに問題を告知し、continue
を呼び出してループの次の繰り返しを始め、
別の推測を求めます。if
式の後、guess
は1から100の範囲にあると把握して、guess
と秘密の数字の比較に進むことができます。
ところが、これは理想的な解決策ではありません: プログラムが1から100の範囲の値しか処理しないことが間違いなく、 肝要であり、この要求がある関数の数が多ければ、このようなチェックを全関数で行うことは、 面倒でパフォーマンスにも影響を及ぼす可能性があるでしょう。
代わりに、新しい型を作って検証を関数内に閉じ込め、検証を全箇所で繰り返すのではなく、
その型のインスタンスを生成することができます。そうすれば、関数がその新しい型をシグニチャに用い、
受け取った値を自信を持って使用することは安全になります。リスト9-9に、new
関数が1から100までの値を受け取った時のみ、
Guess
のインスタンスを生成するGuess
型を定義する一つの方法を示しました。
#![allow(unused)] fn main() { pub struct Guess { value: u32, } impl Guess { pub fn new(value: u32) -> Guess { if value < 1 || value > 100 { // 予想の値は1から100の範囲でなければなりませんが、{}でした panic!("Guess value must be between 1 and 100, got {}.", value); } Guess { value } } pub fn value(&self) -> u32 { self.value } } }
リスト9-9: 値が1から100の場合のみ処理を継続するGuess
型
まず、u32
型のvalue
をフィールドに持つGuess
という名前の構造体を定義しています。
ここに数値が保管されます。
それからGuess
にGuess
値のインスタンスを生成するnew
という名前の関連関数を実装しています。
new
関数は、u32
型のvalue
という引数を取り、Guess
を返すように定義されています。
new
関数の本体のコードは、value
をふるいにかけ、1から100の範囲であることを確かめます。
value
がふるいに引っかかったら、panic!
呼び出しを行います。これにより、呼び出しコードを書いているプログラマに、
修正すべきバグがあると警告します。というのも、この範囲外のvalue
でGuess
を生成することは、
Guess::new
が頼りにしている契約を侵害するからです。Guess::new
がパニックするかもしれない条件は、
公開されているAPIドキュメントで議論されるべきでしょう; あなたが作成するAPIドキュメントでpanic!
の可能性を示唆する、
ドキュメントの規約は、第14章で講義します。value
が確かにふるいを通ったら、
value
フィールドがvalue
引数にセットされた新しいGuess
を作成して返します。
次に、self
を借用し、他に引数はなく、u32
を返すvalue
というメソッドを実装します。
この類のメソッドは時にゲッターと呼ばれます。目的がフィールドから何らかのデータを得て返すことだからです。
この公開メソッドは、Guess
構造体のvalue
フィールドが非公開なので、必要になります。
value
フィールドが非公開なことは重要であり、そのためにGuess
構造体を使用するコードは、
直接value
をセットすることが叶わないのです: モジュール外のコードは、
Guess::new
関数を使用してGuess
のインスタンスを生成しなければならず、
それにより、Guess::new
関数の条件式でチェックされていないvalue
がGuess
に存在する手段はないことが保証されるわけです。
そうしたら、引数を一つ持つか、1から100の範囲の数値のみを返す関数は、シグニチャでu32
ではなく、
Guess
を取るか返し、本体内で追加の確認を行う必要はなくなると宣言できるでしょう。
まとめ
Rustのエラー処理機能は、プログラマがより頑健なコードを書く手助けをするように設計されています。
panic!
マクロは、プログラムが処理できない状態にあり、無効だったり不正な値で処理を継続するのではなく、
プロセスに処理を中止するよう指示することを通知します。Result
enumは、Rustの型システムを使用して、
コードが回復可能な方法で処理が失敗するかもしれないことを示唆します。Result
を使用して、
呼び出し側のコードに成功や失敗する可能性を処理する必要があることも教えます。
適切な場面でpanic!
やResult
を使用することで、必然的な問題の眼前でコードの信頼性を上げてくれます。
今や、標準ライブラリがOption
やResult
enumなどでジェネリクスを有効活用するところを目の当たりにしたので、
ジェネリクスの動作法と自分のコードでの使用方法について語りましょう。
ジェネリック型、トレイト、ライフタイム
全てのプログラミング言語には、概念の重複を効率的に扱う道具があります。Rustにおいて、そのような道具の一つがジェネリクスです。 ジェネリクスは、具体型や他のプロパティの抽象的な代役です。コード記述の際、コンパイルやコード実行時に、 ジェネリクスの位置に何が入るかを知ることなく、ジェネリクスの振る舞いや他のジェネリクスとの関係を表現できるのです。
関数が未知の値の引数を取り、同じコードを複数の具体的な値に対して走らせるように、
i32
やString
などの具体的な型の代わりに何かジェネリックな型の引数を取ることができます。
実際、第6章でOption<T>
、第8章でVec<T>
とHashMap<K, V>
、第9章でResult<T, E>
を既に使用しました。
この章では、独自の型、関数、メソッドをジェネリクスとともに定義する方法を探究します!
まず、関数を抽出して、コードの重複を減らす方法を確認しましょう。次に同じテクニックを活用して、 引数の型のみが異なる2つの関数からジェネリックな関数を生成します。また、 ジェネリックな型を構造体やenum定義で使用する方法も説明します。
それから、トレイトを使用して、ジェネリックな方法で振る舞いを定義する方法を学びます。 ジェネリックな型にトレイトを組み合わせることで、ジェネリックな型を、単にあらゆる型に対してではなく、特定の振る舞いのある型のみに制限できます。
最後に、ライフタイムを議論します。ライフタイムとは、コンパイラに参照がお互いにどう関係しているかの情報を与える一種のジェネリクスです。 ライフタイムのおかげでコンパイラに参照が有効であることを確認してもらうことを可能にしつつ、多くの場面で値を借用できます。
関数を抽出することで重複を取り除く
ジェネリクスの記法に飛び込む前にまずは、関数を抽出することでジェネリックな型が関わらない重複を取り除く方法を見ましょう。 そして、このテクニックを適用してジェネリックな関数を抽出するのです!重複したコードを認識して関数に抽出できるのと同じように、 ジェネリクスを使用できる重複コードも認識し始めるでしょう。
リスト10-1に示したように、リスト内の最大値を求める短いプログラムを考えてください。
ファイル名: src/main.rs
fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = number_list[0]; for number in number_list { if number > largest { largest = number; } } // 最大値は{}です println!("The largest number is {}", largest); assert_eq!(largest, 100); }
リスト10-1: 数字のリストから最大値を求めるコード
このコードは、整数のリストを変数number_list
に格納し、リストの最初の数字をlargest
という変数に配置しています。
それからリストの数字全部を走査し、現在の数字がlargest
に格納された数値よりも大きければ、
その変数の値を置き換えます。ですが、現在の数値が今まで見た最大値よりも小さければ、
変数は変わらず、コードはリストの次の数値に移っていきます。リストの数値全てを吟味した後、
largest
は最大値を保持しているはずで、今回は100になります。
2つの異なる数値のリストから最大値を発見するには、リスト10-1のコードを複製し、 プログラムの異なる2箇所で同じロジックを使用できます。リスト10-2のようにですね。
ファイル名: src/main.rs
fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = number_list[0]; for number in number_list { if number > largest { largest = number; } } println!("The largest number is {}", largest); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let mut largest = number_list[0]; for number in number_list { if number > largest { largest = number; } } println!("The largest number is {}", largest); }
リスト10-2: 2つの数値のリストから最大値を探すコード
このコードは動くものの、コードを複製することは退屈ですし、間違いも起きやすいです。また、 コードを変更したい時に複数箇所、更新しなければなりません。
この重複を排除するには、引数で与えられた整数のどんなリストに対しても処理が行える関数を定義して抽象化できます。 この解決策によりコードがより明確になり、リストの最大値を探すという概念を抽象的に表現させてくれます。
リスト10-3では、最大値を探すコードをlargest
という関数に抽出しました。リスト10-1のコードは、
たった1つの特定のリストからだけ最大値を探せますが、それとは異なり、このプログラムは2つの異なるリストから最大値を探せます。
ファイル名: src/main.rs
fn largest(list: &[i32]) -> i32 { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {}", result); assert_eq!(result, 100); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let result = largest(&number_list); println!("The largest number is {}", result); assert_eq!(result, 6000); }
リスト10-3: 2つのリストから最大値を探す抽象化されたコード
largest
関数にはlist
と呼ばれる引数があり、これは、関数に渡す可能性のある、あらゆるi32
値の具体的なスライスを示します。
結果的に、関数呼び出しの際、コードは渡した特定の値に対して走るのです。
まとめとして、こちらがリスト10-2のコードからリスト10-3に変更するのに要したステップです:
- 重複したコードを見分ける。
- 重複コードを関数本体に抽出し、コードの入力と戻り値を関数シグニチャで指定する。
- 重複したコードの2つの実体を代わりに関数を呼び出すように更新する。
次は、この同じ手順をジェネリクスでも踏んで異なる方法でコードの重複を減らします。
関数本体が特定の値ではなく抽象的なlist
に対して処理できたのと同様に、
ジェネリクスは抽象的な型に対して処理するコードを可能にしてくれます。
例えば、関数が2つあるとしましょう: 1つはi32
値のスライスから最大の要素を探し、1つはchar
値のスライスから最大要素を探します。
この重複はどう排除するのでしょうか?答えを見つけましょう!
ジェネリックなデータ型
関数シグニチャや構造体などの要素の定義を生成するのにジェネリクスを使用することができ、 それはさらに他の多くの具体的なデータ型と使用することもできます。まずは、 ジェネリクスで関数、構造体、enum、メソッドを定義する方法を見ましょう。それから、 ジェネリクスがコードのパフォーマンスに与える影響を議論します。
関数定義では
ジェネリクスを使用する関数を定義する時、通常、引数や戻り値のデータ型を指定する関数のシグニチャにジェネリクスを配置します。 そうすることでコードがより柔軟になり、コードの重複を阻止しつつ、関数の呼び出し元により多くの機能を提供します。
largest
関数を続けます。リスト10-4はどちらもスライスから最大値を探す2つの関数を示しています。
ファイル名: src/main.rs
fn largest_i32(list: &[i32]) -> i32 { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest } fn largest_char(list: &[char]) -> char { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest_i32(&number_list); println!("The largest number is {}", result); assert_eq!(result, 100); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest_char(&char_list); println!("The largest char is {}", result); assert_eq!(result, 'y'); }
リスト10-4: 名前とシグニチャの型のみが異なる2つの関数
largest_i32
関数は、リスト10-3で抽出したスライスから最大のi32
を探す関数です。
largest_char
関数は、スライスから最大のchar
を探します。関数本体には同じコードがあるので、
単独の関数にジェネリックな型引数を導入してこの重複を排除しましょう。
これから定義する新しい関数の型を引数にするには、ちょうど関数の値引数のように型引数に名前をつける必要があります。
型引数の名前にはどんな識別子も使用できますが、T
を使用します。というのも、慣習では、
Rustの引数名は短く(しばしばたった1文字になります)、Rustの型の命名規則がキャメルケースだからです。
"type"の省略形なので、T
が多くのRustプログラマの既定の選択なのです。
関数の本体で引数を使用するとき、コンパイラがその名前の意味を把握できるようにシグニチャでその引数名を宣言しなければなりません。
同様に、型引数名を関数シグニチャで使用する際には、使用する前に型引数名を宣言しなければなりません。
ジェネリックなlargest
関数を定義するために、型名宣言を山カッコ(<>
)内、関数名と引数リストの間に配置してください。
こんな感じに:
fn largest<T>(list: &[T]) -> T {
この定義は以下のように解読します: 関数largest
は、なんらかの型T
に関してジェネリックであると。
この関数にはlist
という引数が1つあり、これは型T
の値のスライスです。
largest
関数は同じT
型の値を返します。
リスト10-5は、シグニチャにジェネリックなデータ型を使用してlargest
関数定義を組み合わせたものを示しています。
このリストはさらに、この関数をi32
値かchar
値のどちらかで呼べる方法も表示しています。
このコードはまだコンパイルできないことに注意してください。ですが、この章の後ほど修正します。
ファイル名: src/main.rs
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
リスト10-5: ジェネリックな型引数を使用するものの、まだコンパイルできないlargest
関数の定義
直ちにこのコードをコンパイルしたら、以下のようなエラーが出ます:
error[E0369]: binary operation `>` cannot be applied to type `T`
(エラー: 2項演算`>`は、型`T`に適用できません)
--> src/main.rs:5:12
|
5 | if item > largest {
| ^^^^^^^^^^^^^^
|
= note: an implementation of `std::cmp::PartialOrd` might be missing for `T`
(注釈: `std::cmp::PartialOrd`の実装が`T`に対して存在しない可能性があります)
注釈がstd::cmp::PartialOrd
に触れています。これは、トレイトです。トレイトについては、次の節で語ります。
とりあえず、このエラーは、largest
の本体は、T
がなりうる全ての可能性のある型に対して動作しないと述べています。
本体で型T
の値を比較したいので、値が順序付け可能な型のみしか使用できないのです。比較を可能にするために、
標準ライブラリには型に実装できるstd::cmp::PartialOrd
トレイトがあります(このトレイトについて詳しくは付録Cを参照されたし)。
ジェネリックな型が特定のトレイトを持つと指定する方法は「トレイト境界」節で習うでしょうが、
先にジェネリックな型引数を使用する他の方法を探究しましょう。
構造体定義では
構造体を定義して<>
記法で1つ以上のフィールドにジェネリックな型引数を使用することもできます。
リスト10-6は、Point<T>
構造体を定義してあらゆる型のx
とy
座標を保持する方法を示しています。
ファイル名: src/main.rs
struct Point<T> { x: T, y: T, } fn main() { let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 }; }
リスト10-6: 型T
のx
とy
値を保持するPoint<T>
構造体
構造体定義でジェネリクスを使用する記法は、関数定義のものと似ています。まず、山カッコ内に型引数の名前を構造体名の直後に宣言します。 そうすると、本来具体的なデータ型を記述する構造体定義の箇所に、ジェネリックな型を使用できます。
ジェネリックな型を1つだけ使用してPoint<T>
を定義したので、この定義は、Point<T>
構造体がなんらかの型T
に関して、
ジェネリックであると述べていて、その型がなんであれ、x
とy
のフィールドは両方その同じ型になっていることに注意してください。
リスト10-7のように、異なる型の値のあるPoint<T>
のインスタンスを生成すれば、コードはコンパイルできません。
ファイル名: src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
リスト10-7: どちらも同じジェネリックなデータ型T
なので、x
とy
というフィールドは同じ型でなければならない
この例で、x
に整数値5を代入すると、このPoint<T>
のインスタンスに対するジェネリックな型T
は整数になるとコンパイラに知らせます。
それからy
に4.0を指定する時に、このフィールドはx
と同じ型と定義したはずなので、このように型不一致エラーが出ます:
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integral variable, found
floating-point variable
|
= note: expected type `{integer}`
found type `{float}`
x
とy
が両方ジェネリックだけれども、異なる型になり得るPoint
構造体を定義するには、
複数のジェネリックな型引数を使用できます。例えば、リスト10-8では、Point
の定義を変更して、
型T
とU
に関してジェネリックにし、x
が型T
で、y
が型U
になります。
ファイル名: src/main.rs
struct Point<T, U> { x: T, y: U, } fn main() { let both_integer = Point { x: 5, y: 10 }; let both_float = Point { x: 1.0, y: 4.0 }; let integer_and_float = Point { x: 5, y: 4.0 }; }
リスト10-8: Point<T, U>
は2つの型に関してジェネリックなので、x
とy
は異なる型の値になり得る
これで、示されたPoint
インスタンスは全部使用可能です!所望の数だけ定義でジェネリックな型引数を使用できますが、
数個以上使用すると、コードが読みづらくなります。コードで多くのジェネリックな型が必要な時は、
コードの小分けが必要なサインかもしれません。
enum定義では
構造体のように、列挙子にジェネリックなデータ型を保持するenumを定義することができます。
標準ライブラリが提供しているOption<T>
enumをもう一度見ましょう。このenumは第6章で使用しました:
#![allow(unused)] fn main() { enum Option<T> { Some(T), None, } }
この定義はもう、あなたにとってより道理が通っているはずです。ご覧の通り、Option<T>
は、
型T
に関してジェネリックで2つの列挙子のあるenumです: その列挙子は、型T
の値を保持するSome
と、
値を何も保持しないNone
です。Option<T>
enumを使用することで、オプショナルな値があるという抽象的な概念を表現でき、
Option<T>
はジェネリックなので、オプショナルな値の型に関わらず、この抽象を使用できます。
enumも複数のジェネリックな型を使用できます。第9章で使用したResult
enumの定義が一例です:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
Result
enumは2つの型T
、E
に関してジェネリックで、2つの列挙子があります: 型T
の値を保持するOk
と、
型E
の値を保持するErr
です。この定義により、Result
enumを、成功する(なんらかの型T
の値を返す)か、
失敗する(なんらかの型E
のエラーを返す)可能性のある処理がある、あらゆる箇所に使用するのが便利になります。
事実、ファイルを開くのに成功した時にT
に型std::fs::File
が入り、ファイルを開く際に問題があった時にE
に型std::io::Error
が入ったものが、
リスト9-3でファイルを開くのに使用したものです。
自分のコード内で、保持している値の型のみが異なる構造体やenum定義の場面を認識したら、 代わりにジェネリックな型を使用することで重複を避けることができます。
メソッド定義では
(第5章のように、)定義にジェネリックな型を使うメソッドを構造体やenumに実装することもできます。リスト10-9は、
リスト10-6で定義したPoint<T>
構造体にx
というメソッドを実装したものを示しています。
ファイル名: src/main.rs
struct Point<T> { x: T, y: T, } impl<T> Point<T> { fn x(&self) -> &T { &self.x } } fn main() { let p = Point { x: 5, y: 10 }; println!("p.x = {}", p.x()); }
リスト10-9: 型T
のx
フィールドへの参照を返すx
というメソッドをPoint<T>
構造体に実装する
ここで、フィールドx
のデータへの参照を返すx
というメソッドをPoint<T>
に定義しました。
impl
の直後にT
を宣言しなければならないことに注意してください。こうすることで、型Point<T>
にメソッドを実装していることを指定するために、T
を使用することができます。
impl
の後にT
をジェネリックな型として宣言することで、コンパイラは、Point
の山カッコ内の型が、
具体的な型ではなくジェネリックな型であることを認識できるのです。
例えば、ジェネリックな型を持つPoint<T>
インスタンスではなく、Point<f32>
だけにメソッドを実装することもできるでしょう。
リスト10-10では、具体的な型f32
を使用しています。つまり、impl
の後に型を宣言しません。
#![allow(unused)] fn main() { struct Point<T> { x: T, y: T, } impl Point<f32> { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() } } }
リスト10-10: ジェネリックな型引数T
に対して特定の具体的な型がある構造体にのみ適用されるimpl
ブロック
このコードは、Point<f32>
にはdistance_from_origin
というメソッドが存在するが、
T
がf32
ではないPoint<T>
の他のインスタンスにはこのメソッドが定義されないことを意味します。
このメソッドは、この点が座標(0.0, 0.0)の点からどれだけ離れているかを測定し、
浮動小数点数にのみ利用可能な数学的処理を使用します。
構造体定義のジェネリックな型引数は、必ずしもその構造体のメソッドシグニチャで使用するものと同じにはなりません。
例を挙げれば、リスト10-11は、リスト10-8のPoint<T, U>
にメソッドmixup
を定義しています。
このメソッドは、他のPoint
を引数として取り、この引数はmixup
を呼び出しているself
のPoint
とは異なる型の可能性があります。
このメソッドは、(型T
の)self
のPoint
のx
値と渡した(型W
の)Point
のy
値から新しいPoint
インスタンスを生成します。
ファイル名: src/main.rs
struct Point<T, U> { x: T, y: U, } impl<T, U> Point<T, U> { fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> { Point { x: self.x, y: other.y, } } } fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: "Hello", y: 'c'}; let p3 = p1.mixup(p2); println!("p3.x = {}, p3.y = {}", p3.x, p3.y); }
リスト10-11: 構造体定義とは異なるジェネリックな型を使用するメソッド
main
で、x
(値は5
)にi32
、y
(値は10.4
)にf64
を持つPoint
を定義しました。p2
変数は、
x
(値は"Hello"
)に文字列スライス、y
(値はc
)にchar
を持つPoint
構造体です。
引数p2
でp1
にmixup
を呼び出すと、p3
が得られ、x
はi32
になります。x
はp1
由来だからです。
p3
変数のy
は、char
になります。y
はp2
由来だからです。println!
マクロの呼び出しは、
p3.x = 5, p3.y = c
と出力するでしょう。
この例の目的は、一部のジェネリックな引数はimpl
で宣言され、他の一部はメソッド定義で宣言される場面をデモすることです。
ここで、ジェネリックな引数T
とU
はimpl
の後に宣言されています。構造体定義にはまるからです。
ジェネリックな引数V
とW
はfn mixup
の後に宣言されています。何故なら、このメソッドにしか関係ないからです。
ジェネリクスを使用したコードのパフォーマンス
ジェネリックな型引数を使用すると、実行時にコストが発生するのかな、と思うかもしれません。 嬉しいことにRustでは、ジェネリクスを、具体的な型があるコードよりもジェネリックな型を使用したコードを実行するのが遅くならないように実装しています。
コンパイラはこれを、ジェネリクスを使用しているコードの単相化をコンパイル時に行うことで達成しています。 単相化(monomorphization)は、コンパイル時に使用されている具体的な型を入れることで、 ジェネリックなコードを特定のコードに変換する過程のことです。
この過程において、コンパイラは、リスト10-5でジェネリックな関数を生成するために使用した手順と真逆のことをしています: コンパイラは、ジェネリックなコードが呼び出されている箇所全部を見て、 ジェネリックなコードが呼び出されている具体的な型のコードを生成するのです。
標準ライブラリのOption<T>
enumを使用する例でこれが動作する方法を見ましょう:
#![allow(unused)] fn main() { let integer = Some(5); let float = Some(5.0); }
コンパイラがこのコードをコンパイルすると、単相化を行います。その過程で、コンパイラはOption<T>
のインスタンスに使用された値を読み取り、
2種類のOption<T>
を識別します: 一方はi32
で、もう片方はf64
です。そのように、
コンパイラは、Option<T>
のジェネリックな定義をOption_i32
とOption_f64
に展開し、
それにより、ジェネリックな定義を特定の定義と置き換えます。
単相化されたバージョンのコードは、以下のようになります。ジェネリックなOption<T>
が、
コンパイラが生成した特定の定義に置き換えられています:
ファイル名: src/main.rs
enum Option_i32 { Some(i32), None, } enum Option_f64 { Some(f64), None, } fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0); }
Rustでは、ジェネリックなコードを各インスタンスで型を指定したコードにコンパイルするので、 ジェネリクスを使用することに対して実行時コストを払うことはありません。コードを実行すると、 それぞれの定義を手作業で複製した時のように振る舞います。単相化の過程により、 Rustのジェネリクスは実行時に究極的に効率的になるのです。
トレイト: 共通の振る舞いを定義する
トレイトは、Rustコンパイラに、特定の型に存在し、他の型と共有できる機能について知らせます。 トレイトを使用すると、共通の振る舞いを抽象的に定義できます。トレイト境界を使用すると、 あるジェネリックが、特定の振る舞いをもつあらゆる型になり得ることを指定できます。
注釈: 違いはあるものの、トレイトは他の言語でよくインターフェイスと呼ばれる機能に類似しています。
トレイトを定義する
型の振る舞いは、その型に対して呼び出せるメソッドから構成されます。異なる型は、それらの型全てに対して同じメソッドを呼び出せるなら、 同じ振る舞いを共有することになります。トレイト定義は、メソッドシグニチャをあるグループにまとめ、なんらかの目的を達成するのに必要な一連の振る舞いを定義する手段です。
例えば、いろんな種類や量のテキストを保持する複数の構造体があるとしましょう: 特定の場所から送られる新しいニュースを保持するNewsArticle
と、
新規ツイートか、リツイートか、はたまた他のツイートへのリプライなのかを示すメタデータを伴う最大で280文字までのTweet
です。
NewsArticle
または Tweet
インスタンスに保存されているデータのサマリーを表示できるメディア アグリゲータ ライブラリを作成します。
これをするには、各型のサマリーが必要で、インスタンスで summarize
メソッドを呼び出してサマリーを要求する必要があります。
リスト10-12は、この振る舞いを表現するSummary
トレイトの定義を表示しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub trait Summary { fn summarize(&self) -> String; } }
リスト10-12: summarize
メソッドで提供される振る舞いからなるSummary
トレイト
ここでは、trait
キーワード、それからトレイト名を使用してトレイトを定義していて、その名前は今回の場合、
Summary
です。波括弧の中にこのトレイトを実装する型の振る舞いを記述するメソッドシグニチャを定義し、
今回の場合は、fn summarize(&self) -> String
です。
メソッドシグニチャの後に、波括弧内に実装を提供する代わりに、セミコロンを使用しています。
このトレイトを実装する型はそれぞれ、メソッドの本体に独自の振る舞いを提供しなければなりません。
コンパイラにより、Summary
トレイトを保持するあらゆる型に、このシグニチャと全く同じメソッドsummarize
が定義されていることが
強制されます。
トレイトには、本体に複数のメソッドを含むことができます: メソッドシグニチャは行ごとに並べられ、 各行はセミコロンで終わります。
トレイトを型に実装する
今や Summary
トレイトを使用して目的の動作を定義できたので、メディア アグリゲータでこれを型に実装できます。
リスト10-13は、 Summary
トレイトを NewsArticle
構造体上に実装したもので、ヘッドライン、著者、そして地域情報を使ってsummarize
の戻り値を作っています。
Tweet
構造体に関しては、ツイートの内容が既に280文字に制限されていると仮定して、ユーザー名の後にツイートのテキスト全体が続くものとして summarize
を定義します。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub trait Summary { fn summarize(&self) -> String; } pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String, } impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) } } pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } } }
リスト10-13: Summary
トレイトをNewsArticle
とTweet
型に実装する
型にトレイトを実装することは、普通のメソッドを実装することに似ています。違いは、impl
の後に、
実装したいトレイトの名前を置き、それからfor
キーワード、さらにトレイトの実装対象の型の名前を指定することです。
impl
ブロック内に、トレイト定義で定義したメソッドシグニチャを置きます。各シグニチャの後にセミコロンを追記するのではなく、
波括弧を使用し、メソッド本体に特定の型のトレイトのメソッドに欲しい特定の振る舞いを入れます。
トレイトを実装後、普通のメソッド同様にNewsArticle
やTweet
のインスタンスに対してこのメソッドを呼び出せます。
こんな感じで:
use chapter10::{self, Summary, Tweet};
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
// もちろん、ご存知かもしれませんがね、みなさん
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
}
このコードは、1 new tweet: horse_ebooks: of course, as you probably already know, people
と出力します。
リスト10-13でSummary
トレイトとNewArticle
、Tweet
型を同じlib.rsに定義したので、
全部同じスコープにあることに注目してください。このlib.rsをaggregator
と呼ばれるクレート専用にして、
誰か他の人が私たちのクレートの機能を活用して自分のライブラリのスコープに定義された構造体にSummary
トレイトを実装したいとしましょう。
まず、トレイトをスコープに取り込む必要があるでしょう。use aggregator::Summary;
と指定してそれを行えば、
これにより、自分の型にSummary
を実装することが可能になるでしょう。Summary
トレイトは、
他のクレートが実装するためには、公開トレイトである必要があり、ここでは、リスト10-12のtrait
の前に、
pub
キーワードを置いたのでそうなっています。
トレイト実装で注意すべき制限の1つは、トレイトか対象の型が自分のクレートに固有(local)である時のみ、
型に対してトレイトを実装できるということです。例えば、Display
のような標準ライブラリのトレイトをaggregator
クレートの機能の一部として、
Tweet
のような独自の型に実装できます。型Tweet
がaggregator
クレートに固有だからです。
また、Summary
をaggregator
クレートでVec<T>
に対して実装することもできます。
トレイトSummary
は、aggregator
クレートに固有だからです。
しかし、外部のトレイトを外部の型に対して実装することはできません。例として、
aggregator
クレート内でVec<T>
に対してDisplay
トレイトを実装することはできません。
Display
とVec<T>
は標準ライブラリで定義され、aggregator
クレートに固有ではないからです。
この制限は、コヒーレンス(coherence)、特に孤児のルール(orphan rule)と呼ばれるプログラムの特性の一部で、
親の型が存在しないためにそう命名されました。この規則により、他の人のコードが自分のコードを壊したり、
その逆が起きないことを保証してくれます。この規則がなければ、2つのクレートが同じ型に対して同じトレイトを実装できてしまい、
コンパイラはどちらの実装を使うべきかわからなくなってしまうでしょう。
デフォルト実装
時として、全ての型の全メソッドに対して実装を要求するのではなく、トレイトの全てあるいは一部のメソッドに対してデフォルトの振る舞いがあると有用です。 そうすれば、特定の型にトレイトを実装する際、各メソッドのデフォルト実装を保持するかオーバーライドするか選べるわけです。
リスト10-14は、リスト10-12のように、メソッドシグニチャだけを定義するのではなく、
Summary
トレイトのsummarize
メソッドにデフォルトの文字列を指定する方法を示しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub trait Summary { fn summarize(&self) -> String { // "(もっと読む)" String::from("(Read more...)") } } pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String, } impl Summary for NewsArticle {} pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) } } }
リスト10-14: summarize
メソッドのデフォルト実装があるSummary
トレイトの定義
独自の実装を定義するのではなく、デフォルト実装を利用してNewsArticle
のインスタンスをまとめるには、
impl Summary for NewsArticle {}
と空のimpl
ブロックを指定します。
もはやNewsArticle
に直接summarize
メソッドを定義してはいませんが、私達はデフォルト実装を提供しており、
NewsArticle
はSummary
トレイトを実装すると指定しました。そのため、
NewsArticle
のインスタンスに対してsummarize
メソッドを同じように呼び出すことができます。
このように:
use chapter10::{self, NewsArticle, Summary};
fn main() {
let article = NewsArticle {
// ペンギンチームがスタンレーカップチャンピオンシップを勝ち取る!
headline: String::from("Penguins win the Stanley Cup Championship!"),
// アメリカ、ペンシルベニア州、ピッツバーグ
location: String::from("Pittsburgh, PA, USA"),
// アイスバーグ
author: String::from("Iceburgh"),
// ピッツバーグ・ペンギンが再度NHL(National Hockey League)で最強のホッケーチームになった
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};
println!("New article available! {}", article.summarize());
}
このコードは、New article available! (Read more...)
(新しい記事があります!(もっと読む)
)と出力します。
summarize
にデフォルト実装を用意しても、リスト10-13のTweet
のSummary
実装を変える必要はありません。
理由は、デフォルト実装をオーバーライドする記法はデフォルト実装のないトレイトメソッドを実装する記法と同じだからです。
デフォルト実装は、自らのトレイトのデフォルト実装を持たない他のメソッドを呼び出すことができます。
このようにすれば、トレイトは多くの有用な機能を提供しつつ、実装者は僅かな部分しか指定しなくて済むようになります。
例えば、Summary
トレイトを、(実装者が)内容を実装しなければならないsummarize_author
メソッドを持つように定義し、
それからsummarize_author
メソッドを呼び出すデフォルト実装を持つsummarize
メソッドを定義することもできます:
#![allow(unused)] fn main() { pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { // "({}さんの文章をもっと読む)" format!("(Read more from {}...)", self.summarize_author()) } } pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool, } impl Summary for Tweet { fn summarize_author(&self) -> String { format!("@{}", self.username) } } }
このバージョンのSummary
を使用するために、型にトレイトを実装する際、実装する必要があるのはsummarize_author
だけです:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
// "({}さんの文章をもっと読む)"
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
summarize_author
定義後、Tweet
構造体のインスタンスに対してsummarize
を呼び出せ、
summarize
のデフォルト実装は、私達が提供したsummarize_author
の定義を呼び出すでしょう。
summarize_author
を実装したので、追加のコードを書く必要なく、Summary
トレイトは、
summarize
メソッドの振る舞いを与えてくれました。
use chapter10::{self, Summary, Tweet};
fn main() {
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
}
このコードは、1 new tweet: (Read more from @horse_ebooks...)
(1つの新しいツイート:(@horse_ebooksさんの文章をもっと読む)
)と出力します。
デフォルト実装を、そのメソッドをオーバーライドしている実装から呼び出すことはできないことに注意してください。
引数としてのトレイト
トレイトを定義し実装する方法はわかったので、トレイトを使っていろんな種類の型を受け付ける関数を定義する方法を学んでいきましょう。
たとえば、Listing 10-13では、NewsArticle
とTweet
型にSummary
トレイトを実装しました。
ここで、引数のitem
のsummarize
メソッドを呼ぶ関数notify
を定義することができます。ただし、引数item
はSummary
トレイトを実装しているような何らかの型であるとします。
このようなことをするためには、impl Trait
構文を使うことができます。
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
引数のitem
には、具体的な型の代わりに、impl
キーワードとトレイト名を指定します。
この引数は、指定されたトレイトを実装しているあらゆる型を受け付けます。
notify
の中身では、summarize
のような、Summary
トレイトに由来するitem
のあらゆるメソッドを呼び出すことができます。
私達は、notify
を呼びだし、NewsArticle
かTweet
のどんなインスタンスでも渡すことができます。
この関数を呼び出すときに、String
やi32
のような他の型を渡すようなコードはコンパイルできません。
なぜなら、これらの型はSummary
を実装していないからです。
トレイト境界構文
impl Trait
構文は単純なケースを解決しますが、実はより長いトレイト境界 (trait bound) と呼ばれる姿の糖衣構文 (syntax sugar) なのです。
それは以下のようなものです:
pub fn notify<T: Summary>(item: &T) {
// 速報! {}
println!("Breaking news! {}", item.summarize());
}
この「より長い」姿は前節の例と等価ですが、より冗長です。 山カッコの中にジェネリックな型引数の宣言を書き、型引数の後ろにコロンを挟んでトレイト境界を置いています。
簡単なケースに対し、impl Trait
構文は便利で、コードを簡潔にしてくれます。
そうでないケースの場合、トレイト境界構文を使えば複雑な状態を表現できます。
たとえば、Summary
を実装する2つのパラメータを持つような関数を考えることができます。
impl Trait
構文を使うとこのようになるでしょう:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
この関数が受け取るitem1
とitem2
の型が(どちらもSummary
を実装する限り)異なっても良いとするならば、impl Trait
は適切でしょう。
両方の引数が同じ型であることを強制することは、以下のようにトレイト境界を使ってのみ表現可能です:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
引数であるitem1
とitem2
の型としてジェネリックな型T
を指定しました。
これにより、item1
とitem2
として関数に渡される値の具体的な型が同一でなければならない、という制約を与えています。
複数のトレイト境界を+
構文で指定する
複数のトレイト境界も指定できます。
たとえば、notify
にsummarize
メソッドに加えてitem
の画面出力形式(ディスプレイフォーマット)を使わせたいとします。
その場合は、notify
の定義にitem
はDisplay
とSummary
の両方を実装していなくてはならないと指定することになります。
これは、以下のように+
構文で行うことができます:
pub fn notify(item: &(impl Summary + Display)) {
+
構文はジェネリック型につけたトレイト境界に対しても使えます:
pub fn notify<T: Summary + Display>(item: &T) {
これら2つのトレイト境界が指定されていれば、notify
の中ではsummarize
を呼び出すことと、{}
を使ってitem
をフォーマットすることの両方が行なえます。
where
句を使ったより明確なトレイト境界
あまりたくさんのトレイト境界を使うことには欠点もあります。
それぞれのジェネリック(な型)がそれぞれのトレイト境界をもつので、複数のジェネリック型の引数をもつ関数は、関数名と引数リストの間に大量のトレイト境界に関する情報を含むことがあります。
これでは関数のシグネチャが読みにくくなってしまいます。
このため、Rustはトレイト境界を関数シグネチャの後のwhere
句の中で指定するという別の構文を用意しています。
なので、このように書く:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
代わりに、where
句を使い、このように書くことができます:
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
この関数シグニチャは、よりさっぱりとしています。トレイト境界を多く持たない関数と同じように、関数名、引数リスト、戻り値の型が一緒になって近くにあるからですね。
トレイトを実装している型を返す
以下のように、impl Trait
構文を戻り値型のところで使うことにより、あるトレイトを実装する何らかの型を返すことができます。
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
戻り値の型としてimpl Summary
を使うことにより、具体的な型が何かを言うことなく、returns_summarizable
関数はSummary
トレイトを実装している何らかの型を返すのだ、と指定することができます。
今回returns_summarizable
はTweet
を返しますが、この関数を呼び出すコードはそのことを知りません。
実装しているトレイトだけで戻り値型を指定できることは、13章で学ぶ、クロージャとイテレータを扱うときに特に便利です。
クロージャとイテレータの作り出す型は、コンパイラだけが知っているものであったり、指定するには長すぎるものであったりします。
impl Trait
構文を使えば、非常に長い型を書くことなく、ある関数はIterator
トレイトを実装するある型を返すのだ、と簡潔に指定することができます。
ただし、impl Trait
は一種類の型を返す場合にのみ使えます。
たとえば、以下のように、戻り値の型はimpl Summary
で指定しつつ、NewsArticle
かTweet
を返すようなコードは失敗します:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"Penguins win the Stanley Cup Championship!",
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
} else {
Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
}
}
}
NewsArticle
かTweet
を返すというのは、コンパイラのimpl Trait
構文の実装まわりの制約により許されていません。
このような振る舞いをする関数を書く方法は、17章のトレイトオブジェクトで異なる型の値を許容する節で学びます。
トレイト境界でlargest
関数を修正する
ジェネリックな型引数の境界で使用したい振る舞いを指定する方法がわかったので、リスト10-5に戻って、
ジェネリックな型引数を使用するlargest
関数の定義を修正しましょう!最後にそのコードを実行しようとした時、
こんなエラーが出ていました:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- T
| |
| T
|
= note: `T` might need a bound for `std::cmp::PartialOrd`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
largest
の本体で、大なり演算子(>
)を使用して型T
の2つの値を比較しようとしていました。この演算子は、
標準ライブラリトレイトのstd::cmp::PartialOrd
でデフォルトメソッドとして定義されているので、
largest
関数が、比較できるあらゆる型のスライスに対して動くようにするためには、T
のトレイト境界にPartialOrd
を指定する必要があります。
PartialOrd
はpreludeに含まれているので、これをスコープに導入する必要はありません。
largest
のシグニチャを以下のように変えてください:
fn largest<T: PartialOrd>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {}", result);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {}", result);
}
今回のコンパイルでは、別のエラーが出てきます:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0508]: cannot move out of type `[T]`, a non-copy slice
(エラー[E0508]: 型`[T]`をもつ、非コピーのスライスからのムーブはできません)
--> src/main.rs:2:23
|
2 | let mut largest = list[0];
| ^^^^^^^
| |
| cannot move out of here
| (ここからムーブすることはできません)
| move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait
| (ムーブが発生するのは、`list[_]`は`T`という、`Copy`トレイトを実装しない型であるためです)
| help: consider borrowing here: `&list[0]`
| (助言:借用するようにしてみてはいかがですか: `&list[0]`)
error[E0507]: cannot move out of a shared reference
(エラー[E0507]:共有の参照からムーブはできません)
--> src/main.rs:4:18
|
4 | for &item in list {
| ----- ^^^^
| ||
| |data moved here
| |(データがここでムーブされています)
| |move occurs because `item` has type `T`, which does not implement the `Copy` trait
| |(ムーブが発生するのは、`item`は`T`という、`Copy`トレイトを実装しない型であるためです)
| help: consider removing the `&`: `item`
| (助言:`&`を取り除いてみてはいかがですか: `item`)
error: aborting due to 2 previous errors
Some errors have detailed explanations: E0507, E0508.
For more information about an error, try `rustc --explain E0507`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
このエラーの鍵となる行は、cannot move out of type [T], a non-copy slice
です。
ジェネリックでないバージョンのlargest
関数では、最大のi32
かchar
を探そうとするだけでした。
第4章のスタックのみのデータ: コピー節で議論したように、i32
やchar
のようなサイズが既知の型は
スタックに格納できるので、Copy
トレイトを実装しています。しかし、largest
関数をジェネリックにすると、
list
引数がCopy
トレイトを実装しない型を含む可能性も出てきたのです。結果として、
list[0]
から値をlargest
にムーブできず、このエラーに陥ったのです。
このコードをCopy
トレイトを実装する型だけを使って呼び出すようにしたいなら、T
のトレイト境界にCopy
を追加すればよいです!
リスト10-15は、関数に渡したスライスの値の型が、i32
やchar
などのようにPartialOrd
とCopy
を実装する限りコンパイルできる、ジェネリックなlargest
関数の完全なコードを示しています。
ファイル名: src/main.rs
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T { let mut largest = list[0]; for &item in list { if item > largest { largest = item; } } largest } fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!("The largest number is {}", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest(&char_list); println!("The largest char is {}", result); }
リスト10-15: PartialOrd
とCopy
トレイトを実装するあらゆるジェネリックな型に対して動く、
largest
関数の実際の定義
もしlargest
関数をCopy
を実装する型だけに制限したくなかったら、T
がCopy
ではなくClone
というトレイト境界を持つと指定することもできます。そうしたら、
largest
関数に所有権が欲しい時にスライスの各値をクローンできます。clone
関数を使用するということは、
String
のようなヒープデータを持つ型の場合により多くのヒープ確保が発生する可能性があることを意味します。
そして、大量のデータを取り扱っていたら、ヒープ確保には時間がかかることもあります。
largest
の別の実装方法は、関数がスライスのT
値への参照を返すようにすることです。
戻り値の型をT
ではなく&T
に変え、それにより関数の本体を参照を返すように変更したら、
Clone
やCopy
トレイト境界は必要なくなり、ヒープ確保も避けられるでしょう。
これらの代替策をご自身で実装してみましょう!
トレイト境界を使用して、メソッド実装を条件分けする
ジェネリックな型引数を持つimpl
ブロックにトレイト境界を与えることで、
特定のトレイトを実装する型に対するメソッド実装を条件分けできます。例えば、
リスト10-16の型Pair<T>
は、常にnew
関数を実装します。しかし、Pair<T>
は、
内部の型T
が比較を可能にするPartialOrd
トレイトと出力を可能にするDisplay
トレイトを実装している時のみ、
cmp_display
メソッドを実装します。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::fmt::Display; struct Pair<T> { x: T, y: T, } impl<T> Pair<T> { fn new(x: T, y: T) -> Self { Self { x, y } } } impl<T: Display + PartialOrd> Pair<T> { fn cmp_display(&self) { if self.x >= self.y { println!("The largest member is x = {}", self.x); } else { println!("The largest member is y = {}", self.y); } } } }
リスト10-16: トレイト境界によってジェネリックな型に対するメソッド実装を条件分けする
また、別のトレイトを実装するあらゆる型に対するトレイト実装を条件分けすることもできます。
トレイト境界を満たすあらゆる型にトレイトを実装することは、ブランケット実装(blanket implementation)と呼ばれ、
Rustの標準ライブラリで広く使用されています。例を挙げれば、標準ライブラリは、
Display
トレイトを実装するあらゆる型にToString
トレイトを実装しています。
標準ライブラリのimpl
ブロックは以下のような見た目です:
impl<T: Display> ToString for T {
// --snip--
}
標準ライブラリにはこのブランケット実装があるので、Display
トレイトを実装する任意の型に対して、
ToString
トレイトで定義されたto_string
メソッドを呼び出せるのです。
例えば、整数はDisplay
を実装するので、このように整数値を対応するString
値に変換できます:
#![allow(unused)] fn main() { let s = 3.to_string(); }
ブランケット実装は、トレイトのドキュメンテーションの「実装したもの」節に出現します。
トレイトとトレイト境界により、ジェネリックな型引数を使用して重複を減らしつつ、コンパイラに対して、 そのジェネリックな型に特定の振る舞いが欲しいことを指定するコードを書くことができます。 それからコンパイラは、トレイト境界の情報を活用してコードに使用された具体的な型が正しい振る舞いを提供しているか確認できます。 動的型付き言語では、その型に定義されていないメソッドを呼び出せば、実行時 (runtime) にエラーが出るでしょう。 しかし、Rustはこの種のエラーをコンパイル時に移したので、コードが動かせるようになる以前に問題を修正することを強制されるのです。 加えて、コンパイル時に既に確認したので、実行時の振る舞いを確認するコードを書かなくても済みます。 そうすることで、ジェネリクスの柔軟性を諦めることなくパフォーマンスを向上させます。
すでに使っている他のジェネリクスに、ライフタイムと呼ばれるものがあります。 ライフタイムは、型が欲しい振る舞いを保持していることではなく、必要な間だけ参照が有効であることを保証します。 ライフタイムがどうやってそれを行うかを見てみましょう。
ライフタイムで参照を検証する
第4章の「参照と借用」節で議論しなかった詳細の一つに、Rustにおいて参照は全てライフタイムを保持するということがあります。 ライフタイムとは、その参照が有効になるスコープのことです。多くの場合、型が推論されるように、 大体の場合、ライフタイムも暗黙的に推論されます。複数の型の可能性があるときには、型を注釈しなければなりません。 同様に、参照のライフタイムがいくつか異なる方法で関係することがある場合には注釈しなければなりません。 コンパイラは、ジェネリックライフタイム引数を使用して関係を注釈し、実行時に実際の参照が確かに有効であることを保証することを要求するのです。
ライフタイムの概念は、他のプログラミング言語の道具とはどこか異なり、間違いなくRustで一番際立った機能になっています。 この章では、ライフタイムの全体を解説することはしませんが、 ライフタイム記法が必要となる最も一般的な場合について議論しますので、ライフタイムの概念について馴染むことができるでしょう。
ライフタイムでダングリング参照を回避する
ライフタイムの主な目的は、ダングリング参照を回避することです。ダングリング参照によりプログラムは、 参照するつもりだったデータ以外のデータを参照してしまいます。リスト10-17のプログラムを考えてください。 これには、外側のスコープと内側のスコープが含まれています。
fn main() {
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
}
リスト10-17: 値がスコープを抜けてしまった参照を使用しようとする
注釈: リスト10-17や10-18、10-24では、変数に初期値を与えずに宣言しているので、変数名は外側のスコープに存在します。 初見では、これはRustにはnull値が存在しないということと衝突しているように見えるかもしれません。 しかしながら、値を与える前に変数を使用しようとすれば、コンパイルエラーになり、 確かにRustではnull値は許可されていないことがわかります。
外側のスコープで初期値なしのr
という変数を宣言し、内側のスコープで初期値5のx
という変数を宣言しています。
内側のスコープ内で、r
の値をx
への参照にセットしようとしています。それから内側のスコープが終わり、
r
の値を出力しようとしています。r
が参照している値が使おうとする前にスコープを抜けるので、
このコードはコンパイルできません。こちらがエラーメッセージです:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
(エラー[E0597]: `x`の生存期間が短すぎます)
--> src/main.rs:7:17
|
7 | r = &x;
| ^^ borrowed value does not live long enough
| (借用された値の生存期間が短すぎます)
8 | }
| - `x` dropped here while still borrowed
| (`x`は借用されている間にここでドロップされました)
9 |
10 | println!("r: {}", r);
| - borrow later used here
| (その後、借用はここで使われています)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
変数x
の「生存期間が短すぎます」。原因は、内側のスコープが7行目で終わった時点でx
がスコープを抜けるからです。
ですが、r
はまだ、外側のスコープに対して有効です; スコープが大きいので、「長生きする」と言います。
Rustで、このコードが動くことを許可していたら、r
はx
がスコープを抜けた時に解放されるメモリを参照していることになり、
r
で行おうとするいかなることもちゃんと動作しないでしょう。では、どうやってコンパイラはこのコードが無効であると決定しているのでしょうか?
それは、借用チェッカーを使用しているのです。
借用精査機
Rustコンパイラには、スコープを比較して全ての借用が有効であるかを決定する借用チェッカーがあります。 リスト10-18は、リスト10-17と同じコードを示していますが、変数のライフタイムを表示する注釈が付いています。
fn main() {
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
}
リスト10-18: それぞれ'a
と'b
と名付けられたr
とx
のライフタイムの注釈
ここで、r
のライフタイムは'a
、x
のライフタイムは'b
で注釈しました。ご覧の通り、
内側の'b
ブロックの方が、外側の'a
ライフタイムブロックよりはるかに小さいです。
コンパイル時に、コンパイラは2つのライフタイムのサイズを比較し、r
は'a
のライフタイムだけれども、
'b
のライフタイムのメモリを参照していると確認します。'b
は'a
よりも短いので、プログラムは拒否されます:
参照の対象が参照ほど長生きしないのです。
リスト10-19でコードを修正したので、ダングリング参照はなくなり、エラーなくコンパイルできます。
fn main() { { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {}", r); // | | // --+ | } // ----------+ }
リスト10-19: データのライフタイムが参照より長いので、有効な参照
ここでx
のライフタイムは'b
になり、今回の場合'a
よりも大きいです。つまり、
コンパイラはx
が有効な間、r
の参照も常に有効になることを把握しているので、r
はx
を参照できます。
今や、参照のライフタイムがどれだけであるかと、コンパイラがライフタイムを解析して参照が常に有効であることを保証する仕組みがわかったので、 関数における引数と戻り値のジェネリックなライフタイムを探究しましょう。
関数のジェネリックなライフタイム
2つの文字列スライスのうち、長い方を返す関数を書きましょう。この関数は、
2つの文字列スライスを取り、1つの文字列スライスを返します。longest
関数の実装完了後、
リスト10-20のコードは、The longest string is abcd
と出力するはずです。
ファイル名: src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
// 最長の文字列は、{}です
println!("The longest string is {}", result);
}
リスト10-20: longest
関数を呼び出して2つの文字列スライスのうち長い方を探すmain
関数
関数に取ってほしい引数が文字列スライス、つまり参照であることに注意してください。
何故なら、longest
関数に引数の所有権を奪ってほしくないからです。
リスト10-20で使用している引数が、我々が必要としているものである理由についてもっと詳しい議論は、
第4章の「引数としての文字列スライス」節をご参照ください。
リスト10-21に示すようにlongest
関数を実装しようとしたら、コンパイルできないでしょう。
ファイル名: src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
リスト10-21: 2つの文字列スライスのうち長い方を返すけれども、コンパイルできないlongest
関数の実装
代わりに、以下のようなライフタイムに言及するエラーが出ます:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
(エラー[E0106]: ライフタイム指定子が不足しています)
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ^ expected lifetime parameter
| (ライフタイム引数があるべきです)
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
(助言: この関数の戻り値型は借用された値を含んでいますが、
シグニチャは、それが`x`と`y`どちらから借用されたものなのか宣言していません)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
助言テキストが、戻り値の型はジェネリックなライフタイム引数である必要があると明かしています。
というのも、返している参照がx
かy
のどちらを参照しているか、コンパイラにはわからないからです。
実際のところ、この関数の本体のif
ブロックはx
への参照を返し、else
ブロックはy
への参照を返すので、
どちらなのか私たちにもわかりません!
この関数を定義する際、この関数に渡される具体的な値がわからないので、if
ケースとelse
ケースのどちらが実行されるかわからないのです。
また、リスト10-18と10-19で、返す参照が常に有効であるかを決定したときのようにスコープを見ることも、渡される参照の具体的なライフタイムがわからないのでできないのです。
借用チェッカーもこれを決定することはできません。x
とy
のライフタイムがどう戻り値のライフタイムと関係するかわからないからです。
このエラーを修正するために、借用チェッカーが解析を実行できるように、参照間の関係を定義するジェネリックなライフタイム引数を追加しましょう。
ライフタイム注釈記法
ライフタイム注釈は、いかなる参照の生存期間も変えることはありません。シグニチャにジェネリックな型引数を指定された 関数が、あらゆる型を受け取ることができるのと同様に、ジェネリックなライフタイム引数を指定された関数は、 あらゆるライフタイムの参照を受け取ることができます。ライフタイム注釈は、ライフタイムに影響することなく、 複数の参照のライフタイムのお互いの関係を記述します。
ライフタイム注釈は、少し不自然な記法です: ライフタイム引数の名前はアポストロフィー('
)で始まらなければならず、
通常全部小文字で、ジェネリック型のようにとても短いです。多くの人は、'a
という名前を使います。
ライフタイム引数注釈は、参照の&
の後に配置し、注釈と参照の型を区別するために空白を1つ使用します。
例を挙げましょう: ライフタイム引数なしのi32
への参照、'a
というライフタイム引数付きのi32
への参照、
そして同じくライフタイム'a
を持つi32
への可変参照です。
&i32 // a reference
// (ただの)参照
&'a i32 // a reference with an explicit lifetime
// 明示的なライフタイム付きの参照
&'a mut i32 // a mutable reference with an explicit lifetime
// 明示的なライフタイム付きの可変参照
1つのライフタイム注釈それだけでは、大して意味はありません。注釈は、複数の参照のジェネリックなライフタイム引数が、
お互いにどう関係するかをコンパイラに指示することを意図しているからです。例えば、
ライフタイム'a
付きのi32
への参照となる引数first
のある関数があるとしましょう。
この関数にはさらに、'a
のライフタイム付きのi32
への別の参照となるsecond
という別の引数もあります。
ライフタイム注釈は、first
とsecond
の参照がどちらもこのジェネリックなライフタイムと同じだけ生きることを示唆します。
関数シグニチャにおけるライフタイム注釈
さて、longest
関数を例にライフタイム注釈を詳しく見ていきましょう。ジェネリックな型引数同様、
関数名と引数リストの間の山カッコの中にジェネリックなライフタイム引数を宣言します。
このシグニチャで表現したい制約は、引数の全ての参照と戻り値が同じライフタイムを持つことです。
リスト10-22に示すように、ライフタイムを'a
と名付け、それを各参照に付与します。
ファイル名: src/main.rs
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {}", result); } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
リスト10-22: シグニチャの全参照が同じライフタイム'a
を持つと指定したlongest
関数の定義
このコードはコンパイルでき、リスト10-20のmain
関数とともに使用したら、欲しい結果になるはずです。
これで関数シグニチャは、何らかのライフタイム'a
に対して、関数は2つの引数を取り、
どちらも少なくともライフタイム'a
と同じだけ生きる文字列スライスであるとコンパイラに教えるようになりました。
また、この関数シグニチャは、関数から返る文字列スライスも少なくともライフタイム'a
と同じだけ生きると、
コンパイラに教えています。
実際には、longest
関数が返す参照のライフタイムは、渡された参照のうち、小さい方のライフタイムと同じであるという事です。
これらの制約は、まさに私たちがコンパイラに保証してほしかったものです。
この関数シグニチャでライフタイム引数を指定する時、渡されたり、返したりした、いかなる値のライフタイムも変更していないことを思い出してください。
むしろ、借用チェッカーは、これらの制約を守らない値全てを拒否するべきと指定しています。
longest
関数は、x
とy
の正確な生存期間を知っている必要はなく、
このシグニチャを満たすようなスコープを'a
に代入できることを知っているだけであることに注意してください。
関数にライフタイムを注釈するときは、注釈は関数の本体ではなくシグニチャに付与します。 コンパイラは注釈がなくとも関数内のコードを解析できます。しかしながら、 関数に関数外からの参照や関数外への参照がある場合、コンパイラが引数や戻り値のライフタイムを自力で解決することはほとんど不可能になります。 そのライフタイムは、関数が呼び出される度に異なる可能性があります。このために、手動でライフタイムを注釈する必要があるのです。
具体的な参照をlongest
に渡すと、'a
に代入される具体的なライフタイムは、x
のスコープの一部であってy
のスコープと重なる部分となります。
言い換えると、ジェネリックなライフタイム'a
は、x
とy
のライフタイムのうち、小さい方に等しい具体的なライフタイムになるのです。
返却される参照を同じライフタイム引数'a
で注釈したので、返却される参照もx
かy
のライフタイムの小さい方と同じだけ有効になるでしょう。
ライフタイム注釈が異なる具体的なライフタイムを持つ参照を渡すことでlongest
関数を制限する方法を見ましょう。
リスト10-23はそのシンプルな例です。
ファイル名: src/main.rs
fn main() { // 長い文字列は長い let string1 = String::from("long string is long"); // (訳注:この言葉自体に深い意味はない。下の"xyz"より長いということだけが重要) { let string2 = String::from("xyz"); let result = longest(string1.as_str(), string2.as_str()); // 一番長い文字列は{} println!("The longest string is {}", result); } } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
リスト10-23: 異なる具体的なライフタイムを持つString
値への参照でlongest
関数を使用する
この例において、string1
は外側のスコープの終わりまで有効で、string2
は内側のスコープの終わりまで有効、
そしてresult
は内側のスコープの終わりまで有効な何かを参照しています。このコードを実行すると、
借用チェッカーがこのコードを良しとするのがわかるでしょう。要するに、コンパイルでき、
The longest string is long string is long
と出力するのです。
次に、result
の参照のライフタイムが2つの引数の小さい方のライフタイムになることを示す例を試しましょう。
result
変数の宣言を内側のスコープの外に移すものの、result
変数への代入はstring2
のスコープ内に残したままにします。
それからresult
を使用するprintln!
を内側のスコープの外、内側のスコープが終わった後に移動します。
リスト10-24のコードはコンパイルできません。
ファイル名: src/main.rs
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
リスト10-24: string2
がスコープを抜けてからresult
を使用しようとする
このコードのコンパイルを試みると、こんなエラーになります:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("The longest string is {}", result);
| ------ borrow later used here
error: aborting due to previous error
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
このエラーは、result
がprintln!
文に対して有効であるためには、string2
が外側のスコープの終わりまで有効である必要があることを示しています。
関数引数と戻り値のライフタイムを同じライフタイム引数'a
で注釈したので、コンパイラはこのことを知っています。
人間からしたら、string1
はstring2
よりも長く、それ故にresult
がstring1
への参照を含んでいることは
コードから明らかです。まだstring1
はスコープを抜けていないので、
string1
への参照はprintln!
にとって有効でしょう。ですが、コンパイラはこの場合、
参照が有効であると見なせません。longest
関数から返ってくる参照のライフタイムは、
渡した参照のうちの小さい方と同じだとコンパイラに指示しました。したがって、
借用チェッカーは、リスト10-24のコードを無効な参照がある可能性があるとして許可しないのです。
試しに、値や、longest
関数に渡される参照のライフタイムや、返される参照の使われかたが異なる実験をもっとしてみてください。
コンパイル前に、その実験が借用チェッカーを通るかどうか仮説を立ててください; そして、正しいか確かめてください!
ライフタイムの観点で思考する
何にライフタイム引数を指定する必要があるかは、関数が行っていることに依存します。例えば、
longest
関数の実装を最長の文字列スライスではなく、常に最初の引数を返すように変更したら、
y
引数に対してライフタイムを指定する必要はなくなるでしょう。以下のコードはコンパイルできます:
ファイル名: src/main.rs
fn main() { let string1 = String::from("abcd"); let string2 = "efghijklmnopqrstuvwxyz"; let result = longest(string1.as_str(), string2); println!("The longest string is {}", result); } fn longest<'a>(x: &'a str, y: &str) -> &'a str { x }
この例では、引数x
と戻り値に対してライフタイム引数'a
を指定しましたが、引数y
には指定していません。
y
のライフタイムはx
や戻り値のライフタイムとは何の関係もないからです。
関数から参照を返す際、戻り値型のライフタイム引数は、引数のうちどれかのライフタイム引数と一致する必要があります。
返される参照が引数のどれかを参照していないならば、この関数内で生成された値を参照しているはずです。
すると、その値は関数の末端でスコープを抜けるので、これはダングリング参照になるでしょう。
以下に示す、コンパイルできないlongest
関数の未完成の実装を考えてください:
ファイル名: src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &str, y: &str) -> &'a str {
// 本当に長い文字列
let result = String::from("really long string");
result.as_str()
}
ここでは、たとえ、戻り値型にライフタイム引数'a
を指定していても、戻り値のライフタイムは、
引数のライフタイムと全く関係がないので、この実装はコンパイルできないでしょう。
こちらが、得られるエラーメッセージです:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
(エラー[E0515]: ローカル変数`result`を参照している値は返せません)
--> src/main.rs:11:5
|
11 | result.as_str()
| ------^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `result` is borrowed here
| (現在の関数に所有されているデータを参照する値を返しています
| `result`はここで借用されています)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
問題は、result
がlongest
関数の末端でスコープを抜け、片付けられてしまうことです。
かつ、関数からresult
への参照を返そうともしています。ダングリング参照を変えてくれるようなライフタイム引数を指定する手段はなく、
コンパイラは、ダングリング参照を生成させてくれません。今回の場合、最善の修正案は、
(呼び出し先ではなく)呼び出し元の関数に値の片付けをさせるために、参照ではなく所有されたデータ型を返すことでしょう。
究極的にライフタイム記法は、関数のいろんな引数と戻り値のライフタイムを接続することに関するものです。 一旦それらが繋がれば、メモリ安全な処理を許可し、ダングリングポインタを生成したりメモリ安全性を侵害したりする処理を禁止するのに十分な情報をコンパイラは得たことになります。
構造体定義のライフタイム注釈
ここまで、所有された型を保持する構造体だけを定義してきました。構造体に参照を保持させることもできますが、
その場合、構造体定義の全参照にライフタイム注釈を付け加える必要があるでしょう。
リスト10-25には、文字列スライスを保持するImportantExcerpt
(重要な一節)という構造体があります。
ファイル名: src/main.rs
struct ImportantExcerpt<'a> { part: &'a str, } fn main() { // 僕をイシュマエルとお呼び。何年か前・・・ let novel = String::from("Call me Ishmael. Some years ago..."); // "'.'が見つかりませんでした" let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence, }; }
リスト10-25: 参照を含む構造体なので、定義にライフタイム注釈が必要
この構造体には文字列スライスを保持する1つのフィールド、part
があり、これは参照です。
ジェネリックなデータ型同様、構造体名の後、山カッコの中にジェネリックなライフタイム引数の名前を宣言するので、
構造体定義の本体でライフタイム引数を使用できます。この注釈は、ImportantExcerpt
のインスタンスが、
part
フィールドに保持している参照よりも長生きしないことを意味します。
ここのmain
関数は、変数novel
に所有されるString
の、最初の文への参照を保持するImportantExcerpt
インスタンスを生成しています。
novel
のデータは、ImportantExcerpt
インスタンスが作られる前に存在しています。
加えて、ImportantExcerpt
がスコープを抜けるまでnovel
はスコープを抜けないので、
ImportantExcerpt
インスタンスの参照は有効なのです。
ライフタイム省略
全参照にはライフタイムがあり、参照を使用する関数や構造体にはライフタイム引数を指定する必要があることを学びました。 しかし、リスト4-9にあった関数(リスト10-26に再度示しました)はライフタイム注釈なしでコンパイルできました。
ファイル名: src/lib.rs
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("hello world"); // first_word works on slices of `String`s let word = first_word(&my_string[..]); let my_string_literal = "hello world"; // first_word works on slices of string literals let word = first_word(&my_string_literal[..]); // Because string literals *are* string slices already, // this works too, without the slice syntax! let word = first_word(my_string_literal); }
リスト10-26: リスト4-9で定義した、引数と戻り値型が参照であるにも関わらず、ライフタイム注釈なしでコンパイルできた関数
この関数がライフタイム注釈なしでコンパイルできる理由には、Rustの歴史が関わっています: 昔のバージョンのRust(1.0以前)では、 全参照に明示的なライフタイムが必要だったので、このコードはコンパイルできませんでした。 その頃、関数シグニチャはこのように記述されていたのです:
fn first_word<'a>(s: &'a str) -> &'a str {
多くのRustコードを書いた後、Rustチームは、Rustプログラマが、 特定の場面で何度も同じライフタイム注釈を入力していることを発見しました。これらの場面は予測可能で、 いくつかの決まりきったパターンに従っていました。開発者はこのパターンをコンパイラのコードに落とし込んだので、 このような場面には借用チェッカーがライフタイムを推論できるようになり、明示的な注釈を必要としなくなったのです。
ここで、このRustの歴史話が関係しているのは、他にも決まりきったパターンが出現し、コンパイラに追加されることもあり得るからです。 将来的に、さらに少数のライフタイム注釈しか必要にならない可能性もあります。
コンパイラの参照解析に落とし込まれたパターンは、ライフタイム省略規則と呼ばれます。 これらはプログラマが従う規則ではありません; コンパイラが考慮する一連の特定のケースであり、 自分のコードがこのケースに当てはまれば、ライフタイムを明示的に書く必要はなくなります。
省略規則は、完全な推論を提供しません。コンパイラが決定的に規則を適用できるけれども、 参照が保持するライフタイムに関してそれでも曖昧性があるなら、コンパイラは、残りの参照がなるべきライフタイムを推測しません。 この場合コンパイラは、それらを推測するのではなくエラーを与えます。 これらは、参照がお互いにどう関係するかを指定するライフタイム注釈を追記することで解決できます。
関数やメソッドの引数のライフタイムは、入力ライフタイムと呼ばれ、 戻り値のライフタイムは出力ライフタイムと称されます。
コンパイラは3つの規則を活用し、明示的な注釈がない時に、参照がどんなライフタイムになるかを計算します。
最初の規則は入力ライフタイムに適用され、2番目と3番目の規則は出力ライフタイムに適用されます。
コンパイラが3つの規則の最後まで到達し、それでもライフタイムを割り出せない参照があったら、
コンパイラはエラーで停止します。
これらのルールはfn
の定義にもimpl
ブロックにも適用されます。
最初の規則は、参照である各引数は、独自のライフタイム引数を得るというものです。換言すれば、
1引数の関数は、1つのライフタイム引数を得るということです: fn foo<'a>(x: &'a i32)
;
2つ引数のある関数は、2つの個別のライフタイム引数を得ます: fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
;
以下同様。
2番目の規則は、1つだけ入力ライフタイム引数があるなら、そのライフタイムが全ての出力ライフタイム引数に代入されるというものです:
fn foo<'a>(x: &'a i32) -> &'a i32
。
3番目の規則は、複数の入力ライフタイム引数があるけれども、メソッドなのでそのうちの一つが&self
や&mut self
だったら、
self
のライフタイムが全出力ライフタイム引数に代入されるというものです。
この3番目の規則により、必要なシンボルの数が減るので、メソッドが遥かに読み書きしやすくなります。
コンパイラの立場になってみましょう。これらの規則を適用して、リスト10-26のfirst_word
関数のシグニチャの参照のライフタイムが何か計算します。
シグニチャは、参照に紐づけられるライフタイムがない状態から始まります:
fn first_word(s: &str) -> &str {
そうして、コンパイラは最初の規則を適用し、各引数が独自のライフタイムを得ると指定します。
それを通常通り'a
と呼ぶので、シグニチャはこうなります:
fn first_word<'a>(s: &'a str) -> &str {
1つだけ入力ライフタイムがあるので、2番目の規則を適用します。2番目の規則は、1つの入力引数のライフタイムが、 出力引数に代入されると指定するので、シグニチャはこうなります:
fn first_word<'a>(s: &'a str) -> &'a str {
もうこの関数シグニチャの全ての参照にライフタイムが付いたので、コンパイラは、 プログラマにこの関数シグニチャのライフタイムを注釈してもらう必要なく、解析を続行できます。
別の例に目を向けましょう。今回は、リスト10-21で取り掛かったときにはライフタイム引数がなかったlongest
関数です:
fn longest(x: &str, y: &str) -> &str {
最初の規則を適用しましょう: 各引数が独自のライフタイムを得るのです。今回は、 1つではなく2つ引数があるので、ライフタイムも2つです:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
2つ以上入力ライフタイムがあるので、2番目の規則は適用されないとわかります。また3番目の規則も適用されません。
longest
はメソッドではなく関数なので、どの引数もself
ではないのです。3つの規則全部を適用した後でも、
まだ戻り値型のライフタイムが判明していません。このために、リスト10-21でこのコードをコンパイルしようとしてエラーになったのです:
コンパイラは、ライフタイム省略規則全てを適用したけれども、シグニチャの参照全部のライフタイムを計算できなかったのです。
実際のところ、3番目の規則はメソッドのシグニチャにしか適用されません。ですので、次はその文脈においてライフタイムを観察し、 3番目の規則のおかげで、メソッドシグニチャであまり頻繁にライフタイムを注釈しなくても済む理由を確認します。
メソッド定義におけるライフタイム注釈
構造体にライフタイムのあるメソッドを実装する際、リスト10-11で示したジェネリックな型引数と同じ記法を使用します。 ライフタイム引数を宣言し使用する場所は、構造体フィールドかメソッド引数と戻り値に関係するかによります。
構造体のフィールド用のライフタイム名は、impl
キーワードの後に宣言する必要があり、
それから構造体名の後に使用されます。そのようなライフタイムは構造体の型の一部になるからです。
impl
ブロック内のメソッドシグニチャでは、参照は構造体のフィールドの参照のライフタイムに紐づいている可能性と、
独立している可能性があります。加えて、ライフタイム省略規則により、メソッドシグニチャでライフタイム注釈が必要なくなることがよくあります。
リスト10-25で定義したImportantExcerpt
という構造体を使用した例をいくつか見てみましょう。
まず、唯一の引数がself
への参照で戻り値がi32
という何かへの参照ではないlevel
というメソッドを使用します:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { // "お知らせします: {}" println!("Attention please: {}", announcement); self.part } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence, }; }
impl
後のライフタイム引数宣言と型名の後にそれを使用するのは必須ですが、最初の省略規則のため、
self
への参照のライフタイムを注釈する必要はありません。
3番目のライフタイム省略規則が適用される例はこちらです:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { // "お知らせします: {}" println!("Attention please: {}", announcement); self.part } } fn main() { let novel = String::from("Call me Ishmael. Some years ago..."); let first_sentence = novel.split('.').next().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence, }; }
2つ入力ライフタイムがあるので、コンパイラは最初のライフタイム省略規則を適用し、
&self
とannouncement
に独自のライフタイムを与えます。それから、
引数の1つが&self
なので、戻り値型は&self
のライフタイムを得て、
全てのライフタイムが説明されました。
静的ライフタイム
議論する必要のある1種の特殊なライフタイムが、'static
であり、これは、この参照がプログラムの全期間生存できる事を意味します。
文字列リテラルは全て'static
ライフタイムになり、次のように注釈できます:
#![allow(unused)] fn main() { // 僕は静的ライフタイムを持ってるよ let s: &'static str = "I have a static lifetime."; }
この文字列のテキストは、プログラムのバイナリに直接格納され、常に利用可能です。故に、全文字列リテラルのライフタイムは、
'static
なのです。
エラーメッセージで、'static
ライフタイムを使用するよう勧める提言を見かける可能性があります。
ですが、参照に対してライフタイムとして'static
を指定する前に、今ある参照が本当にプログラムの全期間生きるかどうか考えてください。
それが可能であったとしても、参照がそれだけの期間生きてほしいのかどうか考慮するのも良いでしょう。
ほとんどの場合、問題は、ダングリング参照を生成しようとしているか、利用可能なライフタイムの不一致が原因です。
そのような場合、解決策はその問題を修正することであり、'static
ライフタイムを指定することではありません。
ジェネリックな型引数、トレイト境界、ライフタイムを一度に
ジェネリックな型引数、トレイト境界、ライフタイム指定の構文のすべてを1つの関数で簡単に見てみましょう!
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest_with_an_announcement( string1.as_str(), string2, "Today is someone's birthday!", ); println!("The longest string is {}", result); } use std::fmt::Display; fn longest_with_an_announcement<'a, T>( x: &'a str, y: &'a str, ann: T, ) -> &'a str where T: Display, { // "アナウンス! {}" println!("Announcement! {}", ann); if x.len() > y.len() { x } else { y } }
これがリスト10-22からの2つの文字列のうち長い方を返すlongest
関数ですが、
ジェネリックな型T
のann
という追加の引数があり、これはwhere
節で指定されているように、
Display
トレイトを実装するあらゆる型で埋めることができます。
この追加の引数は、関数が文字列スライスの長さを比較する前に出力されるので、
Display
トレイト境界が必要なのです。ライフタイムは一種のジェネリックなので、
ライフタイム引数'a
とジェネリックな型引数T
が関数名の後、山カッコ内の同じリストに収まっています。
まとめ
たくさんのことをこの章では講義しましたね!今やジェネリックな型引数、トレイトとトレイト境界、そしてジェネリックなライフタイム引数を知ったので、 多くの異なる場面で動くコードを繰り返すことなく書く準備ができました。ジェネリックな型引数により、 コードを異なる型に適用させてくれます。トレイトとトレイト境界は、型がジェネリックであっても、 コードが必要とする振る舞いを持つことを保証します。ライフタイム注釈を活用して、 この柔軟なコードにダングリング参照が存在しないことを保証する方法を学びました。 さらにこの解析は全てコンパイル時に起こり、実行時のパフォーマンスには影響しません!
信じられないかもしれませんが、この章で議論した話題にはもっともっと学ぶべきことがあります: 第17章ではトレイトオブジェクトを議論します。これはトレイトを使用する別の手段です。 非常に高度な筋書きの場合でのみ必要になる、ライフタイム注釈が関わる、もっと複雑な筋書きもあります。 それらについては、Rust Referenceをお読みください。 ですが次は、コードがあるべき通りに動いていることを確かめられるように、Rustでテストを書く方法を学びます。
自動テストを書く
1972年のエッセイ「謙虚なプログラマ」でエドガー・W・ダイクストラは以下のように述べています。 「プログラムのテストは、バグの存在を示すには非常に効率的な手法であるが、 バグの不在を示すには望み薄く不適切である」と。これは、できるだけテストを試みるべきではないということではありません。
プログラムの正当性は、どこまで自分のコードが意図していることをしているかなのです。 Rustは、プログラムの正当性に重きを置いて設計されていますが、 正当性は複雑で、単純に証明することはありません。Rustの型システムは、 この重荷の多くの部分を肩代わりしてくれますが、型システムはあらゆる種類の不当性を捕捉してはくれません。 ゆえに、Rustでは、言語内で自動化されたソフトウェアテストを書くことをサポートしているのです。
例として、渡された何かの数値に2を足すadd_two
という関数を書くとしましょう。
この関数のシグニチャは、引数に整数を取り、結果として整数を返します。
この関数を実装してコンパイルすると、コンパイラはこれまでに学んできた型チェックと借用チェックを全て行い、
例えば、String
の値や無効な参照をこの関数に渡していないかなどを確かめるのです。
ところが、コンパイラはプログラマがまさしく意図したことを関数が実行しているかどうかは確かめられません。
つまり、そうですね、引数に10を足したり、50を引いたりするのではなく、引数に2を足していることです。
そんな時に、テストは必要になるのです。
例えば、add_two
関数に3
を渡した時に、戻り値は5であることをアサーションするようなテストを書くことができます。
コードに変更を加えた際にこれらのテストを走らせ、既存の正当な振る舞いが変わっていないことを確認できます。
テストは、複雑なスキルです: いいテストの書き方をあらゆる方面から講義することは1章だけではできないのですが、 Rustのテスト機構のメカニズムについて議論します。テストを書く際に利用可能になるアノテーションとマクロについて、 テストを実行するのに提供されているオプションと標準の動作、さらにテストをユニットテストや統合テストに体系化する方法について語ります。
テストの記述法
テストは、テスト以外のコードが想定された方法で機能していることを実証するRustの関数です。 テスト関数の本体は、典型的には以下の3つの動作を行います:
- 必要なデータや状態をセットアップする。
- テスト対象のコードを走らせる。
- 結果が想定通りであることを断定(以下、アサーションという)する。
Rustが、特にこれらの動作を行うテストを書くために用意している機能を見ていきましょう。
これには、test
属性、いくつかのマクロ、should_panic
属性が含まれます。
テスト関数の構成
最も単純には、Rustにおけるテストはtest
属性で注釈された関数のことです。属性とは、
Rustコードの部品に関するメタデータです; 一例を挙げれば、構造体とともに第5章で使用したderive
属性です。
関数をテスト関数に変えるには、fn
の前に#[test]
を付け加えてください。
cargo test
コマンドでテストを実行したら、コンパイラはtest
属性で注釈された関数を走らせるテスト用バイナリをビルドし、
各テスト関数が通過したか失敗したかを報告します。
新しいライブラリプロジェクトをCargoで作ると、テスト関数付きのテストモジュールが自動的に生成されます。 このモジュールのおかげで、新しいプロジェクトを始めるたびにテスト関数の正しい構造とか文法をいちいち検索しなくてすみます。 ここに好きな数だけテスト関数やテストモジュールを追加すればいいというわけです!
まず、実際にはコードをテストしない、自動生成されたテンプレートのテストで実験して、テストの動作の性質をいくらか学びましょう。 その後で、以前書いたコードを呼び出し、振る舞いが正しいことをアサーションする、ホンモノのテストを書きましょう。
adder
という新しいライブラリプロジェクトを生成しましょう:
$ cargo new adder --lib
Created library `adder` project
$ cd adder
adder
ライブラリのsrc/lib.rsファイルの中身は、リスト11-1のような見た目のはずです。
ファイル名: src/lib.rs
#[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } } fn main() {}
リスト11-1: cargo new
で自動生成されたテストモジュールと関数
とりあえず、最初の2行は無視し、関数に集中してその動作法を見ましょう。
fn
行の#[test]
注釈に注目してください: この属性は、これがテスト関数であることを示すので、
テスト実行機はこの関数をテストとして扱うとわかるのです。さらに、tests
モジュール内にはテスト関数以外の関数を入れ、
一般的なシナリオをセットアップしたり、共通の処理を行う手助けをしたりもできるので、
#[test]
属性でどの関数がテストかを示す必要があるのです。
関数本体は、assert_eq!
マクロを使用して、2 + 2が4に等しいことをアサーションしています。
このアサーションは、典型的なテストのフォーマット例をなしているわけです。走らせてこのテストが通る(訳注:テストが成功する、の意味。英語でpassということから、このように表現される)ことを確かめましょう。
cargo test
コマンドでプロジェクトにあるテストが全て実行されます。リスト11-2に示したようにですね。
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.57s
Running target/debug/deps/adder-92948b65e88960b4
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
リスト11-2: 自動生成されたテストを走らせた出力
Cargoがテストをコンパイルし、走らせました。Compiling
, Finished
, Running
の行の後にrunning 1 test
の行があります。
次行が、生成されたテスト関数のit_works
という名前とこのテストの実行結果、ok
を示しています。
テスト実行の総合的なまとめが次に出現します。test result:ok.
というテキストは、
全テストが通ったことを意味し、1 passed; 0 failed
と読める部分は、通過または失敗したテストの数を合計しているのです。
無視すると指定したテストは何もなかったため、まとめは0 ignored
と示しています。
また、実行するテストにフィルタをかけもしなかったので、まとめの最後に0 filtered out
と表示されています。
テストを無視することとフィルタすることに関しては次の節、テストの実行され方を制御するで語ります。
0 measured
という統計は、パフォーマンスを測定するベンチマークテスト用です。
ベンチマークテストは、本書記述の時点では、nightly版のRustでのみ利用可能です。
詳しくは、ベンチマークテストのドキュメンテーションを参照されたし。
テスト出力の次の部分、つまりDoc-tests adder
で始まる部分は、ドキュメンテーションテストの結果用のものです。
まだドキュメンテーションテストは何もないものの、コンパイラは、APIドキュメントに現れるどんなコード例もコンパイルできます。
この機能により、ドキュメントとコードを同期することができるわけです。ドキュメンテーションテストの書き方については、
第14章のテストとしてのドキュメンテーションコメント節で議論しましょう。今は、Doc-tests
出力は無視します。
テストの名前を変更してどうテスト出力が変わるか確かめましょう。it_works
関数を違う名前、exploration
などに変えてください。
そう、以下のように:
ファイル名: src/lib.rs
#[cfg(test)] mod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); } } fn main() {}
そして、cargo test
を再度走らせます。これで出力がit_works
の代わりにexploration
と表示しています:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.59s
Running target/debug/deps/adder-92948b65e88960b4
running 1 test
test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
別のテストを追加しますが、今回は失敗するテストにしましょう!テスト関数内の何かがパニックすると、
テストは失敗します。各テストは、新規スレッドで実行され、メインスレッドが、テストスレッドが死んだと確認した時、
テストは失敗と印づけられます。第9章でパニックを引き起こす最も単純な方法について語りました。
そう、panic!
マクロを呼び出すことですね。src/lib.rsファイルがリスト11-3のような見た目になるよう、
新しいテストanother
を入力してください。
ファイル名: src/lib.rs
#[cfg(test)] mod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); } #[test] fn another() { //このテストを失敗させる panic!("Make this test fail"); } } fn main() {}
リスト11-3: panic!
マクロを呼び出したために失敗する2番目のテストを追加する
cargo test
で再度テストを走らせてください。出力はリスト11-4のようになるはずであり、
exploration
テストは通り、another
は失敗したと表示されます。
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.72s
Running target/debug/deps/adder-92948b65e88960b4
running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok
failures:
---- tests::another stdout ----
thread 'main' panicked at 'Make this test fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
failures:
tests::another
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--lib'
リスト11-4: 一つのテストが通り、一つが失敗するときのテスト結果
ok
の代わりにtest test::another
の行は、FAILED
を表示しています。個々の結果とまとめの間に、
2つ新たな区域ができました: 最初の区域は、失敗したテスト各々の具体的な理由を表示しています。
今回の場合、another
は'Make this test fail'でパニックした
ために失敗し、
これは、src/lib.rsファイルの10行で起きました。次の区域は失敗したテストの名前だけを列挙しています。
これは、テストがたくさんあり、失敗したテストの詳細がたくさん表示されるときに有用になります。
失敗したテストの名前を使用してそのテストだけを実行し、より簡単にデバッグすることができます。
テストの実行方法については、テストの実行され方を制御する節でもっと語りましょう。
サマリー行が最後に出力されています: 総合的に言うと、テスト結果はFAILED
でした。
一つのテストが通り、一つが失敗したわけです。
様々な状況でのテスト結果がどんな風になるか見てきたので、テストを行う際に有用になるpanic!
以外のマクロに目を向けましょう。
assert!
マクロで結果を確認する
assert!
マクロは、標準ライブラリで提供されていますが、テスト内の何らかの条件がtrue
と評価されることを確かめたいときに有効です。
assert!
マクロには、論理値に評価される引数を与えます。その値がtrue
なら、
assert!
は何もせず、テストは通ります。その値がfalse
なら、assert!
マクロはpanic!
マクロを呼び出し、
テストは失敗します。assert!
マクロを使用することで、コードが意図した通りに機能していることを確認する助けになるわけです。
第5章のリスト5-15で、Rectangle
構造体とcan_hold
メソッドを使用しました。リスト11-5でもそれを繰り返しています。
このコードをsrc/lib.rsファイルに放り込み、assert!
マクロでそれ用のテストを何か書いてみましょう。
ファイル名: src/lib.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } fn main() {}
リスト11-5: 第5章からRectangle
構造体とそのcan_hold
メソッドを使用する
can_hold
メソッドは論理値を返すので、assert!
マクロの完璧なユースケースになるわけです。
リスト11-6で、幅が8、高さが7のRectangle
インスタンスを生成し、これが幅5、
高さ1の別のRectangle
インスタンスを保持できるとアサーションすることでcan_hold
を用いるテストを書きます。
ファイル名: src/lib.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } #[cfg(test)] mod tests { use super::*; #[test] fn larger_can_hold_smaller() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(larger.can_hold(&smaller)); } } fn main() {}
リスト11-6: より大きな長方形がより小さな長方形を確かに保持できるかを確認するcan_hold
用のテスト
tests
モジュール内に新しい行を加えたことに注目してください: use super::*
です。
tests
モジュールは、第7章のモジュールツリーの要素を示すためのパス節で講義した通常の公開ルールに従う普通のモジュールです。
tests
モジュールは、内部モジュールなので、外部モジュール内のテスト配下にあるコードを内部モジュールのスコープに持っていく必要があります。
ここではglobを使用して、外部モジュールで定義したもの全てがこのtests
モジュールでも使用可能になるようにしています。
テストはlarger_can_hold_smaller
と名付け、必要なRectangle
インスタンスを2つ生成しています。
そして、assert!
マクロを呼び出し、larger.can_hold(&smaller)
の呼び出し結果を渡しました。
この式は、true
を返すと考えられるので、テストは通るはずです。確かめましょう!
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running target/debug/deps/rectangle-6584c4561e48942e
running 1 test
test tests::larger_can_hold_smaller ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
通ります!別のテストを追加しましょう。今回は、小さい長方形は、より大きな長方形を保持できないことをアサーションします。
ファイル名: src/lib.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } #[cfg(test)] mod tests { use super::*; #[test] fn larger_can_hold_smaller() { // --snip-- let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(larger.can_hold(&smaller)); } #[test] fn smaller_cannot_hold_larger() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(!smaller.can_hold(&larger)); } } fn main() {}
今回の場合、can_hold
関数の正しい結果はfalse
なので、その結果をassert!
マクロに渡す前に反転させる必要があります。
結果として、can_hold
がfalse
を返せば、テストは通ります。
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running target/debug/deps/rectangle-6584c4561e48942e
running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
通るテストが2つ!さて、コードにバグを導入したらテスト結果がどうなるか確認してみましょう。
幅を比較する大なり記号を小なり記号で置き換えてcan_hold
メソッドの実装を変更しましょう:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } // --snip-- impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width < other.width && self.height > other.height } } #[cfg(test)] mod tests { use super::*; #[test] fn larger_can_hold_smaller() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(larger.can_hold(&smaller)); } #[test] fn smaller_cannot_hold_larger() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(!smaller.can_hold(&larger)); } } fn main() {}
テストを実行すると、以下のような出力をします:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running target/debug/deps/rectangle-6584c4561e48942e
running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok
failures:
---- tests::larger_can_hold_smaller stdout ----
thread 'main' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:28:9
(スレッド'main'はsrc/lib.rs:28:9の'assertion failed: larger.can_hold(&smaller)'でパニックしました)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
failures:
tests::larger_can_hold_smaller
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--lib'
テストによりバグが捕捉されました!larger.width
が8、smaller.width
が5なので、
can_hold
内の幅の比較が今はfalse
を返すようになったのです: 8は5より小さくないですからね。
assert_eq!
とassert_ne!
マクロで等値性をテストする
機能をテストする一般的な方法は、テスト下にあるコードの結果をコードが返すと期待される値と比較して、
等しいと確かめることです。これをassert
マクロを使用して==
演算子を使用した式を渡すことで行うこともできます。
しかしながら、これはありふれたテストなので、標準ライブラリには1組のマクロ(assert_eq!
とassert_ne!
)が提供され、
このテストをより便利に行うことができます。これらのマクロはそれぞれ、二つの引数を比べ、等しいかと等しくないかを確かめます。
また、アサーションが失敗したら二つの値の出力もし、テストが失敗した原因を確認しやすくなります。
一方でassert!
マクロは、==
式の値がfalse
になったことしか示さず、false
になった原因の値は出力しません。
リスト11-7において、引数に2
を加えて結果を返すadd_two
という名前の関数を書いています。
そして、assert_eq!
マクロでこの関数をテストしています。
ファイル名: src/lib.rs
pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn it_adds_two() { assert_eq!(4, add_two(2)); } } fn main() {}
リスト11-7: assert_eq!
マクロでadd_two
関数をテストする
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running target/debug/deps/adder-92948b65e88960b4
running 1 test
test tests::it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
assert_eq!
マクロに与えた第1引数の4
は、add_two(2)
の呼び出し結果と等しいです。
このテストの行はtest tests::it_adds_two ... ok
であり、ok
というテキストはテストが通ったことを示しています!
コードにバグを仕込んで、assert_eq!
を使ったテストが失敗した時にどんな見た目になるのか確認してみましょう。
add_two
関数の実装を代わりに3
を足すように変えてください:
pub fn add_two(a: i32) -> i32 { a + 3 } #[cfg(test)] mod tests { use super::*; #[test] fn it_adds_two() { assert_eq!(4, add_two(2)); } } fn main() {}
テストを再度実行します:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished test [unoptimized + debuginfo] target(s) in 0.61s
Running target/debug/deps/adder-92948b65e88960b4
running 1 test
test tests::it_adds_two ... FAILED
failures:
---- tests::it_adds_two stdout ----
thread 'main' panicked at 'assertion failed: `(left == right)`
left: `4`,
right: `5`', src/lib.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
failures:
tests::it_adds_two
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--lib'
テストがバグを捕捉しました!it_adds_two
のテストは失敗し、assertion failed: `(left == right)`
というメッセージを表示し、
left
は4
で、right
は5
だったと示しています。このメッセージは有用で、デバッグを開始する助けになります:
assert_eq!
のleft
引数は4
だったが、add_two(2)
があるright
引数は5
だったことを意味しています。
二つの値が等しいとアサーションを行う関数の引数を
expected
とactual
と呼び、引数を指定する順序が問題になる言語やテストフレームワークもあることに注意してください。
ですがRustでは、left
とright
と呼ばれ、期待する値とテスト下のコードが生成する値を指定する順序は
問題になりません。今回のテストのアサーションをassert_eq!(add_two(2), 4)
と書くこともでき、
そうすると失敗メッセージは、assertion failed: `(left == right)`
となり、
left
が5
でright
が4
と表示されるでしょう。
assert_ne!
マクロは、与えた2つの値が等しくなければ通り、等しければ失敗します。
このマクロは、値が何になるだろうか確信が持てないけれども、コードが意図した通りに動いていれば、
確実にこの値にはならないだろうとわかっているような場合に最も有用になります。例えば、
入力を何らかの手段で変え(て出力す)ることが保証されているけれども、入力の変え方がテストを実行する曜日に依存する関数をテストしているなら、
アサーションすべき最善の事柄は、関数の出力が入力と等しくないことかもしれません。
内部的には、assert_eq!
とassert_ne!
マクロは、それぞれ==
と!=
演算子を使用しています。
アサーションが失敗すると、これらのマクロは引数をデバッグフォーマットを使用してプリントするので、
比較対象の値はPartialEq
とDebug
トレイトを実装していなければなりません。
すべての組み込み型と、ほぼすべての標準ライブラリの型はこれらのトレイトを実装しています。
自分で定義した構造体やenumについては、
その型の値が等しいか等しくないかをアサーションするために、PartialEq
を実装する必要があるでしょう。
それが失敗した時にその値をプリントできるように、Debug
を実装する必要もあるでしょう。
第5章のリスト5-12で触れたように、どちらのトレイトも導出可能なトレイトなので、
これは通常、単純に構造体やenum定義に#[derive(PartialEq, Debug)]
という注釈を追加するだけですみます。
これらやその他の導出可能なトレイトに関する詳細については、付録C、導出可能なトレイトをご覧ください。
カスタムの失敗メッセージを追加する
さらに、assert!
、assert_eq!
、assert_ne!
の追加引数として、失敗メッセージと共にカスタムのメッセージが表示されるよう、
追加することもできます。assert!
の1つの必須引数の後に、
あるいはassert_eq!
とassert_ne!
の2つの必須引数の後に指定された引数はすべてformat!
マクロに渡されるので、
(format!マクロについては第8章の+
演算子、またはformat!
マクロで連結節で議論しました)、
{}
プレースホルダーを含むフォーマット文字列とこのプレースホルダーに置き換えられる値を渡すことができます。
カスタムメッセージは、アサーションがどんな意味を持つかドキュメント化するのに役に立ちます;
もしテストが失敗した時、コードにどんな問題があるのかをよりしっかり把握できるはずです。
例として、人々に名前で挨拶をする関数があり、関数に渡した名前が出力に出現することをテストしたいとしましょう:
ファイル名: src/lib.rs
pub fn greeting(name: &str) -> String { format!("Hello {}!", name) } #[cfg(test)] mod tests { use super::*; #[test] fn greeting_contains_name() { let result = greeting("Carol"); assert!(result.contains("Carol")); } } fn main() {}
このプログラムの要件はまだ取り決められておらず、挨拶の先頭のHello
というテキストはおそらく変わります。
要件が変わった時にテストを更新しなくてもよいようにしたいと考え、
greeting
関数から返る値と正確な等値性を確認するのではなく、出力が入力引数のテキストを含むことをアサーションするだけにします。
greeting
がname
を含まないように変更してこのコードにバグを仕込み、このテストの失敗がどんな風になるのか確かめましょう:
pub fn greeting(name: &str) -> String { String::from("Hello!") } #[cfg(test)] mod tests { use super::*; #[test] fn greeting_contains_name() { let result = greeting("Carol"); assert!(result.contains("Carol")); } } fn main() {}
このテストを実行すると、以下のように出力されます:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished test [unoptimized + debuginfo] target(s) in 0.91s
Running target/debug/deps/greeter-170b942eb5bf5e3a
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'assertion failed: result.contains("Carol")', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--lib'
この結果は、アサーションが失敗し、どの行にアサーションがあるかを示しているだけです。
今回の場合、失敗メッセージがgreeting
関数から得た値を出力していればより有用でしょう。
テスト関数を変更し、
greeting
関数から得た実際の値で埋められるプレースホルダーを含むフォーマット文字列からなるカスタムの失敗メッセージを与えてみましょう。
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
//挨拶(greeting)は名前を含んでいません。その値は`{}`でした
"Greeting did not contain name, value was `{}`",
result
);
}
}
これでテストを実行したら、より有益なエラーメッセージが得られるでしょう:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished test [unoptimized + debuginfo] target(s) in 0.93s
Running target/debug/deps/greeter-170b942eb5bf5e3a
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'main' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--lib'
実際に得られた値がテスト出力に表示されているので、起こると想定していたものではなく、 起こったものをデバッグするのに役に立ちます。
should_panic
でパニックを確認する
期待する正しい値をコードが返すことを確認することに加えて、想定通りにコードがエラー状態を扱っていることを確認するのも重要です。
例えば、第9章のリスト9-10で生成したGuess
型を考えてください。Guess
を使用する他のコードは、
Guess
のインスタンスは1から100の範囲の値しか含まないという保証に依存しています。
その範囲外の値でGuess
インスタンスを生成しようとするとパニックすることを確認するテストを書くことができます。
これは、テスト関数にshould_panic
という別の属性を追加することで達成できます。
この属性は、関数内のコードがパニックしたら、テストを通過させます。つまり、
関数内のコードがパニックしなかったら、テストは失敗するわけです。
リスト11-8は、予想どおりにGuess::new
のエラー条件が発生していることを確認するテストを示しています。
ファイル名: src/lib.rs
pub struct Guess { value: i32, } impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { //予想値は1から100の間でなければなりませんが、{}でした。 panic!("Guess value must be between 1 and 100, got {}.", value); } Guess { value } } } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic] fn greater_than_100() { Guess::new(200); } } fn main() {}
リスト11-8: 状況がpanic!
を引き起こすとテストする
#[test]
属性の後、適用するテスト関数の前に#[should_panic]
属性を配置しています。
このテストが通るときの結果を見ましょう:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.58s
Running target/debug/deps/guessing_game-57d70c3acb738f4d
running 1 test
test tests::greater_than_100 ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests guessing_game
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
よさそうですね!では、値が100より大きいときにnew
関数がパニックするという条件を除去することでコードにバグを導入しましょう:
pub struct Guess { value: i32, } // --snip-- impl Guess { pub fn new(value: i32) -> Guess { if value < 1 { //予想値は1から100の間でなければなりませんが、{}でした。 panic!("Guess value must be between 1 and 100, got {}.", value); } Guess { value } } } #[cfg(test)] mod tests { use super::*; #[test] #[should_panic] fn greater_than_100() { Guess::new(200); } } fn main() {}
リスト11-8のテストを実行すると、失敗するでしょう:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.62s
Running target/debug/deps/guessing_game-57d70c3acb738f4d
running 1 test
test tests::greater_than_100 ... FAILED
failures:
---- tests::greater_than_100 stdout ----
note: test did not panic as expected
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--lib'
この場合、それほど役に立つメッセージは得られませんが、テスト関数に目を向ければ、
#[should_panic]
で注釈されていることがわかります。得られた失敗は、
テスト関数のコードがパニックを引き起こさなかったことを意味するのです。
should_panic
を使用するテストは不正確なこともあります。なぜなら、コードが何らかのパニックを起こしたことしか示さないからです。
should_panic
のテストは、起きると想定していたもの以外の理由でテストがパニックしても通ってしまうのです。
should_panic
のテストの正確を期すために、should_panic
属性にexpected
引数を追加することもできます。
このテストハーネスは、失敗メッセージに与えられたテキストが含まれていることを確かめてくれます。
例えば、リスト11-9の修正されたGuess
のコードを考えてください。ここでは、
new
関数は、値が大きすぎるか小さすぎるかによって異なるメッセージでパニックします。
ファイル名: src/lib.rs
pub struct Guess { value: i32, } // --snip-- impl Guess { pub fn new(value: i32) -> Guess { if value < 1 { panic!( //予想値は1以上でなければなりませんが、{}でした。 "Guess value must be greater than or equal to 1, got {}.", value ); } else if value > 100 { panic!( //予想値は100以下でなければなりませんが、{}でした。 "Guess value must be less than or equal to 100, got {}.", value ); } Guess { value } } } #[cfg(test)] mod tests { use super::*; #[test] //予想値は100以下でなければなりません #[should_panic(expected = "Guess value must be less than or equal to 100")] fn greater_than_100() { Guess::new(200); } } fn main() {}
リスト11-9: 状況が特定のパニックメッセージでpanic!
を引き起こすことをテストする
should_panic
属性のexpected
引数に置いた値がGuess::new
関数がパニックしたメッセージの一部になっているので、
このテストは通ります。予想されるパニックメッセージ全体を指定することもでき、今回の場合、
Guess value must be less than or equal to 100, got 200.
となります。
should_panic
の予想される引数に何を指定するかは、パニックメッセージのどこが固有でどこが動的か、
またテストをどの程度正確に行いたいかによります。今回の場合、パニックメッセージの一部でも、テスト関数内のコードが、
else if value > 100
の場合を実行していると確認するのに事足りるのです。
expected
メッセージありのshould_panic
テストが失敗すると何が起きるのが確かめるために、
if value < 1
とelse if value > 100
ブロックの本体を入れ替えることで再度コードにバグを仕込みましょう:
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
//予想値は100以下でなければなりませんが、{}でした。
"Guess value must be less than or equal to 100, got {}.",
value
);
} else if value > 100 {
panic!(
//予想値は1以上でなければなりませんが、{}でした。
"Guess value must be greater than or equal to 1, got {}.",
value
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Guess value must be less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
should_panic
テストを実行すると、今回は失敗するでしょう:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished test [unoptimized + debuginfo] target(s) in 0.66s
Running target/debug/deps/guessing_game-57d70c3acb738f4d
running 1 test
test tests::greater_than_100 ... FAILED
failures:
---- tests::greater_than_100 stdout ----
thread 'main' panicked at 'Guess value must be greater than or equal to 1, got 200.', src/lib.rs:13:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
note: panic did not contain expected string
panic message: `"Guess value must be greater than or equal to 1, got 200."`,
expected substring: `"Guess value must be less than or equal to 100"`
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--lib'
この失敗メッセージは、このテストが確かに予想通りパニックしたことを示していますが、
パニックメッセージは、予想される文字列の'Guess value must be less than or equal to 100'
を含んでいませんでした。
実際に得られたパニックメッセージは今回の場合、Guess value must be greater than or equal to 1, got 200
でした。
そうしてバグの所在地を割り出し始めることができるわけです!
Result<T, E>
をテストで使う
これまでは、失敗するとパニックするようなテストを書いてきましたが、
Result<T, E>
を使うようなテストを書くこともできます!
以下は、Listing 11-1のテストを、Result<T, E>
を使い、パニックする代わりにErr
を返すように書き直したものです:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { #[test] fn it_works() -> Result<(), String> { if 2 + 2 == 4 { Ok(()) } else { Err(String::from("two plus two does not equal four")) } } } }
it_works
関数の戻り値の型はResult<(), String>
になりました。
関数内でassert_eq!
マクロを呼び出す代わりに、テストが成功すればOk(())
を、失敗すればErr
にString
を入れて返すようにします。
Result<T, E>
を返すようなテストを書くと、?
演算子をテストの中で使えるようになります。
これは、テスト内で何らかの工程がErr
ヴァリアントを返したときに失敗するべきテストを書くのに便利です。
Result<T, E>
を使うテストに#[should_panic]
注釈を使うことはできません。
テストが失敗しなければならないときは、直接Err
値を返してください。
今やテスト記法を複数知ったので、テストを走らせる際に起きていることに目を向け、
cargo test
で使用できるいろんなオプションを探究しましょう。
テストの実行のされ方を制御する
cargo run
がコードをコンパイルし、出来上がったバイナリを走らせるのと全く同様に、
cargo test
はコードをテストモードでコンパイルし、出来上がったテストバイナリを実行します。
コマンドラインオプションを指定してcargo test
の既定動作を変更することができます。
例えば、cargo test
で生成されるバイナリの既定動作は、テストを全て並行に実行し、
テスト実行中に生成された出力をキャプチャして出力が表示されるのを防ぎ、
テスト結果に関係する出力を読みやすくすることです。
コマンドラインオプションの中にはcargo test
にかかるものや、出来上がったテストバイナリにかかるものがあります。
この2種の引数を区別するために、cargo test
にかかる引数を--
という区分記号の後に列挙し、
それからテストバイナリにかかる引数を列挙します。cargo test --help
を走らせると、cargo test
で使用できるオプションが表示され、
cargo test -- --help
を走らせると、--
という区分記号の後に使えるオプションが表示されます。
テストを並行または連続して実行する
複数のテストを実行するとき、標準では、スレッドを使用して並行に走ります。これはつまり、 テストが早く実行し終わり、コードが機能しているいかんにかかわらず、反応をより早く得られることを意味します。 テストは同時に実行されているので、テストが相互や共有された環境を含む他の共通の状態に依存してないことを確かめてください。 現在の作業対象ディレクトリや環境変数などですね。
例えば、各テストがディスクにtest_output.txtというファイルを作成し、何らかのデータを書き込むコードを走らせるとしてください。 そして、各テストはそのファイルのデータを読み取り、ファイルが特定の値を含んでいるとアサーションし、 その値は各テストで異なります。テストが同時に走るので、あるテストが、 他のテストが書き込んだり読み込んだりする間隙にファイルを上書きするかもしれません。 それから2番目のテストが失敗します。コードが不正だからではなく、 並行に実行されている間にテストがお互いに邪魔をしてしまったせいです。 各テストが異なるファイルに書き込むことを確かめるのが一つの解決策です; 別の解決策では、 一度に一つのテストを実行します。
並行にテストを実行したくなかったり、使用されるスレッド数をよりきめ細かく制御したい場合、
--test-threads
フラグと使用したいスレッド数をテストバイナリに送ることができます。
以下の例に目を向けてください:
$ cargo test -- --test-threads=1
テストスレッドの数を1
にセットし、並行性を使用しないようにプログラムに指示しています。
1スレッドのみを使用してテストを実行すると、並行に実行するより時間がかかりますが、
状態を共有していても、お互いに邪魔をすることはありません。
関数の出力を表示する
標準では、テストが通ると、Rustのテストライブラリは標準出力に出力されたものを全てキャプチャします。例えば、
テストでprintln!
を呼び出してテストが通ると、println!
の出力は、端末に表示されません;
テストが通ったことを示す行しか見られないでしょう。テストが失敗すれば、
残りの失敗メッセージと共に、標準出力に出力されたものが全て見えるでしょう。
例として、リスト11-10は引数の値を出力し、10を返す馬鹿げた関数と通過するテスト1つ、失敗するテスト1つです。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { fn prints_and_returns_10(a: i32) -> i32 { //{}という値を得た println!("I got the value {}", a); 10 } #[cfg(test)] mod tests { use super::*; #[test] fn this_test_will_pass() { let value = prints_and_returns_10(4); assert_eq!(10, value); } #[test] fn this_test_will_fail() { let value = prints_and_returns_10(8); assert_eq!(5, value); } } }
リスト11-10: println!
を呼び出す関数用のテスト
これらのテストをcargo test
で実行すると、以下のような出力を目の当たりにするでしょう:
running 2 tests
test tests::this_test_will_pass ... ok
test tests::this_test_will_fail ... FAILED
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `10`', src/lib.rs:19:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
この出力のどこにも I got the value 4
と表示されていないことに注意してください。
これは、テストに合格した場合に出力されるものです。
その出力はキャプチャされてしまいました。
失敗したテストのからの出力 I got the value 8
がテストサマリー出力のセクションに表示され、テストが失敗した原因も示されます。
通過するテストについても出力される値が見たかったら、出力キャプチャ機能を--nocapture
フラグで無効化することができます:
$ cargo test -- --nocapture
リスト11-10のテストを--nocapture
フラグと共に再度実行したら、以下のような出力を目の当たりにします:
running 2 tests
I got the value 4
I got the value 8
test tests::this_test_will_pass ... ok
thread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)`
left: `5`,
right: `10`', src/lib.rs:19:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
test tests::this_test_will_fail ... FAILED
failures:
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
テスト用の出力とテスト結果の出力がまぜこぜになっていることに注意してください;
その理由は、前節で語ったようにテストが並行に実行されているからです。
-test-threads=1
オプションと--nocapture
フラグを使ってみて、
その時、出力がどうなるか確かめてください!
名前でテストの一部を実行する
時々、全テストを実行すると時間がかかってしまうことがあります。特定の部分のコードしか対象にしていない場合、
そのコードに関わるテストのみを走らせたいかもしれません。cargo test
に走らせたいテストの名前を引数として渡すことで、
実行するテストを選ぶことができます。
テストの一部を走らせる方法を模擬するために、リスト11-11に示したように、
add_two
関数用に3つテストを作成し、走らせるテストを選択します。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub fn add_two(a: i32) -> i32 { a + 2 } #[cfg(test)] mod tests { use super::*; #[test] fn add_two_and_two() { assert_eq!(4, add_two(2)); } #[test] fn add_three_and_two() { assert_eq!(5, add_two(3)); } #[test] fn one_hundred() { assert_eq!(102, add_two(100)); } } }
リスト11-11: 異なる名前の3つのテスト
以前見かけたように、引数なしでテストを走らせたら、全テストが並行に走ります:
running 3 tests
test tests::add_two_and_two ... ok
test tests::add_three_and_two ... ok
test tests::one_hundred ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
単独のテストを走らせる
あらゆるテスト関数の名前をcargo test
に渡して、そのテストのみを実行することができます:
$ cargo test one_hundred
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/adder-06a75b4a1f2515e9
running 1 test
test tests::one_hundred ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out
one_hundred
という名前のテストだけが走りました; 他の2つのテストはその名前に合致しなかったのです。
まとめ行の最後に2 filtered out
と表示することでテスト出力は、このコマンドが走らせた以上のテストがあることを知らせてくれています。
この方法では、複数のテストの名前を指定することはできません; cargo test
に与えられた最初の値のみが使われるのです。
ですが、複数のテストを走らせる方法もあります。
複数のテストを実行するようフィルターをかける
テスト名の一部を指定でき、その値に合致するあらゆるテストが走ります。例えば、
我々のテストの2つがadd
という名前を含むので、cargo test add
を実行することで、その二つを走らせることができます:
$ cargo test add
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/adder-06a75b4a1f2515e9
running 2 tests
test tests::add_two_and_two ... ok
test tests::add_three_and_two ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
このコマンドは名前にadd
を含むテストを全て実行し、one_hundred
という名前のテストを除外しました。
また、テストが出現するモジュールがテスト名の一部になっていて、
モジュール名でフィルターをかけることで、あるモジュール内のテスト全てを実行できることに注目してください。
特に要望のない限りテストを無視する
時として、いくつかの特定のテストが実行するのに非常に時間がかかることがあり、
cargo test
の実行のほとんどで除外したくなるかもしれません。引数として確かに実行したいテストを全て列挙するのではなく、
ここに示したように代わりに時間のかかるテストをignore
属性で除外すると注釈することができます。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[test] fn it_works() { assert_eq!(2 + 2, 4); } #[test] #[ignore] fn expensive_test() { // 実行に1時間かかるコード // code that takes an hour to run } }
#[test]
の後の除外したいテストに#[ignore]
行を追加しています。これで、
テストを実行したら、it_works
は実行されるものの、expensive_test
は実行されません:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.24 secs
Running target/debug/deps/adder-ce99bcc2479f4607
running 2 tests
test expensive_test ... ignored
test it_works ... ok
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
expensive_test
関数は、ignored
と列挙されています。無視されるテストのみを実行したかったら、
cargo test -- --ignored
を使うことができます:
$ cargo test -- --ignored
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/deps/adder-ce99bcc2479f4607
running 1 test
test expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
どのテストを走らせるか制御することで、結果が早く出ることを確かめることができるのです。
ignored
テストの結果を確認することが道理に合い、結果を待つだけの時間ができたときに、
代わりにcargo test -- --ignored
を走らせることができます。
テストの体系化
章の初めで触れたように、テストは複雑な鍛錬であり、人によって専門用語や体系化が異なります。 Rustのコミュニティでは、テストを2つの大きなカテゴリで捉えています: 単体テストと結合テストです。 単体テストは小規模でより集中していて、個別に1回に1モジュールをテストし、非公開のインターフェイスもテストすることがあります。 結合テストは、完全にライブラリ外になり、他の外部コード同様に自分のコードを使用し、公開インターフェイスのみ使用し、 1テストにつき複数のモジュールを用いることもあります。
どちらのテストを書くのも、ライブラリの一部が個別かつ共同でしてほしいことをしていることを確認するのに重要なのです。
単体テスト
単体テストの目的は、残りのコードから切り離して各単位のコードをテストし、
コードが想定通り、動いたり動いていなかったりする箇所を迅速に特定することです。
単体テストは、テスト対象となるコードと共に、srcディレクトリの各ファイルに置きます。
慣習は、各ファイルにtests
という名前のモジュールを作り、テスト関数を含ませ、
そのモジュールをcfg(test)
で注釈することです。
テストモジュールと#[cfg(test)]
testsモジュールの#[cfg(test)]
という注釈は、コンパイラにcargo build
を走らせた時ではなく、cargo test
を走らせた時にだけ、
テストコードをコンパイルし走らせるよう指示します。これにより、ライブラリをビルドしたいだけの時にはコンパイルタイムを節約し、
テストが含まれないので、コンパイル後の成果物のサイズも節約します。結合テストは別のディレクトリに存在することになるので、
#[cfg(test)]
注釈は必要ないとわかるでしょう。しかしながら、単体テストはコードと同じファイルに存在するので、
#[cfg(test)]
を使用してコンパイル結果に含まれないよう指定するのです。
この章の最初の節で新しいadder
プロジェクトを生成した時に、Cargoがこのコードも生成してくれたことを思い出してください:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[cfg(test)] mod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); } } }
このコードが自動生成されたテストモジュールです。cfg
という属性は、configurationを表していて、
コンパイラに続く要素が、ある特定の設定オプションを与えられたら、含まれるように指示します。
今回の場合、設定オプションは、test
であり、言語によって提供されているテストをコンパイルし、
走らせるためのものです。cfg
属性を使用することで、cargo test
で積極的にテストを実行した場合のみ、
Cargoがテストコードをコンパイルします。これには、このモジュールに含まれるかもしれないヘルパー関数全ても含まれ、
#[test]
で注釈された関数だけにはなりません。
非公開関数をテストする
テストコミュニティ内で非公開関数を直接テストするべきかについては議論があり、
他の言語では非公開関数をテストするのは困難だったり、不可能だったりします。
あなたがどちらのテストイデオロギーを支持しているかに関わらず、Rustの公開性規則により、
非公開関数をテストすることが確かに可能です。リスト11-12の非公開関数internal_adder
を含むコードを考えてください。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub fn add_two(a: i32) -> i32 { internal_adder(a, 2) } fn internal_adder(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn internal() { assert_eq!(4, internal_adder(2, 2)); } } }
リスト11-12: 非公開関数をテストする
internal_adder
関数はpub
とマークされていないものの、テストも単なるRustのコードであり、
tests
モジュールもただのモジュールでしかないので、テスト内でinternal_adder
を普通にインポートし呼び出すことができます。
非公開関数はテストするべきではないとお考えなら、Rustにはそれを強制するものは何もありません。
結合テスト
Rustにおいて、結合テストは完全にライブラリ外のものです。他のコードと全く同様にあなたのライブラリを使用するので、 ライブラリの公開APIの一部である関数しか呼び出すことはできません。その目的は、 ライブラリのいろんな部分が共同で正常に動作しているかをテストすることです。 単体では正常に動くコードも、結合した状態だと問題を孕む可能性もあるので、 結合したコードのテストの範囲も同様に重要になるのです。結合テストを作成するには、 まずtestsディレクトリが必要になります。
testsディレクトリ
プロジェクトディレクトリのトップ階層、srcの隣にtestsディレクトリを作成します。 Cargoは、このディレクトリに結合テストのファイルを探すことを把握しています。 そして、このディレクトリ内にいくらでもテストファイルを作成することができ、 Cargoはそれぞれのファイルを個別のクレートとしてコンパイルします。
結合テストを作成しましょう。リスト11-12のコードがsrc/lib.rsファイルにあるまま、 testsディレクトリを作成し、tests/integration_test.rsという名前の新しいファイルを生成し、 リスト11-13のコードを入力してください。
ファイル名: tests/integration_test.rs
extern crate adder;
#[test]
fn it_adds_two() {
assert_eq!(4, adder::add_two(2));
}
リスト11-13: adder
クレートの関数の結合テスト
コードの頂点にextern crate adder
を追記しましたが、これは単体テストでは必要なかったものです。
理由は、tests
ディレクトリのテストはそれぞれ個別のクレートであるため、
各々ライブラリをインポートする必要があるためです。
tests/integration_test.rsのどんなコードも#[cfg(test)]
で注釈する必要はありません。
Cargoはtests
ディレクトリを特別に扱い、cargo test
を走らせた時にのみこのディレクトリのファイルをコンパイルするのです。
さあ、cargo test
を実行してください:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
Running target/debug/deps/adder-abcabcabc
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/integration_test-ce99bcc2479f4607
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
3つの区域の出力が単体テスト、結合テスト、ドックテストを含んでいます。単体テスト用の最初の区域は、
今まで見てきたものと同じです: 各単体テストに1行(リスト11-12で追加したinternal
という名前のもの)と、
単体テストのサマリー行です。
結合テストの区域は、
Running target/debug/deps/integration-test-ce99bcc2479f4607
という行で始まっています(最後のハッシュはあなたの出力とは違うでしょう)。
次に、この結合テストの各テスト関数用の行があり、Doc-tests adder
区域が始まる直前に、
結合テストの結果用のサマリー行があります。
単体テスト関数を追加することで単体テスト区域のテスト結果の行が増えたように、 作成した結合テストファイルにテスト関数を追加することでそのファイルの区域に結果の行が増えることになります。 結合テストファイルはそれぞれ独自の区域があるため、testsディレクトリにさらにファイルを追加すれば、 結合テストの区域が増えることになるでしょう。
それでも、テスト関数の名前を引数としてcargo test
に指定することで、特定の結合テスト関数を走らせることができます。
特定の結合テストファイルにあるテストを全て走らせるには、cargo test
に--test
引数、
その後にファイル名を続けて使用してください:
$ cargo test --test integration_test
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running target/debug/integration_test-952a27e0126bb565
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
このコマンドは、tests/integration_test.rsファイルにあるテストのみを実行します。
結合テスト内のサブモジュール
結合テストを追加するにつれて、testsディレクトリに2つ以上のファイルを作成して体系化したくなるかもしれません; 例えば、テスト対象となる機能でテスト関数をグループ化することができます。前述したように、 testsディレクトリの各ファイルは、個別のクレートとしてコンパイルされます。
各結合テストファイルをそれ自身のクレートとして扱うと、 エンドユーザがあなたのクレートを使用するかのように個別のスコープを生成するのに役立ちます。 ですが、これはtestsディレクトリのファイルが、コードをモジュールとファイルに分ける方法に関して第7章で学んだように、 srcのファイルとは同じ振る舞いを共有しないことを意味します。
testsディレクトリのファイルの異なる振る舞いは、複数の結合テストファイルで役に立ちそうなヘルパー関数ができ、
第7章の「モジュールを別のファイルに移動する」節の手順に従って共通モジュールに抽出しようとした時に最も気付きやすくなります。
例えば、tests/common.rsを作成し、そこにsetup
という名前の関数を配置したら、
複数のテストファイルの複数のテスト関数から呼び出したいsetup
に何らかのコードを追加することができます:
ファイル名: tests/common.rs
#![allow(unused)] fn main() { pub fn setup() { // ここにライブラリテスト固有のコードが来る // setup code specific to your library's tests would go here } }
再度テストを実行すると、common.rsファイルは何もテスト関数を含んだり、setup
関数をどこかから呼んだりしてないのに、
テスト出力にcommon.rs用の区域が見えるでしょう。
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/common-b8b07b6f1be2db70
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/integration_test-d993c68b431d39df
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
common
がrunning 0 tests
とテスト結果に表示されるのは、望んだ結果ではありません。
ただ単に他の結合テストファイルと何らかのコードを共有したかっただけです。
common
がテスト出力に出現するのを防ぐには、tests/common.rsを作成する代わりに、
tests/common/mod.rsを作成します。第7章の「モジュールファイルシステムの規則」節において、
module_name/mod.rsという命名規則をサブモジュールのあるモジュールのファイルに使用しました。
ここではcommon
にサブモジュールはありませんが、
このように命名することでコンパイラにcommon
モジュールを結合テストファイルとして扱わないように指示します。
setup
関数のコードをtests/common/mod.rsに移動し、tests/common.rsファイルを削除すると、
テスト出力に区域はもう表示されなくなります。testsディレクトリのサブディレクトリ内のファイルは個別クレートとしてコンパイルされたり、
テスト出力に区域が表示されることがないのです。
tests/common/mod.rsを作成した後、それをどの結合テストファイルからもモジュールとして使用することができます。
こちらは、tests/integration_test.rs内のit_adds_two
テストからsetup
関数を呼び出す例です:
ファイル名: tests/integration_test.rs
extern crate adder;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::add_two(2));
}
mod common;
という宣言は、リスト7-21で模擬したモジュール宣言と同じであることに注意してください。それから、テスト関数内でcommon::setup()
関数を呼び出すことができます。
バイナリクレート用の結合テスト
もしもプロジェクトがsrc/main.rsファイルのみを含み、src/lib.rsファイルを持たないバイナリクレートだったら、
testsディレクトリに結合テストを作成し、
extern crate
を使用してsrc/main.rsファイルに定義された関数をインポートすることはできません。
ライブラリクレートのみが、他のクレートが呼び出して使用できる関数を晒せるのです;
バイナリクレートはそれ単体で実行することを意味しています。
これは、バイナリを提供するRustのプロジェクトに、
src/lib.rsファイルに存在するロジックを呼び出す単純なsrc/main.rsファイルがある一因になっています。
この構造を使用して結合テストは、extern crate
を使用して重要な機能を用いることでライブラリクレートをテストすることができます。
この重要な機能が動作すれば、src/main.rsファイルの少量のコードも動作し、その少量のコードはテストする必要がないわけです。
まとめ
Rustのテスト機能は、変更を加えた後でさえ想定通りにコードが機能し続けることを保証して、 コードが機能すべき方法を指定する手段を提供します。単体テストはライブラリの異なる部分を個別に用い、 非公開の実装詳細をテストすることができます。結合テストは、ライブラリのいろんな部分が共同で正常に動作することを確認し、 ライブラリの公開APIを使用して外部コードが使用するのと同じ方法でコードをテストします。 Rustの型システムと所有権ルールにより防がれるバグの種類もあるものの、それでもテストは、 コードが振る舞うと予想される方法に関するロジックのバグを減らすのに重要なのです。
この章と以前の章で学んだ知識を結集して、とあるプロジェクトに取り掛かりましょう!
入出力プロジェクト: コマンドラインプログラムを構築する
この章は、ここまでに学んできた多くのスキルを思い出すきっかけであり、もういくつか標準ライブラリの機能も探究します。 ファイルやコマンドラインの入出力と相互作用するコマンドラインツールを構築し、 今やあなたの支配下にあるRustの概念の一部を練習していきます。
Rustの速度、安全性、単バイナリ出力、クロスプラットフォームサポートにより、コマンドラインツールを作るのにふさわしい言語なので、
このプロジェクトでは、独自の伝統的なコマンドラインツールのgrep
(globally search a regular expression
and print: 正規表現をグローバルで検索し表示する)を作成していきます。最も単純な使用法では、
grep
は指定したファイルから指定した文字列を検索します。そうするには、
grep
は引数としてファイル名と文字列を受け取ります。それからファイルを読み込んでそのファイル内で文字列引数を含む行を探し、
検索した行を出力するのです。
その過程で、多くのコマンドラインツールが使用している端末の機能を使用させる方法を示します。
環境変数の値を読み取ってユーザがこのツールの振る舞いを設定できるようにします。また、
標準出力(stdout
)の代わりに、標準エラーに出力(stderr
)するので、例えば、
ユーザはエラーメッセージは画面上で確認しつつ、成功した出力はファイルにリダイレクトできます。
Rustコミュニティのあるメンバであるアンドリュー・ガラント(Andrew Gallant)が既に全機能装備の非常に高速なgrep
、
ripgrep
と呼ばれるものを作成しました。比較対象として、我々のgrep
はとても単純ですが、
この章により、ripgrep
のような現実世界のプロジェクトを理解するのに必要な背景知識の一部を身に付けられるでしょう。
このgrep
プロジェクトは、ここまでに学んできた多くの概念を集結させます:
- コードを体系化する(モジュール、第7章で学んだことを使用)
- ベクタと文字列を使用する(コレクション、第8章)
- エラーを処理する(第9章)
- 適切な箇所でトレイトとライフタイムを使用する(第10章)
- テストを記述する(第11章)
さらに、クロージャ、イテレータ、トレイトオブジェクトなど、第13章、17章で詳しく講義するものもちょっとだけ紹介します。
コマンドライン引数を受け付ける
いつものように、cargo new
で新しいプロジェクトを作りましょう。プロジェクトをminigrep
と名付けて、
既に自分のシステムに存在するかもしれないgrep
ツールと区別しましょう。
最初の仕事は、minigrep
を二つの引数を受け付けるようにすることです: ファイル名と検索する文字列ですね。
つまり、cargo run
で検索文字列と検索を行うファイルへのパスと共にプログラムを実行できるようになりたいということです。
こんな感じにね:
$ cargo run searchstring example-filename.txt
今現在は、cargo new
で生成されたプログラムは、与えた引数を処理できません。
Crates.ioに存在する既存のライブラリには、
コマンドライン引数を受け付けるプログラムを書く手助けをしてくれるものもありますが、ちょうどこの概念を学んでいる最中なので、
この能力を自分で実装しましょう。
引数の値を読み取る
minigrep
が渡したコマンドライン引数の値を読み取れるようにするために、Rustの標準ライブラリで提供されている関数が必要になり、
それは、std::env::args
です。この関数は、minigrep
に与えられたコマンドライン引数のイテレータを返します。
イテレータについてはまだ議論していません(完全には第13章で講義します)が、とりあえずイテレータに関しては、
2つの詳細のみ知っていればいいです: イテレータは一連の値を生成することと、イテレータに対してcollect
関数を呼び出し、
イテレータが生成する要素全部を含むベクタなどのコレクションに変えられることです。
リスト12-1のコードを使用してminigrep
プログラムに渡されたあらゆるコマンドライン引数を読み取れるようにし、
それからその値をベクタとして集結させてください。
ファイル名: src/main.rs
use std::env; fn main() { let args: Vec<String> = env::args().collect(); println!("{:?}", args); }
リスト12-1: コマンドライン引数をベクタに集結させ、出力する
まず、std::env
モジュールをuse
文でスコープに導入したので、args
関数が使用できます。
std::env::args
関数は、2レベルモジュールがネストされていることに注目してください。
第7章で議論したように、希望の関数が2モジュール以上ネストされている場合、
関数ではなく親モジュールをスコープに導入するのが因習的です。そうすることで、
std::env
から別の関数も容易に使用することができます。また、
use std::env::args
を追記し、関数をargs
とするだけで呼び出すのに比べて曖昧でもありません。
というのも、args
は現在のモジュールに定義されている関数と容易に見間違えられるかもしれないからです。
args
関数と不正なユニコード引数のどれかが不正なユニコードを含んでいたら、
std::env::args
はパニックすることに注意してください。 プログラムが不正なユニコードを含む引数を受け付ける必要があるなら、代わりにstd::env::args_os
を使用してください。 この関数は、String
値ではなく、OsString
値を生成するイテレータを返します。ここでは、 簡潔性のためにstd::env::args
を使うことを選択しました。 なぜなら、OsString
値はプラットフォームごとに異なり、String
値に比べて取り扱いが煩雑だからです。
main
の最初の行でenv::args
を呼び出し、そして即座にcollect
を使用して、
イテレータをイテレータが生成する値全てを含むベクタに変換しています。
collect
関数を使用して多くの種類のコレクションを生成することができるので、
args
の型を明示的に注釈して文字列のベクタが欲しいのだと指定しています。Rustにおいて、
型を注釈しなければならない頻度は非常に少ないのですが、collect
はよく確かに注釈が必要になる一つの関数なのです。
コンパイラには、あなたが欲しているコレクションの種類が推論できないからです。
最後に、デバッグ整形機の:?
を使用してベクタを出力しています。引数なしでコードを走らせてみて、
それから引数二つで試してみましょう:
$ cargo run
--snip--
["target/debug/minigrep"]
$ cargo run needle haystack
--snip--
["target/debug/minigrep", "needle", "haystack"]
ベクタの最初の値は"target/debug/minigrep"
であることに注目してください。これはバイナリの名前です。
これはCの引数リストの振る舞いと合致し、実行時に呼び出された名前をプログラムに使わせてくれるわけです。
メッセージで出力したり、プログラムを起動するのに使用されたコマンドラインエイリアスによってプログラムの振る舞いを変えたい場合に、
プログラム名にアクセスするのにしばしば便利です。ですが、この章の目的には、これを無視し、必要な二つの引数のみを保存します。
引数の値を変数に保存する
引数のベクタの値を出力すると、プログラムはコマンドライン引数として指定された値にアクセスできることが説明されました。 さて、プログラムの残りを通して使用できるように、二つの引数の値を変数に保存する必要があります。 それをしているのがリスト12-2です。
ファイル名: src/main.rs
use std::env; fn main() { let args: Vec<String> = env::args().collect(); let query = &args[1]; let filename = &args[2]; // {}を探しています println!("Searching for {}", query); // {}というファイルの中 println!("In file {}", filename); }
リスト12-2: クエリ引数とファイル名引数を保持する変数を生成
ベクタを出力した時に確認したように、プログラム名がベクタの最初の値、args[0]
を占めているので、
添え字1
から始めます。minigrep
が取る最初の引数は、検索する文字列なので、
最初の引数への参照を変数query
に置きました。2番目の引数はファイル名でしょうから、
2番目の引数への参照は変数filename
に置きました。
一時的にこれらの変数の値を出力して、コードが意図通りに動いていることを証明しています。
再度このプログラムをtest
とsample.txt
という引数で実行しましょう:
$ cargo run test sample.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt
素晴らしい、プログラムは動作しています!必要な引数の値が、正しい変数に保存されています。後ほど、 何らかのエラー処理を加えて、ユーザが引数を提供しなかった場合など、可能性のある特定のエラー状況に対処します; 今は、そのような状況はないものとし、代わりにファイル読み取り能力を追加することに取り組みます。
ファイルを読み込む
では、filename
コマンドライン引数で指定されたファイルを読み込む機能を追加しましょう。
まず、テスト実行するためのサンプルファイルが必要ですね: minigrep
が動作していることを確かめるために使用するのに最適なファイルは、
複数行にわたって同じ単語の繰り返しのある少量のテキストです。リスト12-3は、
うまくいくであろうエミリー・ディキンソン(Emily Dickinson)の詩です!
プロジェクトのルート階層にpoem.txtというファイルを作成し、この詩「私は誰でもない!あなたは誰?」を入力してください。
ファイル名: poem.txt
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
私は誰でもない!あなたは誰?
あなたも誰でもないの?
なら、私たちは組だね、何も言わないで!
あの人たちは、私たちを追放するでしょう。わかりますよね?
誰かでいるなんて侘しいじゃない!
カエルみたいで公すぎるじゃない。
自分の名を長い1日に告げるのなんて。
感服するような沼地にね!
リスト12-3: エミリー・ディキンソンの詩は、いいテストケースになる
テキストを適当な場所に置いて、src/main.rsを編集し、ファイルを開くコードを追加してください。 リスト12-4に示したようにですね。
ファイル名: src/main.rs
use std::env; use std::fs::File; use std::io::prelude::*; fn main() { let args: Vec<String> = env::args().collect(); let query = &args[1]; let filename = &args[2]; println!("Searching for {}", query); // --snip-- println!("In file {}", filename); // ファイルが見つかりませんでした let mut f = File::open(filename).expect("file not found"); let mut contents = String::new(); f.read_to_string(&mut contents) // ファイルの読み込み中に問題がありました .expect("something went wrong reading the file"); // テキストは\n{}です println!("With text:\n{}", contents); }
リスト12-4: 第2引数で指定されたファイルの中身を読み込む
最初に、もう何個かuse
文を追記して、標準ライブラリの関係のある箇所を持ってきています:
ファイルを扱うのにstd::fs::File
が必要ですし、
std::io::prelude::*
はファイル入出力を含む入出力処理をするのに有用なトレイトを色々含んでいます。
言語が一般的な初期化処理で特定の型や関数を自動的にスコープに導入するように、
std::io
モジュールにはそれ独自の共通の型や関数の初期化処理があり、入出力を行う際に必要になるわけです。
標準の初期化処理とは異なり、std::io
の初期化処理には明示的にuse
文を加えなければなりません。
main
に3文を追記しました: 一つ目が、File::open
関数を呼んでfilename
変数の値に渡して、
ファイルへの可変なハンドルを得る処理です。二つ目が、contents
という名の変数を生成して、
可変で空のString
を割り当てる処理です。この変数が、ファイル読み込み後に中身を保持します。
三つ目が、ファイルハンドルに対してread_to_string
を呼び出し、引数としてcontents
への可変参照を渡す処理です。
それらの行の後に、今回もファイル読み込み後にcontents
の値を出力する一時的なprintln!
文を追記したので、
ここまでプログラムがきちんと動作していることを確認できます。
第1コマンドライン引数には適当な文字列(まだ検索する箇所は実装してませんからね)を、第2引数にpoem.txtファイルを入れて、 このコードを実行しましょう:
$ cargo run the poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
素晴らしい!コードがファイルの中身を読み込み、出力するようになりました。しかし、このコードにはいくつか欠陥があります。
main
関数が複数の責任を受け持っています: 一般に、各関数がただ一つの責任だけを持つようになれば、
関数は明確かつ、管理しやすくなります。もう一つの問題点は、できうる限りのエラー処理を怠っていることです。
まだプログラムが小規模なので、これらの欠陥は大きな問題にはなりませんが、プログラムが大規模になるにつれ、
それを綺麗に解消するのは困難になっていきます。プログラムを開発する際に早い段階でリファクタリングを行うのは、
良い戦術です。リファクタリングするコードの量が少なければ、はるかに簡単になりますからね。次は、それを行いましょう。
リファクタリングしてモジュール性とエラー処理を向上させる
プログラムを改善するために、プログラムの構造と起こりうるエラーに対処する方法に関連する4つの問題を修正していきましょう。
1番目は、main
関数が2つの仕事を受け持っていることです: 引数を解析し、ファイルを開いています。
このような小さな関数なら、これは、大した問題ではありませんが、main
内でプログラムを巨大化させ続けたら、
main
関数が扱う個別の仕事の数も増えていきます。関数が責任を受け持つごとに、
正しいことを確認しにくくなり、テストも行いづらくなり、機能を壊さずに変更するのも困難になっていきます。
機能を小分けして、各関数が1つの仕事のみに責任を持つようにするのが最善です。
この問題は、2番目の問題にも結びついています: query
とfilename
はプログラムの設定用変数ですが、
f
やcontents
といった変数は、プログラムのロジックを担っています。main
が長くなるほど、
スコープに入れるべき変数も増えます。そして、スコープにある変数が増えれば、各々の目的を追うのも大変になるわけです。
設定用変数を一つの構造に押し込め、目的を明瞭化するのが最善です。
3番目の問題は、ファイルを開き損ねた時にexpect
を使ってエラーメッセージを出力しているのに、
エラーメッセージがファイルが見つかりませんでした
としか表示しないことです。
ファイルを開く行為は、ファイルが存在しない以外にもいろんな方法で失敗することがあります:
例えば、ファイルは存在するかもしれないけれど、開く権限がないかもしれないなどです。
現時点では、そのような状況になった時、「ファイルが見つかりませんでした」というエラーメッセージを出力し、
これはユーザに間違った情報を与えるのです。
4番目は、異なるエラーを処理するのにexpect
を繰り返し使用しているので、ユーザが十分な数の引数を渡さずにプログラムを起動した時に、
問題を明確に説明しない「範囲外アクセス(index out of bounds)」というエラーがRustから得られることです。
エラー処理のコードが全て1箇所に存在し、将来エラー処理ロジックが変更になった時に、
メンテナンス者が1箇所のコードのみを考慮すればいいようにするのが最善でしょう。
エラー処理コードが1箇所にあれば、エンドユーザにとって意味のあるメッセージを出力していることを確認することにもつながります。
プロジェクトをリファクタリングして、これら4つの問題を扱いましょう。
バイナリプロジェクトの責任の分離
main
関数に複数の仕事の責任を割り当てるという構造上の問題は、多くのバイナリプロジェクトでありふれています。
結果として、main
が肥大化し始めた際にバイナリプログラムの個別の責任を分割するためにガイドラインとして活用できる工程をRustコミュニティは、
開発しました。この工程は、以下のような手順になっています:
- プログラムをmain.rsとlib.rsに分け、ロジックをlib.rsに移動する。
- コマンドライン引数の解析ロジックが小規模な限り、main.rsに置いても良い。
- コマンドライン引数の解析ロジックが複雑化の様相を呈し始めたら、main.rsから抽出してlib.rsに移動する。
この工程の後にmain
関数に残る責任は以下に限定される:
- 引数の値でコマンドライン引数の解析ロジックを呼び出す
- 他のあらゆる設定を行う
- lib.rsの
run
関数を呼び出す run
がエラーを返した時に処理する
このパターンは、責任の分離についてです: main.rsはプログラムの実行を行い、
そして、lib.rsが手にある仕事のロジック全てを扱います。main
関数を直接テストすることはできないので、
この構造により、プログラムのロジック全てをlib.rsの関数に移すことでテストできるようになります。
main.rsに残る唯一のコードは、読めばその正当性が評価できるだけ小規模になるでしょう。
この工程に従って、プログラムのやり直しをしましょう。
引数解析器を抽出する
引数解析の機能をmain
が呼び出す関数に抽出して、コマンドライン引数解析ロジックをsrc/lib.rsに移動する準備をします。
リスト12-5に新しい関数parse_config
を呼び出すmain
の冒頭部を示し、
この新しい関数は今だけsrc/main.rsに定義します。
ファイル名: src/main.rs
fn main() {
let args: Vec<String> = env::args().collect();
let (query, filename) = parse_config(&args);
// --snip--
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let filename = &args[2];
(query, filename)
}
リスト12-5: main
からparse_config
関数を抽出する
それでもまだ、コマンドライン引数をベクタに集結させていますが、main
関数内で引数の値の添え字1を変数query
に、
添え字2を変数filename
に代入する代わりに、ベクタ全体をparse_config
関数に渡しています。
そして、parse_config
関数にはどの引数がどの変数に入り、それらの値をmain
に返すというロジックが存在します。
まだmain
内にquery
とfilename
という変数を生成していますが、もうmain
は、
コマンドライン引数と変数がどう対応するかを決定する責任は持ちません。
このやり直しは、私たちの小規模なプログラムにはやりすぎに思えるかもしれませんが、 少しずつ段階的にリファクタリングしているのです。この変更後、プログラムを再度実行して、 引数解析がまだ動作していることを実証してください。問題が発生した時に原因を特定する助けにするために頻繁に進捗を確認するのはいいことです。
設定値をまとめる
もう少しparse_config
関数を改善することができます。現時点では、タプルを返していますが、
即座にタプルを分解して再度個別の値にしています。これは、正しい抽象化をまだできていないかもしれない兆候です。
まだ改善の余地があると示してくれる他の徴候は、parse_config
のconfig
の部分であり、
返却している二つの値は関係があり、一つの設定値の一部にどちらもなることを暗示しています。
現状では、一つのタプルにまとめていること以外、この意味をデータの構造に載せていません;
この二つの値を1構造体に置き換え、構造体のフィールドそれぞれに意味のある名前をつけることもできるでしょう。
そうすることで将来このコードのメンテナンス者が、異なる値が相互に関係する仕方や、目的を理解しやすくできるでしょう。
注釈: この複雑型(complex type)がより適切な時に組み込みの値を使うアンチパターンを、 primitive obsession(
訳注
: 初めて聞いた表現。組み込み型強迫観念といったところだろうか)と呼ぶ人もいます。
リスト12-6は、parse_config
関数の改善を示しています。
ファイル名: src/main.rs
use std::env; use std::fs::File; fn main() { let args: Vec<String> = env::args().collect(); let config = parse_config(&args); println!("Searching for {}", config.query); println!("In file {}", config.filename); let mut f = File::open(config.filename).expect("file not found"); // --snip-- } struct Config { query: String, filename: String, } fn parse_config(args: &[String]) -> Config { let query = args[1].clone(); let filename = args[2].clone(); Config { query, filename } }
リスト12-6: parse_config
をリファクタリングしてConfig
構造体のインスタンスを返す
query
とfilename
というフィールドを持つよう定義されたConfig
という構造体を追加しました。
parse_config
のシグニチャは、これでConfig
値を返すと示すようになりました。parse_config
の本体では、
以前はargs
のString
値を参照する文字列スライスを返していましたが、
今では所有するString
値を含むようにConfig
を定義しています。main
のargs
変数は引数値の所有者であり、
parse_config
関数だけに借用させていますが、これはConfig
がargs
の値の所有権を奪おうとしたら、
Rustの借用規則に違反してしまうことを意味します。
String
のデータは、多くの異なる手法で管理できますが、最も単純だけれどもどこか非効率的な手段は、
値に対してclone
メソッドを呼び出すことです。これにより、Config
インスタンスが所有するデータの総コピーが生成されるので、
文字列データへの参照を保持するよりも時間とメモリを消費します。ですが、データをクローンすることで、
コードがとても素直にもなります。というのも、参照のライフタイムを管理する必要がないからです。
つまり、この場面において、少々のパフォーマンスを犠牲にして単純性を得るのは、価値のある代償です。
clone
を使用する代償実行時コストのために
clone
を使用して所有権問題を解消するのを避ける傾向が多くのRustaceanにあります。 第13章で、この種の状況においてより効率的なメソッドの使用法を学ぶでしょう。ですがとりあえずは、 これらのコピーをするのは1回だけですし、ファイル名とクエリ文字列は非常に小さなものなので、 いくつかの文字列をコピーして進捗するのは良しとしましょう。最初の通り道でコードを究極的に効率化しようとするよりも、 ちょっと非効率的でも動くプログラムを用意する方がいいでしょう。もっとRustの経験を積めば、 最も効率的な解決法から開始することも簡単になるでしょうが、今は、clone
を呼び出すことは完璧に受け入れられることです。
main
を更新したので、parse_config
から返されたConfig
のインスタンスをconfig
という変数に置くようになり、
以前は個別のquery
とfilename
変数を使用していたコードを更新したので、代わりにConfig
構造体のフィールドを使用するようになりました。
これでコードはquery
とfilename
が関連していることと、その目的がプログラムの振る舞い方を設定するということをより明確に伝えます。
これらの値を使用するあらゆるコードは、config
インスタンスの目的の名前を冠したフィールドにそれらを発見することを把握しています。
Config
のコンストラクタを作成する
ここまでで、コマンドライン引数を解析する責任を負ったロジックをmain
から抽出し、parse_config
関数に配置しました。
そうすることでquery
とfilename
の値が関連し、その関係性がコードに載っていることを確認する助けになりました。
それからConfig
構造体を追加してquery
とfilename
の関係する目的を名前付けし、
構造体のフィールド名としてparse_config
関数からその値の名前を返すことができています。
したがって、今やparse_config
関数の目的はConfig
インスタンスを生成することになったので、
parse_config
をただの関数からConfig
構造体に紐づくnew
という関数に変えることができます。
この変更を行うことで、コードがより慣用的になります。String
などの標準ライブラリの型のインスタンスを、
String::new
を呼び出すことで生成できます。同様に、parse_config
をConfig
に紐づくnew
関数に変えれば、
Config::new
を呼び出すことでConfig
のインスタンスを生成できるようになります。リスト12-7が、
行う必要のある変更を示しています。
ファイル名: src/main.rs
use std::env; fn main() { let args: Vec<String> = env::args().collect(); let config = Config::new(&args); // --snip-- } struct Config { query: String, filename: String, } // --snip-- impl Config { fn new(args: &[String]) -> Config { let query = args[1].clone(); let filename = args[2].clone(); Config { query, filename } } }
リスト12-7: parse_config
をConfig::new
に変える
parse_config
を呼び出していたmain
を代わりにConfig::new
を呼び出すように更新しました。
parse_config
の名前をnew
に変え、impl
ブロックに入れ込んだので、new
関数とConfig
が紐づくようになりました。
再度このコードをコンパイルしてみて、動作することを確かめてください。
エラー処理を修正する
さて、エラー処理の修正に取り掛かりましょう。ベクタが2個以下の要素しか含んでいないときにargs
ベクタの添え字1か2にアクセスしようとすると、
プログラムがパニックすることを思い出してください。試しに引数なしでプログラムを実行してください。すると、こんな感じになります:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep`
thread 'main' panicked at 'index out of bounds: the len is 1
but the index is 1', src/main.rs:29:21
(スレッド'main'は、「境界外アクセス: 長さは1なのに添え字も1です」でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.
境界外アクセス: 長さは1なのに添え字も1です
という行は、プログラマ向けのエラーメッセージです。
エンドユーザが起きたことと代わりにすべきことを理解する手助けにはならないでしょう。これを今修正しましょう。
エラーメッセージを改善する
リスト12-8で、new
関数に、添え字1と2にアクセスする前にスライスが十分長いことを実証するチェックを追加しています。
スライスの長さが十分でなければ、プログラムはパニックし、境界外インデックス
よりもいいエラーメッセージを表示します。
ファイル名: src/main.rs
// --snip--
fn new(args: &[String]) -> Config {
if args.len() < 3 {
// 引数の数が足りません
panic!("not enough arguments");
}
// --snip--
リスト12-8: 引数の数のチェックを追加する
このコードは、リスト9-9で記述したvalue
引数が正常な値の範囲外だった時にpanic!
を呼び出したGuess::new
関数と似ています。
ここでは、値の範囲を確かめる代わりに、args
の長さが少なくとも3であることを確かめていて、
関数の残りの部分は、この条件が満たされているという前提のもとで処理を行うことができます。
args
に2要素以下しかなければ、この条件は真になり、panic!
マクロを呼び出して、即座にプログラムを終了させます。
では、new
のこの追加の数行がある状態で、再度引数なしでプログラムを走らせ、エラーがどんな見た目か確かめましょう:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep`
thread 'main' panicked at 'not enough arguments', src/main.rs:30:12
(スレッド'main'は「引数が足りません」でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.
この出力の方がマシです: これでエラーメッセージが合理的になりました。ですが、
ユーザに与えたくない追加の情報も含まれてしまっています。おそらく、
ここではリスト9-9で使用したテクニックを使用するのは最善ではありません:
panic!
の呼び出しは、第9章で議論したように、使用の問題よりもプログラミング上の問題により適しています。
代わりに、第9章で学んだもう一つのテクニックを使用することができます。成功か失敗かを示唆するResult
を返すことです。
panic!
を呼び出す代わりにnew
からResult
を返す
代わりに、成功時にはConfig
インスタンスを含み、エラー時には問題に言及するResult
値を返すことができます。
Config::new
がmain
と対話する時、Result
型を使用して問題があったと信号を送ることができます。
それからmain
を変更して、panic!
呼び出しが引き起こしていたthread 'main'
とRUST_BACKTRACE
に関する周囲のテキストがない、
ユーザ向けのより実用的なエラーにErr
列挙子を変換することができます。
リスト12-9は、Config::new
の戻り値に必要な変更とResult
を返すのに必要な関数の本体を示しています。
main
も更新するまで、これはコンパイルできないことに注意してください。その更新は次のリストで行います。
ファイル名: src/main.rs
impl Config {
fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
Ok(Config { query, filename })
}
}
リスト12-9: Config::new
からResult
を返却する
new
関数は、これで、成功時にはConfig
インスタンスを、エラー時には&'static str
を伴うResult
を返すようになりました。
第10章の「静的ライフタイム」節から&'static str
は文字列リテラルの型であることを思い出してください。
これは、今はエラーメッセージの型になっています。
new
関数の本体で2つ変更を行いました: 十分な数の引数をユーザが渡さなかった場合にpanic!
を呼び出す代わりに、
今はErr
値を返し、Config
戻り値をOk
に包んでいます。これらの変更により、関数が新しい型シグニチャに適合するわけです。
Config::new
からErr
値を返すことにより、main
関数は、new
関数から返ってくるResult
値を処理し、
エラー時により綺麗にプロセスから抜け出すことができます。
Config::new
を呼び出し、エラーを処理する
エラーケースを処理し、ユーザフレンドリーなメッセージを出力するために、main
を更新して、
リスト12-10に示したようにConfig::new
から返されているResult
を処理する必要があります。
また、panic!
からコマンドラインツールを0以外のエラーコードで抜け出す責任も奪い取り、
手作業でそれも実装します。0以外の終了コードは、
我々のプログラムを呼び出したプロセスにプログラムがエラー状態で終了したことを通知する慣習です。
ファイル名: src/main.rs
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
// 引数解析時に問題
println!("Problem parsing arguments: {}", err);
process::exit(1);
});
// --snip--
リスト12-10: 新しいConfig
作成に失敗したら、エラーコードで終了する
このリストにおいて、以前には講義していないメソッドを使用しました: unwrap_or_else
です。
これは標準ライブラリでResult<T, E>
に定義されています。unwrap_or_else
を使うことで、
panic!
ではない何らか独自のエラー処理を定義できるのです。このResult
がOk
値だったら、
このメソッドの振る舞いはunwrap
に似ています: Ok
が包んでいる中身の値を返すのです。
しかし、値がErr
値なら、このメソッドは、クロージャ内でコードを呼び出し、
クロージャは私たちが定義し、引数としてunwrap_or_else
に渡す匿名関数です。クロージャについては第13章で詳しく講義します。
とりあえず、unwrap_or_else
は、今回リスト12-9で追加したnot enough arguments
という静的文字列のErr
の中身を、
縦棒の間に出現するerr
引数のクロージャに渡していることだけ知っておく必要があります。
クロージャのコードはそれから、実行された時にerr
値を使用できます。
新規use
行を追加して標準ライブラリからprocess
をインポートしました。クロージャ内のエラー時に走るコードは、
たった2行です: err
の値を出力し、それからprocess::exit
を呼び出します。process::exit
関数は、
即座にプログラムを停止させ、渡された数字を終了コードとして返します。これは、リスト12-8で使用したpanic!
ベースの処理と似ていますが、
もう余計な出力はされません。試しましょう:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.48 secs
Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments
素晴らしい!この出力の方が遥かにユーザに優しいです。
main
からロジックを抽出する
これで設定解析のリファクタリングが終了したので、プログラムのロジックに目を向けましょう。
「バイナリプロジェクトの責任の分離」で述べたように、
現在main
関数に存在する設定のセットアップやエラー処理に関わらない全てのロジックを保持することになるrun
という関数を抽出します。
やり終わったら、main
は簡潔かつ視察で確かめやすくなり、他のロジック全部に対してテストを書くことができるでしょう。
リスト12-11は、抜き出したrun
関数を示しています。今は少しずつ段階的に関数を抽出する改善を行っています。
それでも、src/main.rsに関数を定義していきます。
ファイル名: src/main.rs
fn main() {
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.filename);
run(config);
}
fn run(config: Config) {
let mut f = File::open(config.filename).expect("file not found");
let mut contents = String::new();
f.read_to_string(&mut contents)
.expect("something went wrong reading the file");
println!("With text:\n{}", contents);
}
// --snip--
リスト12-11: 残りのプログラムロジックを含むrun
関数を抽出する
これでrun
関数は、ファイル読み込みから始まるmain
関数の残りのロジック全てを含むようになりました。
このrun
関数は、引数にConfig
インスタンスを取ります。
run
関数からエラーを返す
残りのプログラムロジックがrun
関数に隔離されたので、リスト12-9のConfig::new
のように、
エラー処理を改善することができます。expect
を呼び出してプログラムにパニックさせる代わりに、
run
関数は、何か問題が起きた時にResult<T, E>
を返します。これにより、
さらにエラー処理周りのロジックをユーザに優しい形でmain
に統合することができます。
リスト12-12にシグニチャとrun
本体に必要な変更を示しています。
ファイル名: src/main.rs
use std::error::Error;
// --snip--
fn run(config: Config) -> Result<(), Box<Error>> {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
println!("With text:\n{}", contents);
Ok(())
}
リスト12-12: run
関数を変更してResult
を返す
ここでは、3つの大きな変更を行いました。まず、run
関数の戻り値をResult<(), Box<Error>>
に変えました。
この関数は、以前はユニット型、()
を返していて、それをOk
の場合に返される値として残しました。
エラー型については、トレイトオブジェクトのBox<Error>
を使用しました(同時に冒頭でuse
文により、
std::error::Error
をスコープに導入しています)。トレイトオブジェクトについては、第17章で講義します。
とりあえず、Box<Error>
は、関数がError
トレイトを実装する型を返すことを意味しますが、
戻り値の型を具体的に指定しなくても良いことを知っておいてください。これにより、
エラーケースによって異なる型のエラー値を返す柔軟性を得ます。
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<Error>> {
// --snip--
}
リスト12-13: Config
とrun
をsrc/lib.rsに移動する
ここでは、寛大にpub
を使用しています: Config
のフィールドとnew
メソッドとrun
関数です。
これでテスト可能な公開APIのあるライブラリクレートができました!
さて、src/lib.rsに移動したコードをsrc/main.rsのバイナリクレートのスコープに持っていく必要があります。 リスト12-14に示したようにですね。
ファイル名: src/main.rs
extern crate minigrep;
use std::env;
use std::process;
use minigrep::Config;
fn main() {
// --snip--
if let Err(e) = minigrep::run(config) {
// --snip--
}
}
リスト12-14: minigrep
クレートをsrc/main.rsのスコープに持っていく
ライブラリクレートをバイナリクレートに持っていくのに、extern crate minigrep
を使用しています。
それからuse minigrep::Config
行を追加してConfig
型をスコープに持ってきて、
run
関数にクレート名を接頭辞として付けます。これで全機能が連結され、動くはずです。
cargo run
でプログラムを走らせて、すべてがうまくいっていることを確かめてください。
ふう!作業量が多かったですね。ですが、将来成功する準備はできています。 もう、エラー処理は遥かに楽になり、コードのモジュール化もできました。 ここから先の作業は、ほぼsrc/lib.rsで完結するでしょう。
古いコードでは大変だけれども、新しいコードでは楽なことをして新発見のモジュール性を活用しましょう: テストを書くのです!
テスト駆動開発でライブラリの機能を開発する
今や、ロジックをsrc/lib.rsに抜き出し、引数集めとエラー処理をsrc/main.rsに残したので、
コードの核となる機能のテストを書くのが非常に容易になりました。いろんな引数で関数を直接呼び出し、
コマンドラインからバイナリを呼び出す必要なく戻り値を確認できます。ご自由にConfig::new
やrun
関数の機能のテストは、
ご自身でお書きください。
この節では、テスト駆動開発(TDD)過程を活用してminigrep
プログラムに検索ロジックを追加します。
このソフトウェア開発テクニックは、以下の手順に従います:
- 失敗するテストを書き、走らせて想定通りの理由で失敗することを確かめる。
- 十分な量のコードを書くか変更して新しいテストを通過するようにする。
- 追加または変更したばかりのコードをリファクタリングし、テストが通り続けることを確認する。
- 手順1から繰り返す!
この過程は、ソフトウェアを書く多くの方法のうちの一つに過ぎませんが、TDDによりコードデザインも駆動することができます。 テストを通過させるコードを書く前にテストを書くことで、過程を通して高いテストカバー率を保つ助けになります。
実際にクエリ文字列の検索を行う機能の実装をテスト駆動し、クエリに合致する行のリストを生成します。
この機能をsearch
という関数に追加しましょう。
失敗するテストを記述する
もう必要ないので、プログラムの振る舞いを確認していたprintln!
文をsrc/lib.rsとsrc/main.rsから削除しましょう。
それからsrc/lib.rsで、テスト関数のあるtest
モジュールを追加します。第11章のようにですね。
このテスト関数がsearch
関数に欲しい振る舞いを指定します: クエリとそれを検索するテキストを受け取り、
クエリを含む行だけをテキストから返します。リスト12-15にこのテストを示していますが、まだコンパイルは通りません。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } #[cfg(test)] mod test { use super::*; #[test] fn one_result() { let query = "duct"; // Rustは // 安全で速く生産性も高い。 // 3つ選んで。 let contents = "\ Rust: safe, fast, productive. Pick three."; assert_eq!( vec!["safe, fast, productive."], search(query, contents) ); } } }
リスト12-15: こうだったらいいなというsearch
関数の失敗するテストを作成する
このテストは、"duct"
という文字列を検索します。検索対象の文字列は3行で、うち1行だけが"duct"
を含みます。
search
関数から返る値が想定している行だけを含むことをアサーションします。
このテストを走らせ、失敗するところを観察することはできません。このテストはコンパイルもできないからです:
まだsearch
関数が存在していません!ゆえに今度は、空のベクタを常に返すsearch
関数の定義を追加することで、
テストをコンパイルし走らせるだけのコードを追記します。リスト12-16に示したようにですね。そうすれば、
テストはコンパイルでき、失敗するはずです。なぜなら、空のベクタは、
"safe, fast, productive."
という行を含むベクタとは合致しないからです。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } }
リスト12-16: テストがコンパイルできるのに十分なだけsearch
関数を定義する
明示的なライフタイムの'a
がsearch
のシグニチャで定義され、contents
引数と戻り値で使用されていることに注目してください。
第10章からライフタイム仮引数は、どの実引数のライフタイムが戻り値のライフタイムに関連づけられているかを指定することを思い出してください。
この場合、返却されるベクタは、
(query
引数ではなく)contents
引数のスライスを参照する文字列スライスを含むべきと示唆しています。
言い換えると、コンパイラにsearch
関数に返されるデータは、
search
関数にcontents
引数で渡されているデータと同期間生きることを教えています。
これは重要なことです!スライスに参照されるデータは、参照が有効になるために有効である必要があるのです;
コンパイラがcontents
ではなくquery
の文字列スライスを生成すると想定してしまったら、
安全性チェックを間違って行うことになってしまいます。
ライフタイム注釈を忘れてこの関数をコンパイルしようとすると、こんなエラーが出ます:
error[E0106]: missing lifetime specifier
(エラー: ライフタイム指定子が欠けています)
--> src/lib.rs:5:51
|
5 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
| ^ expected lifetime
parameter
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `query` or `contents`
(助言: この関数の戻り値は、借用された値を含んでいますが、シグニチャにはそれが、
`query`か`contents`から借用されたものであるかが示されていません)
コンパイラには、二つの引数のどちらが必要なのか知る由がないので、教えてあげる必要があるのです。
contents
がテキストを全て含む引数で、合致するそのテキストの一部を返したいので、
contents
がライフタイム記法で戻り値に関連づくはずの引数であることをプログラマは知っています。
他のプログラミング言語では、シグニチャで引数と戻り値を関連づける必要はありません。これは奇妙に思えるかもしれませんが、 時間とともに楽になっていきます。この例を第10章、「ライフタイムで参照を有効化する」節と比較したくなるかもしれません。
さあ、テストを実行しましょう:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
--warnings--
Finished dev [unoptimized + debuginfo] target(s) in 0.43 secs
Running target/debug/deps/minigrep-abcabcabc
running 1 test
test test::one_result ... FAILED
failures:
---- test::one_result stdout ----
thread 'test::one_result' panicked at 'assertion failed: `(left ==
right)`
left: `["safe, fast, productive."]`,
right: `[]`)', src/lib.rs:48:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
test::one_result
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
error: test failed, to rerun pass '--lib'
素晴らしい。テストは全く想定通りに失敗しています。テストが通るようにしましょう!
テストを通過させるコードを書く
空のベクタを常に返しているために、現状テストは失敗しています。それを修正し、search
を実装するには、
プログラムは以下の手順に従う必要があります:
- 中身を各行ごとに繰り返す。
- 行にクエリ文字列が含まれるか確認する。
- するなら、それを返却する値のリストに追加する。
- しないなら、何もしない。
- 一致する結果のリストを返す。
各行を繰り返す作業から、この手順に順に取り掛かりましょう。
lines
メソッドで各行を繰り返す
Rustには、文字列を行ごとに繰り返す役立つメソッドがあり、利便性のためにlines
と名付けられ、
リスト12-17のように動作します。まだ、これはコンパイルできないことに注意してください。
ファイル名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// 行に対して何かする
// do something with line
}
}
リスト12-17: contents
の各行を繰り返す
lines
メソッドはイテレータを返します。イテレータについて詳しくは、第13章で話しますが、
リスト3-5でこのようなイテレータの使用法は見かけたことを思い出してください。
そこでは、イテレータにfor
ループを使用してコレクションの各要素に対して何らかのコードを走らせていました。
クエリを求めて各行を検索する
次に現在の行がクエリ文字列を含むか確認します。幸運なことに、
文字列にはこれを行ってくれるcontains
という役に立つメソッドがあります!search
関数に、
contains
メソッドの呼び出しを追加してください。リスト12-18のようにですね。
それでもまだコンパイルできないことに注意してください。
ファイル名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(query) {
// do something with line
}
}
}
リスト12-18: 行がquery
の文字列を含むか確認する機能を追加する
合致した行を保存する
また、クエリ文字列を含む行を保存する方法が必要です。そのために、for
ループの前に可変なベクタを生成し、
push
メソッドを呼び出してline
をベクタに保存することができます。for
ループの後でベクタを返却します。
リスト12-19のようにですね。
ファイル名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
リスト12-19: 合致する行を保存したので、返すことができる
これでsearch
関数は、query
を含む行だけを返すはずであり、テストも通るはずです。
テストを実行しましょう:
$ cargo test
--snip--
running 1 test
test test::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
テストが通り、動いていることがわかりました!
ここで、テストが通過するよう保ったまま、同じ機能を保持しながら、検索関数の実装をリファクタリングする機会を考えることもできます。 検索関数のコードは悪すぎるわけではありませんが、イテレータの有用な機能の一部を活用していません。 この例には第13章で再度触れ、そこでは、イテレータをより深く探究し、さらに改善する方法に目を向けます。
run
関数内でsearch
関数を使用する
search
関数が動きテストできたので、run
関数からsearch
を呼び出す必要があります。config.query
の値と、
ファイルからrun
が読み込むcontents
の値をsearch
関数に渡す必要があります。
それからrun
は、search
から返ってきた各行を出力するでしょう:
ファイル名: src/lib.rs
pub fn run(config: Config) -> Result<(), Box<Error>> {
let mut f = File::open(config.filename)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
for line in search(&config.query, &contents) {
println!("{}", line);
}
Ok(())
}
それでもfor
ループでsearch
から各行を返し、出力しています。
さて、プログラム全体が動くはずです!試してみましょう。まずはエミリー・ディキンソンの詩から、 ちょうど1行だけを返すはずの言葉から。"frog"です:
$ cargo run frog poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.38 secs
Running `target/debug/minigrep frog poem.txt`
How public, like a frog
かっこいい!今度は、複数行にマッチするであろう言葉を試しましょう。"body"とかね:
$ cargo run body poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep body poem.txt`
I’m nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!
そして最後に、詩のどこにも現れない単語を探したときに、何も出力がないことを確かめましょう。 "monomorphization"などね:
$ cargo run monomorphization poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep monomorphization poem.txt`
最高です!古典的なツールの独自のミニバージョンを構築し、アプリケーションを構造化する方法を多く学びました。 また、ファイル入出力、ライフタイム、テスト、コマンドライン引数の解析についても、少し学びました。
このプロジェクトをまとめ上げるために、環境変数を扱う方法と標準エラー出力に出力する方法を少しだけデモします。 これらはどちらも、コマンドラインプログラムを書く際に有用です。
環境変数を取り扱う
おまけの機能を追加してminigrep
を改善します: 環境変数でユーザがオンにできる大文字小文字無視の検索用のオプションです。
この機能をコマンドラインオプションにして、適用したい度にユーザが入力しなければならないようにすることもできますが、
代わりに環境変数を使用します。そうすることでユーザは1回環境変数をセットすれば、そのターミナルセッションの間は、
大文字小文字無視の検索を行うことができるようになるわけです。
大文字小文字を区別しないsearch
関数用に失敗するテストを書く
環境変数がオンの場合に呼び出すsearch_case_insensitive
関数を新しく追加したいです。テスト駆動開発の過程に従い続けるので、
最初の手順は、今回も失敗するテストを書くことです。新しいsearch_case_insensitive
関数用の新規テストを追加し、
古いテストをone_result
からcase_sensitive
に名前変更して、二つのテストの差異を明確化します。
リスト12-20に示したようにですね。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[cfg(test)] mod test { use super::*; #[test] fn case_sensitive() { let query = "duct"; // Rust // 安全かつ高速で生産的 // 三つを選んで // ガムテープ let contents = "\ Rust: safe, fast, productive. Pick three. Duct tape."; assert_eq!( vec!["safe, fast, productive."], search(query, contents) ); } #[test] fn case_insensitive() { let query = "rUsT"; // (最後の行のみ) // 私を信じて let contents = "\ Rust: safe, fast, productive. Pick three. Trust me."; assert_eq!( vec!["Rust:", "Trust me."], search_case_insensitive(query, contents) ); } } }
リスト12-20: 追加しようとしている大文字小文字を区別しない関数用の失敗するテストを新しく追加する
古いテストのcontents
も変更していることに注意してください。大文字小文字を区別する検索を行う際に、
"duct"
というクエリに合致しないはずの大文字Dを使用した"Duct tape"
(ガムテープ)という新しい行を追加しました。
このように古いテストを変更することで、既に実装済みの大文字小文字を区別する検索機能を誤って壊してしまわないことを保証する助けになります。
このテストはもう通り、大文字小文字を区別しない検索に取り掛かっても通り続けるはずです。
大文字小文字を区別しない検索の新しいテストは、クエリに"rUsT"を使用しています。
追加直前のsearch_case_insensitive
関数では、"rUsT"というクエリは、
両方ともクエリとは大文字小文字が異なるのに、大文字Rの"Rust:"を含む行と、
“Trust me.”
という行にもマッチするはずです。これが失敗するテストであり、まだsearch_case_insensitive
関数を定義していないので、
コンパイルは失敗するでしょう。リスト12-16のsearch
関数で行ったのと同様に空のベクタを常に返すような仮実装を追加し、テストがコンパイルされるものの、失敗する様をご自由に確認してください。
search_case_insensitive
関数を実装する
search_case_insensitive
関数は、リスト12-21に示しましたが、search
関数とほぼ同じです。
唯一の違いは、query
と各line
を小文字化していることなので、入力引数の大文字小文字によらず、
行がクエリを含んでいるか確認する際には、同じになるわけです。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let query = query.to_lowercase(); let mut results = Vec::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line); } } results } }
リスト12-21: 比較する前にクエリと行を小文字化するよう、search_case_insensitive
関数を定義する
まず、query
文字列を小文字化し、同じ名前の覆い隠された変数に保存します。ユーザのクエリが"rust"
や"RUST"
、
"Rust"
、"rUsT"
などだったりしても、"rust"
であり、大文字小文字を区別しないかのようにクエリを扱えるように、
to_lowercase
をクエリに対して呼び出すことは必須です。
query
は最早、文字列スライスではなくString
であることに注意してください。というのも、
to_lowercase
を呼び出すと、既存のデータを参照するというよりも、新しいデータを作成するからです。
例として、クエリは"rUsT"
だとしましょう: その文字列スライスは、小文字のu
やt
を使えるように含んでいないので、
"rust"
を含む新しいString
のメモリを確保しなければならないのです。今、contains
メソッドに引数としてquery
を渡すと、
アンド記号を追加する必要があります。contains
のシグニチャは、文字列スライスを取るよう定義されているからです。
次に、各line
がquery
を含むか確かめる前にto_lowercase
の呼び出しを追加し、全文字を小文字化しています。
今やline
とquery
を小文字に変換したので、クエリが大文字であろうと小文字であろうとマッチを検索するでしょう。
この実装がテストを通過するか確認しましょう:
running 2 tests
test test::case_insensitive ... ok
test test::case_sensitive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
素晴らしい!どちらも通りました。では、run
関数から新しいsearch_case_insensitive
関数を呼び出しましょう。
1番目に大文字小文字の区別を切り替えられるよう、Config
構造体に設定オプションを追加します。
まだどこでも、このフィールドの初期化をしていないので、追加するとコンパイルエラーが起きます:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub struct Config { pub query: String, pub filename: String, pub case_sensitive: bool, } }
論理値を持つcase_sensitive
フィールドを追加したことに注意してください。次に、run
関数に、
case_sensitive
フィールドの値を確認し、search
関数かsearch_case_insensitive
関数を呼ぶかを決定するのに使ってもらう必要があります。
リスト12-22のようにですね。それでも、これはまだコンパイルできないことに注意してください。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::error::Error; use std::fs::File; use std::io::prelude::*; fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } pub fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } pub struct Config { query: String, filename: String, case_sensitive: bool, } pub fn run(config: Config) -> Result<(), Box<Error>> { let mut f = File::open(config.filename)?; let mut contents = String::new(); f.read_to_string(&mut contents)?; let results = if config.case_sensitive { search(&config.query, &contents) } else { search_case_insensitive(&config.query, &contents) }; for line in results { println!("{}", line); } Ok(()) } }
リスト12-22: config.case_sensitive
の値に基づいてsearch
かsearch_case_insensitive
を呼び出す
最後に、環境変数を確認する必要があります。環境変数を扱う関数は、標準ライブラリのenv
モジュールにあるので、
use std::env;
行でsrc/lib.rsの冒頭でそのモジュールをスコープに持ってくる必要があります。そして、
env
モジュールからvar
関数を使用してCASE_INSENSITIVE
という環境変数のチェックを行います。
リスト12-23のようにですね。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::env; struct Config { query: String, filename: String, case_sensitive: bool, } // --snip-- impl Config { pub fn new(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let filename = args[2].clone(); let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); Ok(Config { query, filename, case_sensitive }) } } }
リスト12-23: CASE_INSENSITIVE
という環境変数のチェックを行う
ここで、case_sensitive
という新しい変数を生成しています。その値をセットするために、
env::var
関数を呼び出し、CASE_INSENSITIVE
環境変数の名前を渡しています。env::var
関数は、
環境変数がセットされていたら、環境変数の値を含むOk
列挙子の成功値になるResult
を返します。
環境変数がセットされていなければ、Err
列挙子を返すでしょう。
Result
のis_err
メソッドを使用して、エラーでありゆえに、セットされていないことを確認しています。
これは大文字小文字を区別する検索をすべきことを意味します。CASE_INSENSITIVE
環境変数が何かにセットされていれば、
is_err
はfalseを返し、プログラムは大文字小文字を区別しない検索を実行するでしょう。環境変数の値はどうでもよく、
セットされているかどうかだけ気にするので、unwrap
やexpect
あるいは、他のここまで見かけたResult
のメソッドではなく、
is_err
をチェックしています。
case_sensitive
変数の値をConfig
インスタンスに渡しているので、リスト12-22で実装したように、
run
関数はその値を読み取り、search
かsearch_case_insensitive
を呼び出すか決定できるのです。
試行してみましょう!まず、環境変数をセットせずにクエリはto
でプログラムを実行し、
この時は全て小文字で"to"という言葉を含むあらゆる行が合致するはずです。
$ cargo run to poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
まだ機能しているようです!では、CASE_INSENSITIVE
を1にしつつ、同じクエリのto
でプログラムを実行しましょう。
PowerShellを使用しているなら、1コマンドではなく、2コマンドで環境変数をセットし、プログラムを実行する必要があるでしょう:
$ $env:CASE_INSENSITIVE=1
$ cargo run to poem.txt
大文字も含む可能性のある"to"を含有する行が得られるはずです:
$ CASE_INSENSITIVE=1 cargo run to poem.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
素晴らしい、"To"を含む行も出てきましたね!minigrep
プログラムはこれで、
環境変数によって制御できる大文字小文字を区別しない検索も行えるようになりました。もうコマンドライン引数か、
環境変数を使ってオプションを管理する方法も知りましたね。
引数と環境変数で同じ設定を行うことができるプログラムもあります。そのような場合、 プログラムはどちらが優先されるか決定します。自身の別の鍛錬として、コマンドライン引数か、 環境変数で大文字小文字の区別を制御できるようにしてみてください。 片方は大文字小文字を区別するようにセットされ、もう片方は区別しないようにセットしてプログラムが実行された時に、 コマンドライン引数と環境変数のどちらの優先度が高くなるかを決めてください。
std::env
モジュールは、環境変数を扱うもっと多くの有用な機能を有しています:
ドキュメンテーションを確認して、何が利用可能か確かめてください。
標準出力ではなく標準エラーにエラーメッセージを書き込む
現時点では、すべての出力をprintln!
関数を使用して端末に書き込んでいます。多くの端末は、
2種類の出力を提供します: 普通の情報用の標準出力(stdout
)とエラーメッセージ用の標準エラー出力(stderr
)です。
この差異のおかげで、ユーザは、エラーメッセージを画面に表示しつつ、
プログラムの成功した出力をファイルにリダイレクトすることを選択できます。
println!
関数は、標準出力に出力する能力しかないので、標準エラーに出力するには他のものを使用しなければなりません。
エラーが書き込まれる場所を確認する
まず、minigrep
に出力される中身が、代わりに標準エラーに書き込みたいいかなるエラーメッセージも含め、
どのように標準出力に書き込まれているかを観察しましょう。意図的にエラーを起こしつつ、
ファイルに標準出力ストリームをリダイレクトすることでそうします。標準エラーストリームはリダイレクトしないので、
標準エラーに送られる内容は、すべて画面に表示され続けます。
コマンドラインプログラムは、エラーメッセージを標準エラー出力に送信していると期待されているので、 標準出力ストリームをファイルにリダイレクトしても、画面にエラーメッセージが見られます。 我々のプログラムは、現状、いい振る舞いをしていません: 代わりにファイルにエラーメッセージ出力を保存するところを、 目撃するところです!
この動作をデモする方法は、>
と標準出力ストリームをリダイレクトする先のファイル名、output.txtでプログラムを走らせることによります。
引数は何も渡さず、そうするとエラーが起きるはずです:
$ cargo run > output.txt
>
記法により、標準出力の中身を画面の代わりにoutput.txtに書き込むようシェルは指示されます。
画面に出力されると期待していたエラーメッセージは見られないので、ファイルに入っているということでしょう。
以下がoutput.txtが含んでいる内容です:
Problem parsing arguments: not enough arguments
そうです。エラーメッセージは標準出力に出力されているのです。このようなエラーメッセージは標準エラーに出力され、 成功した状態のデータのみがファイルに残ると遥かに有用です。それを変更します。
エラーを標準エラーに出力する
リスト12-24のコードを使用して、エラーメッセージの出力の仕方を変更します。この章の前で行ったリファクタリングのため、
エラーメッセージを出力するコードはすべて1関数、main
にあります。標準ライブラリは、
標準エラーストリームに出力するeprintln!
マクロを提供しているので、
println!
を呼び出してエラーを出力していた2箇所を代わりにeprintln!
を使うように変更しましょう。
ファイル名: src/main.rs
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
if let Err(e) = minigrep::run(config) {
eprintln!("Application error: {}", e);
process::exit(1);
}
}
リスト12-24: eprintln!
を使って標準出力ではなく、標準エラーにエラーメッセージを書き込む
println!
をeprintln!
に変えてから、再度同じようにプログラムを実行しましょう。
引数なしかつ、標準出力を>
でリダイレクトしてね:
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
これで、エラーは画面に見えつつ、output.txtは何も含まなくなり、これはコマンドラインプログラムに期待する動作です。
再度、標準出力をファイルにリダイレクトしてエラーは起こさない引数でプログラムを走らせましょう。以下のようにですね:
$ cargo run to poem.txt > output.txt
ターミナルには出力は見られず、output.txtに結果が含まれます:
ファイル名: output.txt
Are you nobody, too?
How dreary to be somebody!
これは、もう成功した出力には標準出力を、エラー出力には標準エラーを適切に使用していることをデモしています。
まとめ
この章では、ここまでに学んできた主要な概念の一部を念押しし、Rustで入出力処理を行う方法を講義しました。
コマンドライン引数、ファイル、環境変数、そしてエラー出力にeprintln!
マクロを使用することで、
もう、コマンドラインアプリケーションを書く準備ができています。以前の章の概念を使用することで、
コードはうまく体系化され、適切なデータ構造に効率的にデータを保存し、エラーをうまく扱い、
よくテストされるでしょう。
次は、関数型言語に影響されたRust機能を一部探究します: クロージャとイテレータです。
関数型言語の機能: イテレータとクロージャ
Rustの設計は、多くの既存の言語やテクニックにインスピレーションを得ていて、 その一つの大きな影響が関数型プログラミングです。関数型でのプログラミングには、しばしば、 引数で渡したり、関数から関数を返したり、関数を後ほど使用するために変数に代入することで関数を値として使用することが含まれます。
この章では、関数型プログラミングがどんなものであったり、なかったりするかという問題については議論しませんが、 代わりに関数型とよく言及される多くの言語の機能に似たRustの機能の一部について議論しましょう。
具体的には、以下を講義します:
- クロージャ、変数に保存できる関数に似た文法要素
- イテレータ、一連の要素を処理する方法
- これら2つの機能を使用して第12章の入出力プロジェクトを改善する方法
- これら2つの機能のパフォーマンス(ネタバレ: 思ったよりも速いです)
パターンマッチングやenumなど、他のRustの機能も関数型に影響されていますが、他の章で講義してきました。 クロージャとイテレータをマスターすることは、慣用的で速いRustコードを書くことの重要な部分なので、 この章を丸ごと捧げます。
クロージャ: 環境をキャプチャできる匿名関数
Rustのクロージャは、変数に保存したり、引数として他の関数に渡すことのできる匿名関数です。 ある場所でクロージャを生成し、それから別の文脈でクロージャを呼び出して評価することができます。 関数と異なり、呼び出されたスコープの値をクロージャは、キャプチャすることができます。 これらのクロージャの機能がコードの再利用や、動作のカスタマイズを行わせてくれる方法を模擬しましょう。
クロージャで動作の抽象化を行う
クロージャを保存して後々使用できるようにするのが有用な場面の例に取り掛かりましょう。その過程で、 クロージャの記法、型推論、トレイトについて語ります。
以下のような架空の場面を考えてください: カスタマイズされたエクササイズのトレーニングプランを生成するアプリを作るスタートアップで働くことになりました。 バックエンドはRustで記述され、トレーニングプランを生成するアルゴリズムは、アプリユーザの年齢や、 BMI、運動の好み、最近のトレーニング、指定された強弱値などの多くの要因を考慮します。 実際に使用されるアルゴリズムは、この例では重要ではありません; 重要なのは、この計算が数秒要することです。 必要なときだけこのアルゴリズムを呼び出し、1回だけ呼び出したいので、必要以上にユーザを待たせないことになります。
リスト13-1に示したsimulated_expensive_calculation
関数でこの仮定のアルゴリズムを呼び出すことをシミュレートし、
この関数はcalculating slowly
と出力し、2秒待ってから、渡した数値をなんでも返します。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; fn simulated_expensive_calculation(intensity: u32) -> u32 { // ゆっくり計算します println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); intensity } }
リスト13-1: 実行に約2秒かかる架空の計算の代役を務める関数
次は、この例で重要なトレーニングアプリの部分を含むmain
関数です。この関数は、
ユーザがトレーニングプランを要求した時にアプリが呼び出すコードを表します。
アプリのフロントエンドと相互作用する部分は、クロージャの使用と関係ないので、プログラムへの入力を表す値をハードコードし、
その出力を表示します。
必要な入力は以下の通りです:
- ユーザの強弱値、これはユーザがトレーニングを要求して、低強度のトレーニングか、 高強度のトレーニングがしたいかを示したときに指定されます。
- 乱数、これはトレーニングプランにバリエーションを起こします。
出力は、推奨されるトレーニングプランになります。リスト13-2は使用するmain
関数を示しています。
ファイル名: src/main.rs
fn main() { let simulated_user_specified_value = 10; let simulated_random_number = 7; generate_workout( simulated_user_specified_value, simulated_random_number ); } fn generate_workout(intensity: u32, random_number: u32) {}
リスト13-2: ユーザ入力や乱数生成をシミュレートするハードコードされた値があるmain
関数
簡潔性のために、変数simulated_user_specified_value
は10、変数simulated_random_number
は7とハードコードしました;
実際のプログラムにおいては、強弱値はアプリのフロントエンドから取得し、乱数の生成には、第2章の数当てゲームの例のように、rand
クレートを使用するでしょう。
main
関数は、シミュレートされた入力値とともにgenerate_workout
関数を呼び出します。
今や文脈ができたので、アルゴリズムに取り掛かりましょう。リスト13-3のgenerate_workout
関数は、
この例で最も気にかかるアプリのビジネスロジックを含んでいます。この例での残りの変更は、
この関数に対して行われるでしょう:
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; fn simulated_expensive_calculation(num: u32) -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num } fn generate_workout(intensity: u32, random_number: u32) { if intensity < 25 { println!( // 今日は{}回腕立て伏せをしてください! "Today, do {} pushups!", simulated_expensive_calculation(intensity) ); println!( // 次に、{}回腹筋をしてください! "Next, do {} situps!", simulated_expensive_calculation(intensity) ); } else { if random_number == 3 { // 今日は休憩してください!水分補給を忘れずに! println!("Take a break today! Remember to stay hydrated!"); } else { println!( // 今日は、{}分間走ってください! "Today, run for {} minutes!", simulated_expensive_calculation(intensity) ); } } } }
リスト13-3: 入力に基づいてトレーニングプランを出力するビジネスロジックと、
simulated_expensive_calculation
関数の呼び出し
リスト13-3のコードには、遅い計算を行う関数への呼び出しが複数あります。最初のif
ブロックが、
simulated_expensive_calculation
を2回呼び出し、外側のelse
内のif
は全く呼び出さず、
2番目のelse
ケースの内側にあるコードは1回呼び出しています。
generate_workout
関数の期待される振る舞いは、まずユーザが低強度のトレーニング(25より小さい数値で表される)か、
高強度のトレーニング(25以上の数値)を欲しているか確認することです。
低強度のトレーニングプランは、シミュレーションしている複雑なアルゴリズムに基づいて、 多くの腕立て伏せや腹筋運動を推奨してきます。
ユーザが高強度のトレーニングを欲していれば、追加のロジックがあります: アプリが生成した乱数がたまたま3なら、 アプリは休憩と水分補給を勧めます。そうでなければ、ユーザは複雑なアルゴリズムに基づいて数分間のランニングをします。
このコードは現在、ビジネスのほしいままに動くでしょうが、データサイエンスチームが、
simulated_expensive_calculation
関数を呼び出す方法に何らかの変更を加える必要があると決定したとしましょう。
そのような変更が起きた時に更新を簡略化するため、simulated_expensive_calculation
関数を1回だけ呼び出すように、
このコードをリファクタリングしたいです。また、その過程でその関数への呼び出しを増やすことなく無駄に2回、
この関数を現時点で呼んでいるところを切り捨てたくもあります。要するに、結果が必要なければ関数を呼び出したくなく、
それでも1回だけ呼び出したいのです。
関数でリファクタリング
多くの方法でトレーニングプログラムを再構築することもできます。
1番目にsimulated_expensive_calculation
関数への重複した呼び出しを変数に抽出しようとしましょう。リスト13-4に示したように。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; fn simulated_expensive_calculation(num: u32) -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num } fn generate_workout(intensity: u32, random_number: u32) { let expensive_result = simulated_expensive_calculation(intensity); if intensity < 25 { println!( "Today, do {} pushups!", expensive_result ); println!( "Next, do {} situps!", expensive_result ); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_result ); } } } }
リスト13-4: 複数のsimulated_expensive_calculation
の呼び出しを1箇所に抽出し、
結果をexpensive_result
変数に保存する
この変更によりsimulated_expensive_calculation
の呼び出しが単一化され、
最初のif
ブロックが無駄に関数を2回呼んでいた問題を解決します。不幸なことに、これでは、
あらゆる場合にこの関数を呼び出し、その結果を待つことになり、結果値を全く使用しない内側のif
ブロックでもそうしてしまいます。
プログラムの1箇所でコードを定義したいですが、結果が本当に必要なところでだけコードを実行します。 これは、クロージャのユースケースです!
クロージャでリファクタリングして、コードを保存する
if
ブロックの前にいつもsimulated_expensive_calculation
関数を呼び出す代わりに、
クロージャを定義し、関数呼び出しの結果を保存するのではなく、そのクロージャを変数に保存できます。リスト13-5のようにですね。
simulated_expensive_calculation
の本体全体を実際に、ここで導入しているクロージャ内に移すことができます。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; let expensive_closure = |num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; expensive_closure(5); }
リスト13-5: クロージャを定義し、expensive_closure
変数に保存する
クロージャ定義が=
に続き、変数expensive_closure
に代入されています。クロージャを定義するには、
1組の縦棒から始め、その内部にクロージャの仮引数を指定します; この記法は、SmalltalkやRubyのクロージャ定義と類似していることから、
選択されました。このクロージャには、num
という引数が1つあります: 2つ以上引数があるなら、
|param1, param2|
のように、カンマで区切ります。
引数の後に、クロージャの本体を保持する波括弧を配置します(これはクロージャ本体が式一つなら省略可能です)。
波括弧の後、クロージャのお尻には、セミコロンが必要で、let
文を完成させます。クロージャ本体の最後の行から返る値(num
)が、
呼び出された時にクロージャから返る値になります。その行がセミコロンで終わっていないからです;
ちょうど関数の本体みたいですね。
このlet
文は、expensive_closure
が、匿名関数を呼び出した結果の値ではなく、
匿名関数の定義を含むことを意味することに注意してください。コードを定義して、
1箇所で呼び出し、そのコードを保存し、後々、それを呼び出したいがためにクロージャを使用していることを思い出してください;
呼び出したいコードは、現在、expensive_closure
に保存されています。
クロージャが定義されたので、if
ブロックのコードを変更して、そのコードを実行するクロージャを呼び出し、結果値を得ることができます。
クロージャは、関数のように呼び出せます: クロージャ定義を含む変数名を指定し、使用したい引数値を含むかっこを続けます。
リスト13-6に示したようにですね。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; fn generate_workout(intensity: u32, random_number: u32) { let expensive_closure = |num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; if intensity < 25 { println!( "Today, do {} pushups!", expensive_closure(intensity) ); println!( "Next, do {} situps!", expensive_closure(intensity) ); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_closure(intensity) ); } } } }
リスト13-6: 定義したexpensive_closure
を呼び出す
今では、重い計算はたった1箇所でのみ呼び出され、その結果が必要なコードを実行するだけになりました。
ところが、リスト13-3の問題の一つを再浮上させてしまいました: それでも、最初のif
ブロックでクロージャを2回呼んでいて、
そうすると、重いコードを2回呼び出し、必要な分の2倍ユーザを待たせてしまいます。そのif
ブロックのみに属する変数を生成して、
クロージャの呼び出し結果を保持するそのif
ブロックに固有の変数を生成することでこの問題を解消することもできますが、
クロージャは他の解決法も用意してくれます。その解決策については、もう少し先で語りましょう。でもまずは、
クロージャ定義に型注釈がない理由とクロージャに関わるトレイトについて話しましょう。
クロージャの型推論と注釈
クロージャでは、fn
関数のように引数の型や戻り値の型を注釈する必要はありません。関数では、
型注釈は必要です。ユーザに露出する明示的なインターフェイスの一部だからです。このインターフェイスを堅実に定義することは、
関数が使用したり、返したりする値の型についてみんなが合意していることを保証するために重要なのです。
しかし、クロージャはこのような露出するインターフェイスには使用されません: 変数に保存され、
名前付けしたり、ライブラリの使用者に晒されることなく、使用されます。
クロージャは通常短く、あらゆる任意の筋書きではなく、狭い文脈でのみ関係します。 このような限定された文脈内では、コンパイラは、多くの変数の型を推論できるのと似たように、 引数や戻り値の型を頼もしく推論することができます。
このような小さく匿名の関数で型をプログラマに注釈させることは煩わしいし、コンパイラがすでに利用可能な情報と大きく被っています。
本当に必要な以上に冗長になることと引き換えに、明示性と明瞭性を向上させたいなら、変数に型注釈を加えることもできます; リスト13-5で定義したクロージャに型を注釈するなら、リスト13-7に示した定義のようになるでしょう。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; let expensive_closure = |num: u32| -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; }
リスト13-7: クロージャの引数と戻り値の省略可能な型注釈を追加する
型注釈を付け加えると、クロージャの記法は、関数の記法により酷似して見えます。以下が、引数に1を加える関数の定義と、 同じ振る舞いをするクロージャの定義の記法を縦に比べたものです。 空白を追加して、関連のある部分を並べています。これにより、縦棒の使用と省略可能な記法の量を除いて、 クロージャ記法が関数記法に似ているところを説明しています。
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
1行目が関数定義を示し、2行目がフルに注釈したクロージャ定義を示しています。 3行目は、クロージャ定義から型注釈を取り除き、4行目は、かっこを取り除いていて、 かっこはクロージャの本体がただ1つの式からなるので、省略可能です。これらは全て、 呼び出された時に同じ振る舞いになる合法な定義です。
クロージャ定義には、引数それぞれと戻り値に対して推論される具体的な型が一つあります。例えば、
リスト13-8に引数として受け取った値を返すだけの短いクロージャの定義を示しました。
このクロージャは、この例での目的以外には有用ではありません。この定義には、
何も型注釈を加えていないことに注意してください: それから1回目にString
を引数に、
2回目にu32
を引数に使用してこのクロージャを2回呼び出そうとしたら、エラーになります。
ファイル名: src/main.rs
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
リスト13-8: 2つの異なる型で型が推論されるクロージャの呼び出しを試みる
コンパイラは、次のエラーを返します:
error[E0308]: mismatched types
--> src/main.rs
|
| let n = example_closure(5);
| ^ expected struct `std::string::String`, found
integral variable
|
= note: expected type `std::string::String`
found type `{integer}`
String
値でexample_closure
を呼び出した最初の時点で、コンパイラはx
とクロージャの戻り値の型をString
と推論します。
そして、その型がexample_closure
のクロージャに閉じ込められ、同じクロージャを異なる型で使用しようとすると、
型エラーが出るのです。
ジェネリック引数とFn
トレイトを使用してクロージャを保存する
トレーニング生成アプリに戻りましょう。リスト13-6において、まだコードは必要以上の回数、重い計算のクロージャを呼んでいました。 この問題を解決する一つの選択肢は、重いクロージャの結果を再利用できるように変数に保存し、クロージャを再度呼ぶ代わりに、 結果が必要になる箇所それぞれでその変数を使用することです。しかしながら、この方法は同じコードを大量に繰り返す可能性があります。
運のいいことに、別の解決策もあります。クロージャやクロージャの呼び出し結果の値を保持する構造体を作れるのです。 結果の値が必要な場合のみにその構造体はクロージャを実行し、その結果の値をキャッシュするので、残りのコードは、 結果を保存し、再利用する責任を負わなくて済むのです。このパターンは、メモ化(memoization)または、 遅延評価(lazy evaluation)として知っているかもしれません。
クロージャを保持する構造体を作成するために、クロージャの型を指定する必要があります。 構造体定義は、各フィールドの型を把握しておく必要がありますからね。各クロージャインスタンスには、 独自の匿名の型があります: つまり、たとえ2つのクロージャが全く同じシグニチャでも、その型はそれでも違うものと考えられるということです。 クロージャを使用する構造体、enum、関数引数を定義するには、第10章で議論したように、 ジェネリクスとトレイト境界を使用します。
Fn
トレイトは、標準ライブラリで用意されています。全てのクロージャは、以下のいずれかのトレイトを実装しています:
Fn
、FnMut
または、FnOnce
です。「クロージャで環境をキャプチャする」節で、これらのトレイト間の差異を議論します;
この例では、Fn
トレイトを使えます。
Fn
トレイト境界にいくつかの型を追加することで、このトレイト境界に合致するクロージャが持つべき引数と戻り値の型を示します。
今回のクロージャはu32
型の引数を一つ取り、u32
を返すので、指定するトレイト境界はFn(u32) -> u32
になります。
リスト13-9は、クロージャとオプションの結果値を保持するCacher
構造体の定義を示しています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { struct Cacher<T> where T: Fn(u32) -> u32 { calculation: T, value: Option<u32>, } }
リスト13-9: クロージャをcalculation
に、オプションの結果値をvalue
に保持するCacher
構造体を定義する
Cacher
構造体は、ジェネリックな型T
のcalculation
フィールドを持ちます。T
のトレイト境界は、
Fn
トレイトを使うことでクロージャであると指定しています。calculation
フィールドに保存したいクロージャは全て、
1つのu32
引数(Fn
の後の括弧内で指定されている)を取り、u32
(->
の後に指定されている)を返さなければなりません。
注釈: 関数も3つの
Fn
トレイト全部を実装します。もし環境から値をキャプチャする必要がなければ、Fn
トレイトを実装する何かが必要になるクロージャではなく、関数を使用できます。
value
フィールドの型は、Option<u32>
です。クロージャを実行する前に、value
はNone
になるでしょう。
Cacher
を使用するコードがクロージャの結果を求めてきたら、その時点でCacher
はクロージャを実行し、
その結果をvalue
フィールドのSome
列挙子に保存します。それから、コードが再度クロージャの結果を求めたら、
クロージャを再実行するのではなく、Cacher
はSome
列挙子に保持された結果を返すでしょう。
たった今解説したvalue
フィールド周りのロジックは、リスト13-10で定義されています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { struct Cacher<T> where T: Fn(u32) -> u32 { calculation: T, value: Option<u32>, } impl<T> Cacher<T> where T: Fn(u32) -> u32 { fn new(calculation: T) -> Cacher<T> { Cacher { calculation, value: None, } } fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { let v = (self.calculation)(arg); self.value = Some(v); v }, } } } }
リスト13-10: Cacher
のキャッシュ機構
呼び出し元のコードにこれらのフィールドの値を直接変えてもらうのではなく、Cacher
に構造体のフィールドの値を管理してほしいので、
これらのフィールドは非公開になっています。
Cacher::new
関数はジェネリックな引数のT
を取り、Cacher
構造体と同じトレイト境界を持つよう定義しました。
それからcalculation
フィールドに指定されたクロージャと、
value
フィールドにNone
値を保持するCacher
インスタンスをCacher::new
は返します。
まだクロージャを実行していないからですね。
呼び出し元のコードがクロージャの評価結果を必要としたら、クロージャを直接呼ぶ代わりに、value
メソッドを呼びます。
このメソッドは、結果の値がself.value
のSome
に既にあるかどうか確認します; そうなら、
クロージャを再度実行することなくSome
内の値を返します。
self.value
がNone
なら、コードはself.calculation
に保存されたクロージャを呼び出し、
結果を将来使えるようにself.value
に保存し、その値を返しもします。
リスト13-11は、リスト13-6の関数generate_workout
でこのCacher
構造体を使用する方法を示しています。
ファイル名: src/main.rs
#![allow(unused)] fn main() { use std::thread; use std::time::Duration; struct Cacher<T> where T: Fn(u32) -> u32 { calculation: T, value: Option<u32>, } impl<T> Cacher<T> where T: Fn(u32) -> u32 { fn new(calculation: T) -> Cacher<T> { Cacher { calculation, value: None, } } fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { let v = (self.calculation)(arg); self.value = Some(v); v }, } } } fn generate_workout(intensity: u32, random_number: u32) { let mut expensive_result = Cacher::new(|num| { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }); if intensity < 25 { println!( "Today, do {} pushups!", expensive_result.value(intensity) ); println!( "Next, do {} situps!", expensive_result.value(intensity) ); } else { if random_number == 3 { println!("Take a break today! Remember to stay hydrated!"); } else { println!( "Today, run for {} minutes!", expensive_result.value(intensity) ); } } } }
リスト13-11: generate_workout
関数内でCacher
を使用し、キャッシュ機構を抽象化する
クロージャを変数に直接保存する代わりに、クロージャを保持するCacher
の新規インスタンスを保存しています。
そして、結果が必要な場所それぞれで、そのCacher
インスタンスに対してvalue
メソッドを呼び出しています。
必要なだけvalue
メソッドを呼び出したり、全く呼び出さないこともでき、重い計算は最大でも1回しか走りません。
リスト13-2のmain
関数とともにこのプログラムを走らせてみてください。
simulated_user_specified_value
とsimulated_random_number
変数の値を変えて、
いろんなif
やelse
ブロックの場合全てで、calculating slowly
は1回だけ、必要な時にのみ出現することを実証してください。
必要以上に重い計算を呼び出さないことを保証するのに必要なロジックの面倒をCacher
は見るので、
generate_workout
はビジネスロジックに集中できるのです。
Cacher
実装の限界
値をキャッシュすることは、コードの他の部分でも異なるクロージャで行いたくなる可能性のある一般的に有用な振る舞いです。
しかし、現在のCacher
の実装には、他の文脈で再利用することを困難にしてしまう問題が2つあります。
1番目の問題は、Cacher
インスタンスが、常にvalue
メソッドの引数arg
に対して同じ値になると想定していることです。
言い換えると、Cacher
のこのテストは、失敗するでしょう:
#[test]
fn call_with_different_values() {
let mut c = Cacher::new(|a| a);
let v1 = c.value(1);
let v2 = c.value(2);
assert_eq!(v2, 2);
}
このテストは、渡された値を返すクロージャを伴うCacher
インスタンスを新しく生成しています。
このCacher
インスタンスに対して1というarg
値で呼び出し、それから2というarg
値で呼び出し、
2というarg
値のvalue
呼び出しは2を返すべきと期待しています。
このテストをリスト13-9とリスト13-10のCacher
実装で動かすと、assert_eq
からこんなメッセージが出て、
テストは失敗します:
thread 'call_with_different_values' panicked at 'assertion failed: `(left == right)`
left: `1`,
right: `2`', src/main.rs
問題は、初めてc.value
を1で呼び出した時に、Cacher
インスタンスはself.value
にSome(1)
を保存したことです。
その後value
メソッドに何を渡しても、常に1を返すわけです。
単独の値ではなく、ハッシュマップを保持するようにCacher
を改変してみてください。ハッシュマップのキーは、
渡されるarg
値になり、ハッシュマップの値は、そのキーでクロージャを呼び出した結果になるでしょう。
self.value
が直接Some
かNone
値であることを調べる代わりに、value
関数はハッシュマップのarg
を調べ、
存在するならその値を返します。存在しないなら、Cacher
はクロージャを呼び出し、
arg
値に紐づけてハッシュマップに結果の値を保存します。
現在のCacher
実装の2番目の問題は、引数の型にu32
を一つ取り、u32
を返すクロージャしか受け付けないことです。
例えば、文字列スライスを取り、usize
を返すクロージャの結果をキャッシュしたくなるかもしれません。
この問題を修正するには、Cacher
機能の柔軟性を向上させるためによりジェネリックな引数を導入してみてください。
クロージャで環境をキャプチャする
トレーニング生成の例においては、クロージャをインラインの匿名関数として使っただけでした。しかし、 クロージャには、関数にはない追加の能力があります: 環境をキャプチャし、 自分が定義されたスコープの変数にアクセスできるのです。
リスト13-12は、equal_to_x
変数に保持されたクロージャを囲む環境からx
変数を使用するクロージャの例です。
ファイル名: src/main.rs
fn main() { let x = 4; let equal_to_x = |z| z == x; let y = 4; assert!(equal_to_x(y)); }
リスト13-12: 内包するスコープの変数を参照するクロージャの例
ここで、x
はequal_to_x
の引数でもないのに、
equal_to_x
が定義されているのと同じスコープで定義されているx
変数をequal_to_x
クロージャは使用できています。
同じことを関数では行うことができません; 以下の例で試したら、コードはコンパイルできません:
ファイル名: src/main.rs
fn main() {
let x = 4;
fn equal_to_x(z: i32) -> bool { z == x }
let y = 4;
assert!(equal_to_x(y));
}
エラーが出ます:
error[E0434]: can't capture dynamic environment in a fn item; use the || { ...
} closure form instead
(エラー: fn要素では動的な環境をキャプチャできません; 代わりに|| { ... }のクロージャ形式を
使用してください)
--> src/main.rs
|
4 | fn equal_to_x(z: i32) -> bool { z == x }
| ^
コンパイラは、この形式はクロージャでのみ動作することさえも思い出させてくれています!
クロージャが環境から値をキャプチャすると、メモリを使用してクロージャ本体で使用できるようにその値を保存します。 このメモリ使用は、環境をキャプチャしないコードを実行するようなもっと一般的な場合には払いたくないオーバーヘッドです。 関数は、絶対に環境をキャプチャすることが許可されていないので、関数を定義して使えば、このオーバーヘッドを招くことは絶対にありません。
クロージャは、3つの方法で環境から値をキャプチャでき、この方法は関数が引数を取れる3つの方法に直に対応します:
所有権を奪う、可変で借用する、不変で借用するです。これらは、以下のように3つのFn
トレイトでコード化されています:
FnOnce
は、クロージャの環境として知られている内包されたスコープからキャプチャした変数を消費します。 キャプチャした変数を消費するために、定義された際にクロージャはこれらの変数の所有権を奪い、 自身にムーブするのです。名前のうち、Once
の部分は、 このクロージャは同じ変数の所有権を2回以上奪うことができないという事実を表しているので、1回しか呼ぶことができないのです。FnMut
は、可変で値を借用するので、環境を変更することができます。Fn
は、環境から値を不変で借用します。
クロージャを生成する時、クロージャが環境を使用する方法に基づいて、コンパイラはどのトレイトを使用するか推論します。
少なくとも1回は呼び出されるので、全てのクロージャはFnOnce
を実装しています。キャプチャした変数をムーブしないクロージャは、
FnMut
も実装し、キャプチャした変数に可変でアクセスする必要のないクロージャは、Fn
も実装しています。
リスト13-12では、equal_to_x
クロージャはx
を不変で借用しています(ゆえにequal_to_x
はFn
トレイトです)。
クロージャの本体は、x
を読む必要しかないからです。
環境でクロージャが使用している値の所有権を奪うことをクロージャに強制したいなら、引数リストの前にmove
キーワードを使用できます。
このテクニックは、新しいスレッドにデータが所有されるように、クロージャを新しいスレッドに渡して、
データをムーブする際に大概は有用です。
並行性について語る第16章で、move
クロージャの例はもっと多く出てきます。とりあえず、
こちらがmove
キーワードがクロージャ定義に追加され、整数の代わりにベクタを使用するリスト13-12からのコードです。
整数はムーブではなく、コピーされてしまいますからね; このコードはまだコンパイルできないことに注意してください。
ファイル名: src/main.rs
fn main() {
let x = vec![1, 2, 3];
let equal_to_x = move |z| z == x;
// ここでは、xを使用できません: {:?}
println!("can't use x here: {:?}", x);
let y = vec![1, 2, 3];
assert!(equal_to_x(y));
}
以下のようなエラーを受けます:
error[E0382]: use of moved value: `x`
(エラー: ムーブされた値の使用: `x`)
--> src/main.rs:6:40
|
4 | let equal_to_x = move |z| z == x;
| -------- value moved (into closure) here
(値はここで(クロージャに)ムーブされた)
5 |
6 | println!("can't use x here: {:?}", x);
| ^ value used here after move
(ムーブ後、値はここで使用された)
|
= note: move occurs because `x` has type `std::vec::Vec<i32>`, which does not
implement the `Copy` trait
(注釈: `x`が`std::vec::Vec<i32>`という`Copy`トレイトを実装しない型のため、ムーブが起きました)
クロージャが定義された際に、クロージャにx
の値はムーブされています。move
キーワードを追加したからです。
そして、クロージャはx
の所有権を持ち、main
がprintln!
でx
を使うことはもう叶わないのです。
println!
を取り除けば、この例は修正されます。
Fn
トレイトのどれかを指定するほとんどの場合、Fn
から始めると、コンパイラがクロージャ本体内で起こっていることにより、
FnMut
やFnOnce
が必要な場合、教えてくれるでしょう。
環境をキャプチャできるクロージャが関数の引数として有用な場面を説明するために、次のトピックに移りましょう: イテレータです。
一連の要素をイテレータで処理する
イテレータパターンにより、一連の要素に順番に何らかの作業を行うことができます。イテレータは、 各要素を繰り返し、シーケンスが終わったことを決定するロジックの責任を負います。イテレータを使用すると、 自身でそのロジックを再実装する必要がなくなるのです。
Rustにおいて、イテレータは怠惰です。つまり、イテレータを使い込んで消費するメソッドを呼ぶまで何の効果もないということです。
例えば、リスト13-13のコードは、Vec<T>
に定義されたiter
メソッドを呼ぶことでv1
ベクタの要素に対するイテレータを生成しています。
このコード単独では、何も有用なことはしません。
#![allow(unused)] fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); }
リスト13-13: イテレータを生成する
一旦イテレータを生成したら、いろんな手段で使用することができます。第3章のリスト3-5では、
ここまでiter
の呼び出しが何をするかごまかしてきましたが、for
ループでイテレータを使い、
各要素に何かコードを実行しています。
リスト13-14の例は、イテレータの生成とfor
ループでイテレータを使用することを区別しています。
イテレータは、v1_iter
変数に保存され、その時には繰り返しは起きていません。v1_iter
のイテレータで、
for
ループが呼び出された時に、イテレータの各要素がループの繰り返しで使用され、各値が出力されます。
#![allow(unused)] fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { // {}でした println!("Got: {}", val); } }
リスト13-14: for
ループでイテレータを使用する
標準ライブラリにより提供されるイテレータが存在しない言語では、変数を添え字0から始め、 その変数でベクタに添え字アクセスして値を得て、ベクタの総要素数に到達するまでループでその変数の値をインクリメントすることで、 この同じ機能を書く可能性が高いでしょう。
イテレータはそのロジック全てを処理してくれるので、めちゃくちゃにしてしまう可能性のあるコードの繰り返しを減らしてくれます。 イテレータにより、添え字を使えるデータ構造、ベクタなどだけではなく、多くの異なるシーケンスに対して同じロジックを使う柔軟性も得られます。 イテレータがそれをする方法を調査しましょう。
Iterator
トレイトとnext
メソッド
全てのイテレータは、標準ライブラリで定義されているIterator
というトレイトを実装しています。
このトレイトの定義は、以下のようになっています:
#![allow(unused)] fn main() { pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // デフォルト実装のあるメソッドは省略 // methods with default implementations elided } }
この定義は新しい記法を使用していることに注目してください: type Item
とSelf::Item
で、
これらはこのトレイトとの関連型(associated type)を定義しています。関連型についての詳細は、第19章で語ります。
とりあえず、知っておく必要があることは、このコードがIterator
トレイトを実装するには、Item
型も定義する必要があり、
そして、このItem
型がnext
メソッドの戻り値の型に使われていると述べていることです。換言すれば、
Item
型がイテレータから返ってくる型になるだろうということです。
Iterator
トレイトは、一つのメソッドを定義することを実装者に要求することだけします: next
メソッドで、
これは1度にSome
に包まれたイテレータの1要素を返し、繰り返しが終わったら、None
を返します。
イテレータに対して直接next
メソッドを呼び出すこともできます; リスト13-15は、
ベクタから生成されたイテレータのnext
を繰り返し呼び出した時にどんな値が返るかを模擬しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[test] fn iterator_demonstration() { let v1 = vec![1, 2, 3]; let mut v1_iter = v1.iter(); assert_eq!(v1_iter.next(), Some(&1)); assert_eq!(v1_iter.next(), Some(&2)); assert_eq!(v1_iter.next(), Some(&3)); assert_eq!(v1_iter.next(), None); } }
リスト13-15: イテレータに対してnext
メソッドを呼び出す
v1_iter
を可変にする必要があったことに注目してください: イテレータのnext
メソッドを呼び出すと、
今シーケンスのどこにいるかを追いかけるためにイテレータが使用している内部の状態が変わります。
つまり、このコードはイテレータを消費、または使い込むのです。
next
の各呼び出しは、イテレータの要素を一つ、食います。for
ループを使用した時には、
v1_iter
を可変にする必要はありませんでした。というのも、ループがv1_iter
の所有権を奪い、
陰で可変にしていたからです。
また、next
の呼び出しで得られる値は、ベクタの値への不変な参照であることにも注目してください。
iter
メソッドは、不変参照へのイテレータを生成します。v1
の所有権を奪い、所有された値を返すイテレータを生成したいなら、
iter
ではなくinto_iter
を呼び出すことができます。同様に、可変参照を繰り返したいなら、
iter
ではなくiter_mut
を呼び出せます。
イテレータを消費するメソッド
Iterator
トレイトには、標準ライブラリが提供してくれているデフォルト実装のある多くの異なるメソッドがあります;
Iterator
トレイトの標準ライブラリのAPIドキュメントを検索することで、これらのメソッドについて知ることができます。
これらのメソッドの中には、定義内でnext
メソッドを呼ぶものもあり、故にIterator
トレイトを実装する際には、
next
メソッドを実装する必要があるのです。
next
を呼び出すメソッドは、消費アダプタ(consuming adaptors)と呼ばれます。呼び出しがイテレータの使い込みになるからです。
一例は、sum
メソッドで、これはイテレータの所有権を奪い、next
を繰り返し呼び出すことで要素を繰り返し、
故にイテレータを消費するのです。繰り返しが進むごとに、各要素を一時的な合計に追加し、
繰り返しが完了したら、その合計を返します。リスト13-16は、sum
の使用を説明したテストです:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[test] fn iterator_sum() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); let total: i32 = v1_iter.sum(); assert_eq!(total, 6); } }
リスト13-16: sum
メソッドを呼び出してイテレータの全要素の合計を得る
sum
は呼び出し対象のイテレータの所有権を奪うので、sum
呼び出し後にv1_iter
を使用することはできません。
他のイテレータを生成するメソッド
Iterator
トレイトに定義された他のメソッドは、イテレータアダプタ(iterator adaptors)として知られていますが、
イテレータを別の種類のイテレータに変えさせてくれます。イテレータアダプタを複数回呼ぶ呼び出しを連結して、
複雑な動作を読みやすい形で行うことができます。ですが、全てのイテレータは怠惰なので、消費アダプタメソッドのどれかを呼び出し、
イテレータアダプタの呼び出しから結果を得なければなりません。
リスト13-17は、イテレータアダプタメソッドのmap
の呼び出し例を示し、各要素に対して呼び出すクロージャを取り、
新しいイテレータを生成します。ここのクロージャは、ベクタの各要素が1インクリメントされる新しいイテレータを作成します。
ところが、このコードは警告を発します:
ファイル名: src/main.rs
#![allow(unused)] fn main() { let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); }
リスト13-17: イテレータアダプタのmap
を呼び出して新規イテレータを作成する
出る警告は以下の通りです:
warning: unused `std::iter::Map` which must be used: iterator adaptors are lazy
and do nothing unless consumed
(警告: 使用されねばならない`std::iter::Map`が未使用です: イテレータアダプタは怠惰で、
消費されるまで何もしません)
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: #[warn(unused_must_use)] on by default
リスト13-17のコードは何もしません; 指定したクロージャは、決して呼ばれないのです。警告が理由を思い出させてくれています: イテレータアダプタは怠惰で、ここでイテレータを消費する必要があるのです。
これを修正し、イテレータを消費するには、collect
メソッドを使用しますが、これは第12章のリスト12-1でenv::args
とともに使用しました。
このメソッドはイテレータを消費し、結果の値をコレクションデータ型に集結させます。
リスト13-18において、map
呼び出しから返ってきたイテレータを繰り返した結果をベクタに集結させています。
このベクタは、最終的に元のベクタの各要素に1を足したものが含まれます。
ファイル名: src/main.rs
#![allow(unused)] fn main() { let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]); }
リスト13-18: map
メソッドを呼び出して新規イテレータを作成し、
それからcollect
メソッドを呼び出してその新規イテレータを消費し、ベクタを生成する
map
はクロージャを取るので、各要素に対して行いたいどんな処理も指定することができます。
これは、Iterator
トレイトが提供する繰り返し動作を再利用しつつ、
クロージャにより一部の動作をカスタマイズできる好例になっています。
環境をキャプチャするクロージャを使用する
イテレータが出てきたので、filter
イテレータアダプタを使って環境をキャプチャするクロージャの一般的な使用をデモすることができます。
イテレータのfilter
メソッドは、イテレータの各要素を取り、論理値を返すクロージャを取ります。
このクロージャがtrue
を返せば、filter
が生成するイテレータにその値が含まれます。クロージャがfalse
を返したら、
結果のイテレータにその値は含まれません。
リスト13-19では、環境からshoe_size
変数をキャプチャするクロージャでfilter
を使って、
Shoe
構造体インスタンスのコレクションを繰り返しています。指定したサイズの靴だけを返すわけです。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[derive(PartialEq, Debug)] struct Shoe { size: u32, style: String, } fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> { shoes.into_iter() .filter(|s| s.size == shoe_size) .collect() } #[test] fn filters_by_size() { let shoes = vec![ Shoe { size: 10, style: String::from("sneaker") }, Shoe { size: 13, style: String::from("sandal") }, Shoe { size: 10, style: String::from("boot") }, ]; let in_my_size = shoes_in_my_size(shoes, 10); assert_eq!( in_my_size, vec![ Shoe { size: 10, style: String::from("sneaker") }, Shoe { size: 10, style: String::from("boot") }, ] ); } }
リスト13-19: shoe_size
をキャプチャするクロージャでfilter
メソッドを使用する
shoes_in_my_size
関数は、引数として靴のベクタとサイズの所有権を奪います。指定されたサイズの靴だけを含むベクタを返します。
shoes_in_my_size
の本体で、into_iter
を呼び出してベクタの所有権を奪うイテレータを作成しています。
そして、filter
を呼び出してそのイテレータをクロージャがtrue
を返した要素だけを含む新しいイテレータに適合させます。
クロージャは、環境からshoe_size
引数をキャプチャし、指定されたサイズの靴だけを保持しながら、
その値を各靴のサイズと比較します。最後に、collect
を呼び出すと、
関数により返ってきたベクタに適合させたイテレータから返ってきた値が集まるのです。
shoes_in_my_size
を呼び出した時に、指定した値と同じサイズの靴だけが得られることをテストは示しています。
Iterator
トレイトで独自のイテレータを作成する
ベクタに対し、iter
、into_iter
、iter_mut
を呼び出すことでイテレータを作成できることを示してきました。
ハッシュマップなどの標準ライブラリの他のコレクション型からもイテレータを作成できます。
Iterator
トレイトを自分で実装することで、したいことを何でもするイテレータを作成することもできます。
前述の通り、定義を提供する必要のある唯一のメソッドは、next
メソッドなのです。一旦、そうしてしまえば、
Iterator
トレイトが用意しているデフォルト実装のある他の全てのメソッドを使うことができるのです!
デモ用に、絶対に1から5をカウントするだけのイテレータを作成しましょう。まず、値を保持する構造体を生成し、
Iterator
トレイトを実装することでこの構造体をイテレータにし、その実装内の値を使用します。
リスト13-20は、Counter
構造体とCounter
のインスタンスを作るnew
関連関数の定義です:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { struct Counter { count: u32, } impl Counter { fn new() -> Counter { Counter { count: 0 } } } }
リスト13-20: Counter
構造体とcount
に対して0という初期値でCounter
のインスタンスを作るnew
関数を定義する
Counter
構造体には、count
というフィールドがあります。このフィールドは、
1から5までの繰り返しのどこにいるかを追いかけるu32
値を保持しています。Counter
の実装にその値を管理してほしいので、
count
フィールドは非公開です。count
フィールドは常に0という値から新規インスタンスを開始するという動作をnew
関数は強要します。
次に、next
メソッドの本体をこのイテレータが使用された際に起きてほしいことを指定するように定義して、
Counter
型に対してIterator
トレイトを実装します。リスト13-21のようにですね:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { struct Counter { count: u32, } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { self.count += 1; if self.count < 6 { Some(self.count) } else { None } } } }
リスト13-21: Counter
構造体にIterator
トレイトを実装する
イテレータのItem
関連型をu32
に設定しました。つまり、イテレータは、u32
の値を返します。
ここでも、まだ関連型について心配しないでください。第19章で講義します。
イテレータに現在の状態に1を足してほしいので、まず1を返すようにcount
を0に初期化しました。
count
の値が5以下なら、next
はSome
に包まれた現在の値を返しますが、
count
が6以上なら、イテレータはNone
を返します。
Counter
イテレータのnext
メソッドを使用する
一旦Iterator
トレイトを実装し終わったら、イテレータの出来上がりです!リスト13-22は、
リスト13-15のベクタから生成したイテレータと全く同様に、直接next
メソッドを呼び出すことで、
Counter
構造体のイテレータ機能を使用できることをデモするテストを示しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { struct Counter { count: u32, } impl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option<Self::Item> { self.count += 1; if self.count < 6 { Some(self.count) } else { None } } } #[test] fn calling_next_directly() { let mut counter = Counter::new(); assert_eq!(counter.next(), Some(1)); assert_eq!(counter.next(), Some(2)); assert_eq!(counter.next(), Some(3)); assert_eq!(counter.next(), Some(4)); assert_eq!(counter.next(), Some(5)); assert_eq!(counter.next(), None); } }
リスト13-22: next
メソッド実装の機能をテストする
このテストは、counter
変数に新しいCounter
インスタンスを生成し、
それからイテレータにほしい動作が実装し終わっていることを実証しながら、next
を繰り返し呼び出しています:
1から5の値を返すことです。
他のIterator
トレイトメソッドを使用する
next
メソッドを定義してIterator
トレイトを実装したので、今では、標準ライブラリで定義されているように、
どんなIterator
トレイトメソッドのデフォルト実装も使えるようになりました。全てnext
メソッドの機能を使っているからです。
例えば、何らかの理由で、Counter
インスタンスが生成する値を取り、最初の値を飛ばしてから、
別のCounter
インスタンスが生成する値と一組にし、各ペアを掛け算し、3で割り切れる結果だけを残し、
全結果の値を足し合わせたくなったら、リスト13-23のテストに示したように、そうすることができます:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { struct Counter { count: u32, } impl Counter { fn new() -> Counter { Counter { count: 0 } } } impl Iterator for Counter { // このイテレータはu32を生成します // Our iterator will produce u32s type Item = u32; fn next(&mut self) -> Option<Self::Item> { // カウントをインクリメントする。故に0から始まる // increment our count. This is why we started at zero. self.count += 1; // カウントが終わったかどうか確認する // check to see if we've finished counting or not. if self.count < 6 { Some(self.count) } else { None } } } #[test] fn using_other_iterator_trait_methods() { let sum: u32 = Counter::new().zip(Counter::new().skip(1)) .map(|(a, b)| a * b) .filter(|x| x % 3 == 0) .sum(); assert_eq!(18, sum); } }
リスト13-23: Counter
イテレータに対していろんなIterator
トレイトのメソッドを使用する
zip
は4組しか生成しないことに注意してください; 理論的な5番目の組の(5, None)
は、
入力イテレータのどちらかがNone
を返したら、zip
はNone
を返却するため、決して生成されることはありません。
next
メソッドの動作方法を指定し、標準ライブラリがnext
を呼び出す他のメソッドにデフォルト実装を提供しているので、
これらのメソッド呼び出しは全て可能です。
入出力プロジェクトを改善する
このイテレータに関する新しい知識があれば、イテレータを使用してコードのいろんな場所をより明確で簡潔にすることで、
第12章の入出力プロジェクトを改善することができます。イテレータがConfig::new
関数とsearch
関数の実装を改善する方法に目を向けましょう。
イテレータを使用してclone
を取り除く
リスト12-6において、スライスに添え字アクセスして値をクローンすることで、Config
構造体に値を所有させながら、
String
値のスライスを取り、Config
構造体のインスタンスを作るコードを追記しました。リスト13-24では、
リスト12-23のようなConfig::new
の実装を再現しました:
ファイル名: src/lib.rs
impl Config {
pub fn new(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let filename = args[2].clone();
let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
Ok(Config { query, filename, case_sensitive })
}
}
リスト13-24: リスト12-23からConfig::new
関数の再現
その際、将来的に除去する予定なので、非効率的なclone
呼び出しを憂慮するなと述べました。
えっと、その時は今です!
引数args
にString
要素のスライスがあるためにここでclone
が必要だったのですが、
new
関数はargs
を所有していません。Config
インスタンスの所有権を返すためには、
Config
インスタンスがその値を所有できるように、Config
のquery
とfilename
フィールドから値をクローンしなければなりませんでした。
イテレータについての新しい知識があれば、new
関数をスライスを借用する代わりに、
引数としてイテレータの所有権を奪うように変更することができます。スライスの長さを確認し、
特定の場所に添え字アクセスするコードの代わりにイテレータの機能を使います。これにより、
イテレータは値にアクセスするので、Config::new
関数がすることが明確化します。
ひとたび、Config::new
がイテレータの所有権を奪い、借用する添え字アクセス処理をやめたら、
clone
を呼び出して新しくメモリ確保するのではなく、イテレータからのString
値をConfig
にムーブできます。
返却されるイテレータを直接使う
入出力プロジェクトのsrc/main.rsファイルを開いてください。こんな見た目のはずです:
ファイル名: src/main.rs
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
// --snip--
}
リスト12-24のようなmain
関数の冒頭をリスト13-25のコードに変更します。
これは、Config::new
も更新するまでコンパイルできません。
ファイル名: src/main.rs
fn main() {
let config = Config::new(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {}", err);
process::exit(1);
});
// --snip--
}
リスト13-25: env::args
の戻り値をConfig::new
に渡す
env::args
関数は、イテレータを返します!イテレータの値をベクタに集結させ、それからスライスをConfig::new
に渡すのではなく、
今ではenv::args
から返ってくるイテレータの所有権を直接Config::new
に渡しています。
次に、Config::new
の定義を更新する必要があります。入出力プロジェクトのsrc/lib.rsファイルで、
Config::new
のシグニチャをリスト13-26のように変えましょう。関数本体を更新する必要があるので、
それでもコンパイルはできません。
ファイル名: src/lib.rs
impl Config {
pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
// --snip--
リスト13-26: Config::new
のシグニチャをイテレータを期待するように更新する
env::args
関数の標準ライブラリドキュメントは、自身が返すイテレータの型は、std::env::Args
であると表示しています。
Config::new
関数のシグニチャを更新したので、引数args
の型は、&[String]
ではなく、
std::env::Args
になりました。args
の所有権を奪い、繰り返しを行うことでargs
を可変化する予定なので、
args
引数の仕様にmut
キーワードを追記でき、可変にします。
添え字の代わりにIterator
トレイトのメソッドを使用する
次に、Config::new
の本体を修正しましょう。標準ライブラリのドキュメントは、
std::env::Args
がIterator
トレイトを実装していることにも言及しているので、
それに対してnext
メソッドを呼び出せることがわかります!リスト13-27は、
リスト12-23のコードをnext
メソッドを使用するように更新したものです:
ファイル名: src/lib.rs
fn main() {} use std::env; struct Config { query: String, filename: String, case_sensitive: bool, } impl Config { pub fn new(mut args: std::env::Args) -> Result<Config, &'static str> { args.next(); let query = match args.next() { Some(arg) => arg, // クエリ文字列を取得しませんでした None => return Err("Didn't get a query string"), }; let filename = match args.next() { Some(arg) => arg, // ファイル名を取得しませんでした None => return Err("Didn't get a file name"), }; let case_sensitive = env::var("CASE_INSENSITIVE").is_err(); Ok(Config { query, filename, case_sensitive }) } }
リスト13-27: Config::new
の本体をイテレータメソッドを使うように変更する
env::args
の戻り値の1番目の値は、プログラム名であることを思い出してください。それは無視し、
次の値を取得したいので、まずnext
を呼び出し、戻り値に対して何もしません。2番目に、
next
を呼び出してConfig
のquery
フィールドに置きたい値を得ます。next
がSome
を返したら、
match
を使用してその値を抜き出します。None
を返したら、十分な引数が与えられなかったということなので、
Err
値で早期リターンします。filename
値に対しても同じことをします。
イテレータアダプタでコードをより明確にする
入出力プロジェクトのsearch
関数でも、イテレータを活用することができます。その関数はリスト12-19に示していますが、以下のリスト13-28に再掲します。
ファイル名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
リスト13-28: リスト12-19のsearch
関数の実装
イテレータアダプタメソッドを使用して、このコードをもっと簡潔に書くことができます。そうすれば、
可変な中間のresults
ベクタをなくすこともできます。関数型プログラミングスタイルは、可変な状態の量を最小化することを好み、
コードを明瞭化します。可変な状態を除去すると、検索を同時並行に行うという将来的な改善をするのが、
可能になる可能性があります。なぜなら、results
ベクタへの同時アクセスを管理する必要がなくなるからです。
リスト13-29は、この変更を示しています:
ファイル名: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents.lines()
.filter(|line| line.contains(query))
.collect()
}
リスト13-29: search
関数の実装でイテレータアダプタのメソッドを使用する
search
関数の目的は、query
を含むcontents
の行全てを返すことであることを思い出してください。
リスト13-19のfilter
例に酷似して、このコードはfilter
アダプタを使用してline.contains(query)
が真を返す行だけを残すことができます。
それから、合致した行を別のベクタにcollect
で集結させます。ずっと単純です!ご自由に、
同じ変更を行い、search_case_insensitive
関数でもイテレータメソッドを使うようにしてください。
次の論理的な疑問は、自身のコードでどちらのスタイルを選ぶかと理由です: リスト13-28の元の実装とリスト13-29のイテレータを使用するバージョンです。 多くのRustプログラマは、イテレータスタイルを好みます。とっかかりが少し困難ですが、 いろんなイテレータアダプタとそれがすることの感覚を一度掴めれば、イテレータの方が理解しやすいこともあります。 いろんなループを少しずつもてあそんだり、新しいベクタを構築する代わりに、コードは、ループの高難度の目的に集中できるのです。 これは、ありふれたコードの一部を抽象化するので、イテレータの各要素が通過しなければならないふるい条件など、 このコードに独特の概念を理解しやすくなります。
ですが、本当に2つの実装は等価なのでしょうか?直観的な仮説は、より低レベルのループの方がより高速ということかもしれません。 パフォーマンスに触れましょう。
パフォーマンス比較: ループVSイテレータ
ループを使うべきかイテレータを使うべきか決定するために、search
関数のうち、どちらのバージョンが速いか知る必要があります:
明示的なfor
ループがあるバージョンと、イテレータのバージョンです。
サー・アーサー・コナン・ドイル(Sir Arthur Conan Doyle)の、
シャーロックホームズの冒険(The Adventures of Sherlock Homes)全体をString
に読み込み、
そのコンテンツでtheという単語を検索することでベンチマークを行いました。
こちらが、for
を使用したsearch
関数のバージョンと、イテレータを使用したバージョンに関するベンチマーク結果です。
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
イテレータバージョンの方が些か高速ですね!ここでは、ベンチマークのコードは説明しません。 なぜなら、要点は、2つのバージョンが等価であることを証明することではなく、 これら2つの実装がパフォーマンス的にどう比較されるかを大まかに把握することだからです。
より包括的なベンチマークとするためには、いろんなサイズの様々なテキストをcontents
として、異なる単語、異なる長さの単語をquery
として、
他のあらゆる種類のバリエーションを確認するべきです。重要なのは: イテレータは、
高度な抽象化にも関わらず、低レベルのコードを自身で書いているかのように、ほぼ同じコードにコンパイルされることです。
イテレータは、Rustのゼロコスト抽象化の一つであり、これは、抽象化を使うことが追加の実行時オーバーヘッドを生まないことを意味しています。
このことは、C++の元の設計者であり実装者のビャーネ・ストロヴストルップ(Bjarne Stroustrup)が、
ゼロオーバーヘッドを「C++の基礎(2012)」で定義したのと類似しています。
一般的に、C++の実装は、ゼロオーバーヘッド原則を遵守します: 使用しないものには、支払わなくてよい。 さらに: 実際に使っているものに対して、コードをそれ以上うまく渡すことはできない。
別の例として、以下のコードは、オーディオデコーダから取ってきました。デコードアルゴリズムは、
線形予測数学演算を使用して、以前のサンプルの線形関数に基づいて未来の値を予測します。このコードは、
イテレータ連結をしてスコープにある3つの変数に計算を行っています: buffer
というデータのスライス、
12のcoefficients
(係数)の配列、qlp_shift
でデータをシフトする量です。この例の中で変数を宣言しましたが、
値は与えていません; このコードは、文脈の外では大して意味を持ちませんが、
それでもRustが高レベルな考えを低レベルなコードに翻訳する簡潔で現実的な例になっています:
let buffer: &mut [i32];
let coefficients: [i64; 12];
let qlp_shift: i16;
for i in 12..buffer.len() {
let prediction = coefficients.iter()
.zip(&buffer[i - 12..i])
.map(|(&c, &s)| c * s as i64)
.sum::<i64>() >> qlp_shift;
let delta = buffer[i];
buffer[i] = prediction as i32 + delta;
}
prediction
の値を算出するために、このコードは、coefficients
の12の値を繰り返し、zip
メソッドを使用して、
係数値を前のbuffer
の12の値と組にします。それから各組について、その値をかけ合わせ、結果を全て合計し、
合計のビットをqlp_shift
ビット分だけ右にシフトさせます。
オーディオデコーダのようなアプリケーションの計算は、しばしばパフォーマンスに最も重きを置きます。
ここでは、イテレータを作成し、2つのアダプタを使用し、それから値を消費しています。
このRustコードは、どんな機械語コードにコンパイルされるのでしょうか?えー、執筆時点では、
手作業で書いたものと同じ機械語にコンパイルされます。coefficients
の値の繰り返しに対応するループは全く存在しません:
コンパイラは、12回繰り返しがあることを把握しているので、ループを「展開」します。
ループの展開は、ループ制御コードのオーバーヘッドを除去し、代わりにループの繰り返しごとに同じコードを生成する最適化です。
係数は全てレジスタに保存されます。つまり、値に非常に高速にアクセスします。実行時に配列の境界チェックをすることもありません。 コンパイラが適用可能なこれらの最適化全てにより、結果のコードは究極的に効率化されます。このことがわかったので、 もうイテレータとクロージャを恐れなしに使用することができますね!それらのおかげでコードは、高レベルだけれども、 そうすることに対して実行時のパフォーマンスを犠牲にしないようになります。
まとめ
クロージャとイテレータは、関数型言語の考えに着想を得たRustの機能です。低レベルのパフォーマンスで、 高レベルの考えを明確に表現するというRustの能力に貢献しています。クロージャとイテレータの実装は、 実行時のパフォーマンスが影響されないようなものです。これは、ゼロ代償抽象化を提供するのに努力を惜しまないRustの目標の一部です。
今や入出力プロジェクトの表現力を改善したので、プロジェクトを世界と共有するのに役に立つcargo
の機能にもっと目を向けましょう。
CargoとCrates.ioについてより詳しく
今までCargoのビルド、実行、コードのテストを行うという最も基礎的な機能のみを使ってきましたが、 他にもできることはたくさんあります。この章では、そのような他のより高度な機能の一部を議論し、 以下のことをする方法をお見せしましょう:
- リリースプロファイルでビルドをカスタマイズする
- crates.ioでライブラリを公開する
- ワークスペースで巨大なプロジェクトを体系化する
- crates.ioからバイナリをインストールする
- 独自のコマンドを使用してCargoを拡張する
また、Cargoはこの章で講義する以上のこともできるので、機能の全解説を見るには、 ドキュメンテーションを参照されたし。
リリースプロファイルでビルドをカスタマイズする
Rustにおいて、リリースプロファイルとは、プログラマがコードのコンパイルオプションについてより制御可能にしてくれる、 定義済みのカスタマイズ可能なプロファイルです。各プロファイルは、それぞれ独立して設定されます。
Cargoには2つの主なプロファイルが存在します: dev
プロファイルは、cargo build
コマンドを実行したときに使用され、
release
プロファイルは、cargo build --release
コマンドを実行したときに使用されます。
dev
プロファイルは、開発中に役に立つデフォルト設定がなされており、release
プロファイルは、
リリース用の設定がなされています。
これらのプロファイル名は、ビルドの出力で馴染みのある可能性があります:
$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
$ cargo build --release
Finished release [optimized] target(s) in 0.0 secs
このビルド出力で表示されているdev
とrelease
は、コンパイラが異なるプロファイルを使用していることを示しています。
プロジェクトのCargo.tomlファイルに[profile.*]
セクションが存在しない際に適用される各プロファイル用のデフォルト設定が、
Cargoには存在します。カスタマイズしたいプロファイル用の[profile.*]
セクションを追加することで、
デフォルト設定の一部を上書きすることができます。例えば、こちらがdev
とrelease
プロファイルのopt-level
設定のデフォルト値です:
ファイル名: Cargo.toml
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
opt-level
設定は、0から3の範囲でコンパイラがコードに適用する最適化の度合いを制御します。
最適化を多くかけると、コンパイル時間が延びるので、開発中に頻繁にコードをコンパイルするのなら、
たとえ出力結果のコードの動作速度が遅くなっても早くコンパイルが済んでほしいですよね。
これが、dev
のopt-level
のデフォルト設定が0
になっている唯一の理由です。
コードのリリース準備ができたら、より長い時間をコンパイルにかけるのが最善の策です。
リリースモードでコンパイルするのはたった1回ですが、コンパイル結果のプログラムは何度も実行するので、
リリースモードでは、長いコンパイル時間と引き換えに、生成したコードが速く動作します。
そのため、release
のopt-level
のデフォルト設定が3
になっているのです。
デフォルト設定に対してCargo.toml
で異なる値を追加すれば、上書きすることができます。
例として、開発用プロファイルで最適化レベル1を使用したければ、以下の2行をプロジェクトのCargo.tomlファイルに追加できます:
ファイル名: Cargo.toml
[profile.dev]
opt-level = 1
このコードは、デフォルト設定の0
を上書きします。こうすると、cargo build
を実行したときに、
dev
プロファイル用のデフォルト設定に加えて、Cargoはopt-level
の変更を適用します。
opt-level
を1
に設定したので、Cargoはデフォルトよりは最適化を行いますが、リリースビルドほどではありません。