The Rust Programming Language 日本語版
著:Steve Klabnik、Carol Nichols、貢献:Rustコミュニティ
このテキストのこの版ではRust 1.58(2022年1月13日リリース)かそれ以降が使われていることを前提にしています。 Rustをインストールしたりアップデートしたりするには第1章の「インストール」節を読んでください。
HTML版はhttps://doc.rust-lang.org/stable/book/で公開されています。
オフラインのときは、rustup
でインストールしたRustを使ってrustup docs --book
で開けます。
訳注:日本語のHTML版はhttps://doc.rust-jp.rs/book-ja/で公開されています。
rustup
を使ってオフラインで読むことはできません。
また、コミュニティによるいくつかの翻訳版もあります。
このテキストの(英語版の)ペーパーバック版と電子書籍版はNo Starch出版から発売されています。
まえがき
すぐにはわかりにくいかもしれませんが、Rustプログラミング言語は、エンパワーメント(empowerment)を根本原理としています: どんな種類のコードを現在書いているにせよ、Rustは幅広い領域で以前よりも遠くへ到達し、 自信を持ってプログラムを組む力を与え(empower)ます。
一例を挙げると、メモリ管理やデータ表現、並行性などの低レベルな詳細を扱う「システムレベル」のプログラミングがあります。 伝統的にこの分野は難解で、年月をかけてやっかいな落とし穴を回避する術を習得した選ばれし者にだけ可能と見なされています。 そのように鍛錬を積んだ者でさえ注意が必要で、さもないと書いたコードがクラッキングの糸口になったりクラッシュやデータ破損を引き起こしかねないのです。
この難しさを取り除くために、Rustは、古い落とし穴を排除し、その過程で使いやすく役に立つ洗練された一連のツールを提供します。 低レベルな制御に「下がる」必要があるプログラマは、お決まりのクラッシュやセキュリティホールのリスクを負わず、 気まぐれなツールチェーンのデリケートな部分を学ぶ必要なくRustで同じことができます。さらにいいことに、 Rustは、スピードとメモリ使用の観点で効率的な信頼性の高いコードへと自然に導くよう設計されています。
既に低レベルコードに取り組んでいるプログラマは、Rustを使用してさらなる高みを目指せます。例えば、 Rustで並列性を導入することは、比較的低リスクです: コンパイラが伝統的なミスを捕捉してくれるのです。 そして、クラッシュや脆弱性の糸口を誤って導入しないという自信を持ってコードの大胆な最適化に取り組めるのです。
ですが、Rustは低レベルなシステムプログラミングに限定されているわけではありません。十分に表現力豊かでエルゴノミックなので、 コマンドラインアプリやWebサーバ、その他様々な楽しいコードを書けます。この本の後半に両者の単純な例が見つかるでしょう。 Rustを使うことで1つの領域から他の領域へと使い回せる技術を身につけられます; ウェブアプリを書いてRustを学び、それからその同じ技術をラズベリーパイを対象に適用できるのです。
この本は、ユーザに力を与え(empower)るRustのポテンシャルを全て含んでいます。あなたのRustの知識のみをレベルアップさせるだけでなく、 プログラマとしての全般的な能力や自信をもレベルアップさせる手助けを意図した親しみやすくわかりやすいテキストです。 さあ、飛び込んで学ぶ準備をしてください。Rustコミュニティへようこそ!
- ニコラス・マットサキス(Nicholas Matsakis)とアーロン・チューロン(Aaron Turon)
はじめに
注釈: この本のこの版は、本として利用可能なThe Rust Programming Languageと、 No Starch Pressのebook形式と同じです。
The Rust Programming Languageへようこそ。Rustに関する入門書です。
Rustプログラミング言語は、高速で信頼できるソフトウェアを書く手助けをしてくれます。
高レベルのエルゴノミクス(訳注
: ergonomicsとは、人間工学的という意味。砕いて言えば、人間に優しいということ)と低レベルの制御は、
しばしばプログラミング言語の設計においてトレードオフの関係になります;
Rustは、その衝突に挑戦しています。バランスのとれた強力な技術の許容量と素晴らしい開発者経験を通して、
Rustは伝統的にそれらの制御と紐付いていた困難全てなしに低レベルの詳細(メモリ使用など)を制御する選択肢を与えてくれます。
Rustは誰のためのものなの
Rustは、様々な理由により多くの人にとって理想的です。いくつか最も重要なグループを見ていきましょう。
開発者チーム
Rustは、いろんなレベルのシステムプログラミングの知識を持つ開発者の巨大なチームとコラボするのに生産的なツールであると証明してきています。 低レベルコードは様々な種類の微細なバグを抱える傾向があり、そのようなバグは他の言語だと広範なテストと、 経験豊富な開発者による注意深いコードレビューによってのみ捕捉されるものです。Rustにおいては、 コンパイラが並行性のバグも含めたこのようなとらえどころのないバグのあるコードをコンパイルするのを拒むことで、 門番の役割を担います。コンパイラとともに取り組むことで、チームはバグを追いかけるよりもプログラムのロジックに集中することに、 時間を費やせるのです。
Rustはまた、現代的な開発ツールをシステムプログラミング世界に導入します。
- Cargoは、付属の依存関係管理ツール兼ビルドツールで、依存関係の追加、コンパイル、管理を容易にし、Rustのエコシステム全体で一貫性を持たせます。
- Rustfmtは開発者の間で一貫したコーディングスタイルを保証します。
- Rust言語サーバーは、IDE(統合開発環境)との統合により、コード補完やインラインエラーメッセージに対応しています。
これらのツールやRustのエコシステムの他のツールを使用することで、開発者はシステムレベルのコードを書きながら生産性を高めることができます。
学生
Rustは、学生やシステムの概念を学ぶことに興味のある方向けです。Rustを使用して、 多くの人がOS開発などの話題を学んできました。コミュニティはとても暖かく、喜んで学生の質問に答えてくれます。 この本のような努力を通じて、Rustチームはシステムの概念を多くの人、特にプログラミング初心者にとってアクセス可能にしたいと考えています。
企業
数百の企業が、大企業、中小企業を問わず、様々なタスクにプロダクションでRustを使用しています。 そのタスクには、コマンドラインツール、Webサービス、DevOpsツール、組み込みデバイス、 オーディオとビデオの解析および変換、暗号通貨、生物情報学、サーチエンジン、IoTアプリケーション、 機械学習、Firefoxウェブブラウザの主要部分さえ含まれます。
オープンソース開発者
Rustは、Rustプログラミング言語やコミュニティ、開発者ツール、ライブラリを開発したい方向けです。 あなたがRust言語に貢献されることを心よりお待ちしております。
スピードと安定性に価値を見出す方
Rustは、スピードと安定性を言語に渇望する方向けです。ここでいうスピードとは、 Rustで作れるプログラムのスピードとソースコードを書くスピードのことです。Rustコンパイラのチェックにより、 機能の追加とリファクタリングを通して安定性を保証してくれます。これはこのようなチェックがない言語の脆いレガシーコードとは対照的で、 その場合開発者はしばしば、変更するのを恐れてしまいます。ゼロコスト抽象化を志向し、 手で書いたコードと同等の速度を誇る低レベルコードにコンパイルされる高レベル機能により、 Rustは安全なコードを高速なコードにもしようと努力しています。
Rust言語は他の多くのユーザのサポートも望んでいます; ここで名前を出した方は、 ただの最大の出資者の一部です。総合すると、Rustの最大の野望は、プログラマが数十年間受け入れてきた代償を、安全性と生産性、 スピードとエルゴノミクスを提供することで排除することです。Rustを試してみて、その選択が自分に合っているか確かめてください。
この本は誰のためのものなの
この本は、あなたが他のプログラミング言語でコードを書いたことがあることを想定していますが、 具体的にどの言語かという想定はしません。私たちは、幅広い分野のプログラミング背景からの人にとってこの資料を広くアクセスできるようにしようとしてきました。 プログラミングとはなんなのかやそれについて考える方法について多くを語るつもりはありません。 もし、完全なプログラミング初心者であれば、プログラミング入門を特に行う本を読むことでよりよく役に立つでしょう。
この本の使い方
一般的に、この本は、順番に読み進めていくことを前提にしています。後の章は、前の章の概念の上に成り立ち、 前の章では、ある話題にさほど深入りしない可能性があります; 典型的に後ほどの章で同じ話題を再度しています。
この本には2種類の章があるとわかるでしょう: 概念の章とプロジェクトの章です。概念の章では、 Rustの一面を学ぶでしょう。プロジェクトの章では、それまでに学んだことを適用して一緒に小さなプログラムを構築します。 2、12、20章がプロジェクトの章です。つまり、残りは概念の章です。
第1章はRustのインストール方法、“Hello, world!”プログラムの書き方、Rustのパッケージマネージャ兼、 ビルドツールのCargoの使用方法を説明します。第2章は、Rust言語への実践的な導入です。ここでは概念をざっくりと講義し、後ほどの章で追加の詳細を提供します。 今すぐRustの世界に飛び込みたいなら、第2章こそがそのためのものです。第3章は他のプログラミング言語の機能に似たRustの機能を講義していますが、 最初その3章すら飛ばして、まっすぐに第4章に向かい、Rustの所有権システムについて学びたくなる可能性があります。 しかしながら、あなたが次に進む前に全ての詳細を学ぶことを好む特別に几帳面な学習者なら、 第2章を飛ばして真っ先に第3章に行き、学んだ詳細を適用するプロジェクトに取り組みたくなった時に第2章に戻りたくなる可能性があります。
第5章は、構造体とメソッドについて議論し、第6章はenum、match
式、if let
制御フロー構文を講義します。
構造体とenumを使用してRustにおいて独自の型を作成します。
第7章では、Rustのモジュールシステムと自分のコードとその公開されたAPI(Application Programming Interface)を体系化するプライバシー規則について学びます。 第8章では、ベクタ、文字列、ハッシュマップなどの標準ライブラリが提供する一般的なコレクションデータ構造の一部を議論します。 第9章では、Rustのエラー処理哲学とテクニックを探究します。
第10章ではジェネリクス、トレイト、ライフタイムについて深入りし、これらは複数の型に適用されるコードを定義する力をくれます。
第11章は、完全にテストに関してで、Rustの安全性保証があってさえ、プログラムのロジックが正しいことを保証するために、
必要になります。第12章では、ファイル内のテキストを検索するgrep
コマンドラインツールの一部の機能を自身で構築します。
このために、以前の章で議論した多くの概念を使用します。
第13章はクロージャとイテレータを探究します。これらは、関数型プログラミング言語由来のRustの機能です。 第14章では、Cargoをより詳しく調査し、他人と自分のライブラリを共有する最善の策について語ります。 第15章では、標準ライブラリが提供するスマートポインタとその機能を可能にするトレイトを議論します。
第16章では、並行プログラミングの異なるモデルを見ていき、Rustが恐れなしに複数のスレッドでプログラムする手助けをする方法を語ります。 第17章では、馴染み深い可能性のあるオブジェクト指向プログラミングの原則とRustのイディオムがどう比較されるかに目を向けます。
第18章は、パターンとパターンマッチングのリファレンスであり、これらはRustプログラムを通して、
考えを表現する強力な方法になります。第19章は、unsafe Rustやマクロ、ライフタイム、トレイト、型、関数、クロージャの詳細を含む、
興味のある高度な話題のスモーガスボード(訳注
: 日本でいうバイキングのこと)を含みます。
第20章では、低レベルなマルチスレッドのWebサーバを実装するプロジェクトを完成させます!
最後に、言語についての有用な情報をよりリファレンスのような形式で含む付録があります。 付録AはRustのキーワードを講義し、付録Bは、Rustの演算子と記号、付録Cは、 標準ライブラリが提供する導出可能なトレイト、付録Dはいくつか便利な開発ツールを講義し、 付録EではRustのエディションについて説明します。
この本を読む間違った方法なんてありません: 飛ばしたければ、どうぞご自由に! 混乱したら、前の章に戻らなければならない可能性もあります。ですが、自分に合った方法でどうぞ。
Rustを学ぶ過程で重要な部分は、コンパイラが表示するエラーメッセージを読む方法を学ぶことです: それは動くコードへと導いてくれます。そのため、各場面でコンパイラが表示するエラーメッセージとともに、 コンパイルできない例を多く提供します。適当に例を選んで走らせたら、コンパイルできないかもしれないことを知ってください! 周りのテキストを読んで実行しようとしている例がエラーになることを意図しているのか確認することを確かめてください。 フェリスもコードが動作するとは意図されていないコードを見分けるのを手助けしてくれます:
Ferris | Meaning |
---|---|
このコードはコンパイルできません! | |
このコードはパニックします! | |
このコードはアンセーフなコードを含みます。 | |
このコードは求められている振る舞いをしません。 |
ほとんどの場合、コンパイルできないあらゆるコードの正しいバージョンへと導きます。
ソースコード
この本が生成されるソースファイルは、GitHubで見つかります。
訳注: 日本語版はこちらです。
事始め
Rustの旅を始めましょう!学ぶべきことはたくさんありますが、いかなる旅もどこかから始まります。 この章では、以下のことを説明します:
- RustをLinux、macOS、Windowsにインストールする
Hello, world!
と表示するプログラムを書くcargo
というRustのパッケージマネージャ兼ビルドシステムを使用する
インストール
最初の手順は、Rustをインストールすることです。Rustは、Rustのバージョンと関連するツールを管理する、rustup
というコマンドラインツールを使用してダウンロードします。ダウンロードには、インターネットへの接続が必要になります。
注釈: なんらかの理由で
rustup
を使用したくない場合、Rustインストールページで、 他の選択肢をご覧になってください。
訳注:日本語版のRustインストールページはこちらです。
以下の手順で最新の安定版のRustコンパイラをインストールします。 Rustは安定性 (stability) を保証しているので、現在この本の例でコンパイルできるものは、新しいバージョンになってもコンパイルでき続けることが保証されます。 出力は、バージョンによって多少異なる可能性があります。Rustは頻繁にエラーメッセージと警告を改善しているからです。 言い換えると、どんな新しいバージョンでもこの手順に従ってインストールした安定版なら、 この本の内容で想定通りに動くはずです。
コマンドラインの記法
この章及び、本を通して、端末で使用するなんらかのコマンドを示すことがあります。読者が入力するべき行は、 全て
$
で始まります。ただし、読者が$
文字を入力する必要はありません; これは各コマンドの開始を示しているだけです。$
で始まらない行は、典型的には直前のコマンドの出力を示します。また、PowerShell限定の例には、$
ではなく、>
を使用します。
LinuxとmacOSにrustup
をインストールする
LinuxかmacOSを使用しているなら、端末(ターミナル)を開き、以下のコマンドを入力してください:
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
このコマンドはスクリプトをダウンロードし、rustup
ツールのインストールを開始し、Rustの最新の安定版をインストールします。
パスワードを求められる可能性があります。インストールがうまく行けば、以下の行が出現するでしょう:
Rust is installed now. Great!
これに加えて、なんらかのリンカが必要になるでしょう。既にインストールされている可能性は高いものの、 Rustプログラムをコンパイルしようとした時、リンカが実行できないというエラーが出たら、 システムにリンカがインストールされていないということなので、手動でインストールする必要があるでしょう。 Cコンパイラは通常正しいリンカとセットになっています。 自分のプラットフォームのドキュメンテーションを見てCコンパイラのインストール方法を確認してください。 一般的なRustパッケージの中には、Cコードに依存し、Cコンパイラが必要になるものもあります。 ですので、Cコンパイラは今のうちにインストールしておく価値があるかもしれません。
Windowsでrustup
をインストールする
Windowsでは、https://www.rust-lang.org/tools/installに行き、手順に従ってRustをインストールしてください。 インストールの途中で、Visual Studio 2013以降用のC++ビルドツールも必要になるという旨のメッセージが出るでしょう。 ビルドツールを取得する最も簡単な方法は、Visual Studio 2019用のビルドツールをインストールすることです。 どのワークロード (workloads) をインストールするかと質問されたときは、"C++ build tools"が選択されており、Windows 10 SDKと英語の言語パック (English language pack) が含まれていることを確かめてください。
訳注:Windowsの言語を日本語にしている場合は言語パックのところで「日本語」が選択されており、そのままの設定でインストールしても基本的に問題ないはずです。しかし、サードパーティーのツールやライブラリの中には英語の言語パックを必要とするものがあるため、「日本語」に加えて「英語」も選択することをお勧めします。
これ以降、cmd.exeとPowerShellの両方で動くコマンドを使用します。 特段の違いがあったら、どちらを使用すべきか説明します。
更新及びアンインストール
rustup
経由でRustをインストールしたなら、最新版へ更新するのは簡単です。
シェルから以下の更新スクリプトを実行してください:
$ rustup update
Rustとrustup
をアンインストールするには、シェルから以下のアンインストールスクリプトを実行してください:
$ rustup self uninstall
トラブルシューティング
Rustが正常にインストールされているか確かめるには、シェルを開いて以下の行を入力してください:
$ rustc --version
バージョンナンバー、コミットハッシュ、最新の安定版がリリースされたコミット日時が以下のフォーマットで表示されるのを目撃するはずです。
rustc x.y.z (abcabcabc yyyy-mm-dd)
この情報が見られたなら、Rustのインストールに成功しています!この情報が出ず、Windowsを使っているなら、
Rustが%PATH%
システム環境変数にあることを確認してください。これらが全て正常であるのに、それでもRustがうまく動かないなら、
助力を得られる場所はたくさんあります。最も簡単なのがRustの公式Discordの#beginnersチャンネルです。そのアドレスで、助けてくれる他のRustacean (Rustユーザが自分たちのことを呼ぶ、冗談めいたニックネーム) たちとチャットできます。
他にも、素晴らしいリソースとしてユーザ・フォーラムとStack Overflowが挙げられます。
訳注1:Rustaceanについて、いらないかもしれない補足です。公式Twitter曰く、Rustaceanはcrustaceans(甲殻類)から来ているそうです。 そのため、Rustのマスコットは(非公式らしいですが)カニ。上の会話でCの欠点を削ぎ落としているからcを省いてるの?みたいなことを聞いていますが、 違うそうです。検索したら、堅牢性が高いから甲殻類という意見もありますが、真偽は不明です。 明日使えるかもしれないトリビアでした。
訳注2:上にある公式Discordは英語話者のコミュニティです。日本語話者のためのコミュニティがZulip rust-lang-jpにあり、こちらでもRustaceanたちが活発に議論をしています。 公式Discord同様、初心者向けの#beginnersチャンネルが存在するので、気軽に質問してみてください。
ローカルのドキュメンテーション
インストールされたRustには、ローカルに複製されたドキュメンテーションのコピーが含まれているので、これをオフラインで閲覧することができます。
ブラウザでローカルのドキュメンテーションを開くには、rustup doc
を実行してください。
標準ライブラリにより提供される型や関数がなんなのかや、それをどう使えば良いのかがよくわからないときは、いつでもAPIのドキュメンテーションを検索してみてください!
Hello, World!
Rustをインストールしたので、最初のRustプログラムを書きましょう。新しい言語を学ぶ際に、
Hello, world!
というテキストを画面に出力する小さなプログラムを書くことは伝統的なことなので、
ここでも同じようにしましょう!
注釈: この本は、コマンドラインに基礎的な馴染みがあることを前提にしています。Rustは、編集やツール、 どこにコードがあるかについて特定の要求をしないので、コマンドラインではなくIDEを使用することを好むのなら、 どうぞご自由にお気に入りのIDEを使用してください。今では、多くのIDEがなんらかの形でRustをサポートしています; 詳しくは、IDEのドキュメンテーションをご覧ください。最近、Rustチームは優れたIDEサポートを有効にすることに注力し、 その前線で急激に成果があがっています!
プロジェクトのディレクトリを作成する
Rustコードを格納するディレクトリを作ることから始めましょう。Rustにとって、コードがどこにあるかは問題ではありませんが、 この本の練習とプロジェクトのために、ホームディレクトリにprojectsディレクトリを作成してプロジェクトを全てそこに保管することを推奨します。
端末を開いて以下のコマンドを入力し、projectsディレクトリと、 projectsディレクトリ内にHello, world!プロジェクトのディレクトリを作成してください。
LinuxとmacOSなら、こう入力してください:
$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world
Windowsのcmdなら、こう:
> mkdir "%USERPROFILE%\projects"
> cd /d "%USERPROFILE%\projects"
> mkdir hello_world
> cd hello_world
WindowsのPowerShellなら、こう:
> mkdir $env:USERPROFILE\projects
> cd $env:USERPROFILE\projects
> mkdir hello_world
> cd hello_world
Rustプログラムを書いて走らせる
次にソースファイルを作り、main.rsというファイル名にしてください。Rustのファイルは常に .rsという拡張子で終わります。 ファイル名に2単語以上使っているなら、アンダースコアで区切ってください。例えば、helloworld.rsではなく、 hello_world.rsを使用してください。
さて、作ったばかりのmain.rsファイルを開き、リスト1-1のコードを入力してください。
ファイル名: main.rs
fn main() { // 世界よ、こんにちは println!("Hello, world!"); }
リスト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
コマンドでプログラムを走らせてください。次の出力に示されているようなエラーメッセージを受け取るはずです:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
(不変変数`x`に2回代入できません)
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| (`x`への最初の代入)
| help: consider making this binding mutable: `mut x`
3 | println!("The value of x is: {}", x);
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` due to previous error
この例では、コンパイラがプログラムに潜むエラーを見つけ出す手助けをしてくれることが示されています。 コンパイルエラーは、イライラすることもあるものですが、まだプログラムにしてほしいことを安全に行えていないだけということなのです。 エラーが出るからといって、あなたがいいプログラマではないという意味ではありません! 経験豊富なRustaceanでも、コンパイルエラーを出すことはあります。
このエラーは、エラーの原因が不変変数xに2回代入できない
であると示しています。不変なx
という変数に別の値を代入しようとしたからです。
以前に不変と指定された値を変えようとした時に、コンパイルエラーが出るのは重要なことです。 なぜなら、この状況はまさしく、バグに繋がるからです。コードのある部分は、 値が変わることはないという前提のもとに処理を行い、別の部分がその値を変更していたら、 最初の部分が目論見通りに動いていない可能性があるのです。このようなバグは、発生してしまってからでは原因が追いかけづらいものです。 特に第2のコード片が、値を時々しか変えない場合、尚更です。
Rustでは、値が不変であると宣言したら、本当に変わらないことをコンパイラが担保してくれます。 つまり、コードを読み書きする際に、どこでどうやって値が変化しているかを追いかける必要がなくなります。 故にコードを通して正しいことを確認するのが簡単になるのです。
しかし、可変性は時として非常に有益なこともあります。変数は、標準でのみ、不変です。つまり、
第2章のように変数名の前にmut
キーワードを付けることで、可変にできるわけです。この値が変化できるようにするとともに、
mut
により、未来の読者に対してコードの別の部分がこの変数の値を変える可能性を示すことで、その意図を汲ませることができるのです。
例として、src/main.rsファイルを以下のように書き換えてください:
ファイル名: src/main.rs
fn main() { let mut x = 5; println!("The value of x is: {}", x); x = 6; println!("The value of x is: {}", x); }
今、このプログラムを走らせると、以下のような出力が得られます:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5 (xの値は5です)
The value of x is: 6
mut
キーワードが使われると、x
が束縛している値を5
から6
に変更できます。
変数を可変にする方が、不変変数だけがあるよりも書きやすくなるので、変数を可変にしたくなることもあるでしょう。
考えるべきトレードオフはバグの予防以外にも、いくつかあります。例えば、大きなデータ構造を使う場合などです。 インスタンスを可変にして変更できるようにする方が、いちいちインスタンスをコピーして新しくメモリ割り当てされたインスタンスを返すよりも速くなります。 小規模なデータ構造なら、新規インスタンスを生成して、もっと関数型っぽいコードを書く方が通して考えやすくなるため、 低パフォーマンスは、その簡潔性を得るのに足りうるペナルティになるかもしれません。
変数と定数(constants)の違い
変数の値を変更できないようにするといえば、他の多くの言語も持っている別のプログラミング概念を思い浮かべるかもしれません: 定数です。不変変数のように、定数は名前に束縛され、変更することが叶わない値のことですが、 定数と変数の間にはいくつかの違いがあります。
まず、定数にはmut
キーワードは使えません: 定数は標準で不変であるだけでなく、常に不変なのです。
定数はlet
キーワードの代わりに、const
キーワードで宣言し、値の型は必ず注釈しなければなりません。
型と型注釈については次のセクション、「データ型」で講義しますので、その詳細について気にする必要はありません。
ただ単に型は常に注釈しなければならないのだと思っていてください。
定数はどんなスコープでも定義できます。グローバルスコープも含めてです。なので、 いろんなところで使用される可能性のある値を定義するのに役に立ちます。
最後の違いは、定数は定数式にしかセットできないことです。関数呼び出し結果や、実行時に評価される値にはセットできません。
定数の名前がMAX_POINTS
で、値が100,000にセットされた定数定義の例をご覧ください。(Rustの定数の命名規則は、
全て大文字でアンダースコアで単語区切りすることです):
#![allow(unused)] fn main() { const MAX_POINTS: u32 = 100_000; }
定数は、プログラムが走る期間、定義されたスコープ内でずっと有効です。従って、 プログラムのいろんなところで使用される可能性のあるアプリケーション空間の値を定義するのに有益な選択肢になります。 例えば、ゲームでプレイヤーが取得可能なポイントの最高値や、光速度などですね。
プログラム中で使用されるハードコードされた値に対して、定数として名前付けすることは、 コードの将来的な管理者にとって値の意味を汲むのに役に立ちます。将来、ハードコードされた値を変える必要が出た時に、 たった1箇所を変更するだけで済むようにもしてくれます。
シャドーイング
第2章の数当てゲームのチュートリアル、「予想と秘密の数字を比較する」節で見たように、前に定義した変数と同じ名前の変数を新しく宣言でき、
新しい変数は、前の変数を覆い隠します。Rustaceanはこれを最初の変数は、
2番目の変数に覆い隠されたと言い、この変数を使用した際に、2番目の変数の値が現れるということです。
以下のようにして、同じ変数名を用いて変数を覆い隠し、let
キーワードの使用を繰り返します:
ファイル名: src/main.rs
fn main() { let x = 5; let x = x + 1; { let x = x * 2; println!("The value of x in the inner scope is: {}", x); } println!("The value of x is: {}", x); }
このプログラムはまず、x
を5
という値に束縛します。それからlet x =
を繰り返すことでx
を覆い隠し、
元の値に1
を加えることになるので、x
の値は6
になります。
3番目のlet
文もx
を覆い隠し、以前の値に2
をかけることになるので、x
の最終的な値は12
になります。
括弧を抜けるとシャドーイングは終了し、x
の値は元の6
に戻ります。
このプログラムを走らせたら、以下のように出力するでしょう:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
シャドーイングは、変数をmut
にするのとは違います。なぜなら、let
キーワードを使わずに、
誤ってこの変数に再代入を試みようものなら、コンパイルエラーが出るからです。let
を使うことで、
値にちょっとした加工は行えますが、その加工が終わったら、変数は不変になるわけです。
mut
と上書きのもう一つの違いは、再度let
キーワードを使用したら、実効的には新しい変数を生成していることになるので、
値の型を変えつつ、同じ変数名を使いまわせることです。例えば、
プログラムがユーザに何らかのテキストに対して空白文字を入力することで何個分のスペースを表示したいかを尋ねますが、
ただ、実際にはこの入力を数値として保持したいとしましょう:
fn main() { let spaces = " "; let spaces = spaces.len(); }
この文法要素は、容認されます。というのも、最初のspaces
変数は文字列型であり、2番目のspaces
変数は、
たまたま最初の変数と同じ名前になったまっさらな変数のわけですが、数値型になるからです。故に、シャドーイングのおかげで、
異なる名前を思いつく必要がなくなるわけです。spaces_str
とspaces_num
などですね; 代わりに、
よりシンプルなspaces
という名前を再利用できるわけです。一方で、この場合にmut
を使おうとすると、
以下に示した通りですが、コンパイルエラーになるわけです:
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}
変数の型を可変にすることは許されていないと言われているわけです:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types (型が合いません)
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
| (&str型を予期しましたが、usizeが見つかりました)
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` due to previous error
さあ、変数が動作する方法を見てきたので、今度は変数が取りうるデータ型について見ていきましょう。
データ型
Rustにおける値は全て、何らかのデータ型になり、コンパイラがどんなデータが指定されているか知れるので、 そのデータの取り扱い方も把握できるというわけです。2種のデータ型のサブセットを見ましょう: スカラー型と複合型です。
Rustは静的型付き言語であることを弁えておいてください。つまり、
コンパイル時に全ての変数の型が判明している必要があるということです。コンパイラは通常、値と使用方法に基づいて、
使用したい型を推論してくれます。複数の型が推論される可能性がある場合、例えば、
第2章の「予想と秘密の数字を比較する」節でparse
メソッドを使ってString
型を数値型に変換した時のように、
複数の型が可能な場合には、型注釈をつけなければいけません。以下のようにですね:
#![allow(unused)] fn main() { let guess: u32 = "42".parse().expect("Not a number!"); // 数字ではありません! }
ここで型注釈を付けなければ、コンパイラは以下のエラーを表示し、これは可能性のある型のうち、 どの型を使用したいのかを知るのに、コンパイラがプログラマからもっと情報を得る必要があることを意味します:
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
(型注釈が必要です)
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ consider giving `guess` a type
| (`guess`に型を与えることを検討してください)
For more information about this error, try `rustc --explain E0282`.
error: could not compile `no_type_annotations` due to previous error
他のデータ型についても、様々な型注釈を目にすることになるでしょう。
スカラー型
スカラー型は、単独の値を表します。Rustには主に4つのスカラー型があります: 整数、浮動小数点数、論理値、最後に文字です。他のプログラミング言語でも、これらの型を見かけたことはあるでしょう。 Rustでの動作方法に飛び込みましょう。
整数型
整数とは、小数部分のない数値のことです。第2章で一つの整数型を使用しましたね。u32
型です。
この型定義は、紐付けられる値が、符号なし整数(符号付き整数はu
ではなく、i
で始まります)になり、
これは、32ビット分のサイズを取ります。表3-1は、Rustの組み込み整数型を表示しています。
符号付きと符号なし欄の各バリアント(例: i16
)を使用して、整数値の型を宣言することができます。
表3-1: Rustの整数型
大きさ | 符号付き | 符号なし |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
arch | isize | usize |
各バリアントは、符号付きか符号なしかを選べ、明示的なサイズを持ちます。符号付きと符号なしは、 数値が正負を持つかどうかを示します。つまり、数値が符号を持つ必要があるかどうか(符号付き)、または、 絶対に正数にしかならず符号なしで表現できるかどうか(符号なし)です。これは、数値を紙に書き下すのと似ています: 符号が問題になるなら、数値はプラス記号、またはマイナス記号とともに表示されます; しかしながら、 その数値が正数であると仮定することが安全なら、符号なしで表示できるわけです。符号付き数値は、 2の補数表現で保持されます(これが何なのか確信を持てないのであれば、ネットで検索することができます。 まあ要するに、この解説は、この本の範疇外というわけです)。
各符号付きバリアントは、-(2n - 1)以上2n - 1 - 1以下の数値を保持でき、
ここでnはこのバリアントが使用するビット数です。以上から、i8
型は-(27)から27 - 1まで、
つまり、-128から127までを保持できます。符号なしバリアントは、0以上2n - 1以下を保持できるので、
u8
型は、0から28 - 1までの値、つまり、0から255までを保持できることになります。
加えて、isize
とusize
型は、プログラムが動作しているコンピュータの種類に依存します:
64ビットアーキテクチャなら、64ビットですし、32ビットアーキテクチャなら、32ビットになります。
整数リテラル(訳注
: リテラルとは、見たままの値ということ)は、表3-2に示すどの形式でも記述することができます。
バイトリテラルを除く数値リテラルは全て、
型接尾辞(例えば、57u8
)と_
を見た目の区切り記号(例えば、1_000
)に付加することができます。
表3-2: Rustの整数リテラル
数値リテラル | 例 |
---|---|
10進数 | 98_222 |
16進数 | 0xff |
8進数 | 0o77 |
2進数 | 0b1111_0000 |
バイト (u8 だけ) | b'A' |
では、どの整数型を使うべきかはどう把握すればいいのでしょうか?もし確信が持てないのならば、
Rustの基準型は一般的にいい選択肢になります。整数型の基準はi32
型です: 64ビットシステム上でも、
この型が普通最速になります。isize
とusize
を使う主な状況は、何らかのコレクションにアクセスすることです。
浮動小数点型
Rustにはさらに、浮動小数点数に対しても、2種類の基本型があり、浮動小数点数とは数値に小数点がついたもののことです。
Rustの浮動小数点型は、f32
とf64
で、それぞれ32ビットと64ビットサイズです。基準型はf64
です。
なぜなら、現代のCPUでは、f32
とほぼ同スピードにもかかわらず、より精度が高くなるからです。
実際に動作している浮動小数点数の例をご覧ください:
ファイル名: src/main.rs
fn main() { let x = 2.0; // f64 let y: f32 = 3.0; // f32 }
浮動小数点数は、IEEE-754規格に従って表現されています。f32
が単精度浮動小数点数、
f64
が倍精度浮動小数点数です。
数値演算
Rustにも全数値型に期待されうる標準的な数学演算が用意されています: 足し算、引き算、掛け算、割り算、余りです。
以下の例では、let
文での各演算の使用方法をご覧になれます:
ファイル名: src/main.rs
fn main() { // addition // 足し算 let sum = 5 + 10; // subtraction // 引き算 let difference = 95.5 - 4.3; // multiplication // 掛け算 let product = 4 * 30; // division // 割り算 let quotient = 56.7 / 32.2; let floored = 2 / 3; // Results in 0 // 結果は0 // remainder // 余り let remainder = 43 % 5; }
これらの文の各式は、数学演算子を使用しており、一つの値に評価され、そして、変数に束縛されます。 付録BにRustで使える演算子の一覧が載っています。
論理値型
他の多くの言語同様、Rustの論理値型も取りうる値は二つしかありません: true
とfalse
です。
Rustの論理値型は、bool
と指定されます。
例です:
ファイル名: src/main.rs
fn main() { let t = true; let f: bool = false; // with explicit type annotation // 明示的型注釈付きで }
論理値を使う主な手段は、条件式です。例えば、if
式などですね。if
式のRustでの動作方法については、
「制御フロー」節で講義します。
文字型
ここまで、数値型のみ扱ってきましたが、Rustには文字も用意されています。Rustのchar
型は、
言語の最も基本的なアルファベット型であり、以下のコードでその使用方法の一例を見ることができます。
(char
は、ダブルクォーテーションマークを使用する文字列に対して、シングルクォートで指定されることに注意してください。)
ファイル名: src/main.rs
fn main() { let c = 'z'; let z = 'ℤ'; let heart_eyed_cat = '😻'; //ハート目の猫 }
Rustのchar
型は、ユニコードのスカラー値を表します。これはつまり、アスキーよりもずっとたくさんのものを表せるということです。
アクセント文字; 中国語、日本語、韓国語文字;
絵文字; ゼロ幅スペースは、全てRustでは、有効なchar
型になります。ユニコードスカラー値は、
U+0000
からU+D7FF
までとU+E000
からU+10FFFF
までの範囲になります。
ところが、「文字」は実はユニコードの概念ではないので、文字とは何かという人間としての直観は、
Rustにおけるchar
値が何かとは合致しない可能性があります。この話題については、第8章の「文字列」で詳しく議論しましょう。
複合型
複合型により、複数の値を一つの型にまとめることができます。Rustには、 2種類の基本的な複合型があります: タプルと配列です。
タプル型
タプルは、複数の型の何らかの値を一つの複合型にまとめ上げる一般的な手段です。
タプルは、丸かっこの中にカンマ区切りの値リストを書くことで生成します。タプルの位置ごとに型があり、 タプル内の値はそれぞれ全てが同じ型である必要はありません。今回の例では、型注釈をあえて追加しました:
ファイル名: src/main.rs
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); }
変数tup
は、タプル全体に束縛されています。なぜなら、タプルは、一つの複合要素と考えられるからです。
タプルから個々の値を取り出すには、パターンマッチングを使用して分解することができます。以下のように:
ファイル名: src/main.rs
fn main() { let tup = (500, 6.4, 1); let (x, y, z) = tup; println!("The value of y is: {}", y); }
このプログラムは、まずタプルを生成し、それを変数tup
に束縛しています。
それからlet
とパターンを使ってtup
変数の中身を3つの個別の変数(x
、y
、z
ですね)に変換しています。
この過程は、分配と呼ばれます。単独のタプルを破壊して三分割しているからです。最後に、
プログラムはy
変数の値を出力し、6.4
と表示されます。
パターンマッチングを通しての分配の他にも、アクセスしたい値の番号をピリオド(.
)に続けて書くことで、
タプルの要素に直接アクセスすることもできます。例です:
ファイル名: src/main.rs
fn main() { let x: (i32, f64, u8) = (500, 6.4, 1); let five_hundred = x.0; let six_point_four = x.1; let one = x.2; }
このプログラムは、新しいタプルx
を作成し、添え字アクセスで各要素に対して新しい変数も作成しています。
多くのプログラミング言語同様、タプルの最初の添え字は0です。
配列型
配列によっても、複数の値のコレクションを得ることができます。タプルと異なり、配列の全要素は、 同じ型でなければなりません。Rustの配列は、他の言語と異なっています。Rustの配列は、 固定長なのです: 一度宣言されたら、サイズを伸ばすことも縮めることもできません。
Rustでは、配列に入れる要素は、角かっこ内にカンマ区切りリストとして記述します:
ファイル名: src/main.rs
fn main() { let a = [1, 2, 3, 4, 5]; }
配列は、ヒープよりもスタック(スタックとヒープについては第4章で詳らかに議論します)にデータのメモリを確保したい時、 または、常に固定長の要素があることを確認したい時に有効です。 ただ、配列は、ベクタ型ほど柔軟ではありません。ベクタは、標準ライブラリによって提供されている配列と似たようなコレクション型で、 こちらは、サイズを伸縮させることができます。配列とベクタ型、どちらを使うべきか確信が持てない時は、 おそらくベクタ型を使うべきです。第8章でベクタについて詳細に議論します。
ベクタ型よりも配列を使いたくなるかもしれない例は、1年の月の名前を扱うプログラムです。そのようなプログラムで、 月を追加したり削除したりすることまずないので、配列を使用できます。常に12個要素があることもわかってますからね:
#![allow(unused)] fn main() { let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; }
例えば次のように、配列の型は角かっこの中に要素の型とセミコロン、そして配列の要素数を与えます。
#![allow(unused)] fn main() { let a: [i32; 5] = [1, 2, 3, 4, 5]; }
ここでのi32
は要素の型です。セミコロンのあとの5
という数字は配列の要素が5つあることを表しています。
次のように、角かっこの中に初期値とセミコロン、そして配列の長さを与えることで、各要素に同じ値を持つように配列を初期化することができます。
#![allow(unused)] fn main() { let a = [3; 5]; }
このa
という名前の配列は3
という値が5つあるものです。これはlet a = [3, 3, 3, 3, 3];
と書くのと同じですが、より簡潔になります。
配列の要素にアクセスする
配列は、スタック上に確保される一塊のメモリです。添え字によって、 配列の要素にこのようにアクセスすることができます:
ファイル名: src/main.rs
fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; let second = a[1]; }
この例では、first
という名前の変数には1
という値が格納されます。配列の[0]
番目にある値が、
それだからですね。second
という名前の変数には、配列の[1]
番目の値2
が格納されます。
配列要素への無効なアクセス
配列の終端を越えて要素にアクセスしようとしたら、どうなるでしょうか? 先ほどの例を以下のように変えたとすると、コンパイルは通りますが、実行するとエラーで終了します:
ファイル名: src/main.rs
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
// 配列の何番目の要素にアクセスするか指定してください
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
// 値の読み込みに失敗しました
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
// 入力された値は数字ではありません
let element = a[index];
println!(
"The value of the element at index {} is: {}",
// {}番目の要素の値は{}です
index, element
);
}
このコードはコンパイルされます。cargo run
で走らせ、0, 1, 2, 3, または4をこのプログラムに入力すると配列の対応する値を出力します。もし配列の末尾を超えるような、例えば10などの数字を与えると、次のような出力が表示されます。
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
スレッド'main'は'範囲外アクセス: 長さは5ですが、添え字は10でした', src/main.rs:19:19
でパニックしました
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
コンパイルでは何もエラーが出なかったものの、プログラムは実行時エラーに陥り、 正常終了しませんでした。要素に添え字アクセスを試みると、言語は、 指定されたその添え字が配列長よりも小さいかを確認してくれます。添え字が配列長よりも大きければ、言語はパニックします。 パニックとは、プログラムがエラーで終了したことを表すRust用語です。
これは、実際に稼働しているRustの安全機構の最初の例になります。低レベル言語の多くでは、 この種のチェックは行われないため、間違った添え字を与えると、無効なメモリにアクセスできてしまいます。 Rustでは、メモリアクセスを許可し、処理を継続する代わりに即座にプログラムを終了することで、 この種のエラーからプログラマを保護しています。Rustのエラー処理については、第9章でもっと議論します。
関数
関数は、Rustのコードにおいてよく見かける存在です。既に、言語において最も重要な関数のうちの一つを目撃していますね:
そう、main
関数です。これは、多くのプログラムのエントリーポイント(訳注
: プログラム実行時に最初に走る関数のこと)になります。
fn
キーワードもすでに見かけましたね。これによって新しい関数を宣言することができます。
Rustの関数と変数の命名規則は、スネークケース(訳注
: some_variableのような命名規則)を使うのが慣例です。
スネークケースとは、全文字を小文字にし、単語区切りにアンダースコアを使うことです。
以下のプログラムで、サンプルの関数定義をご覧ください:
ファイル名: src/main.rs
fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); // 別の関数 }
Rustにおいて関数定義は、fn
キーワードで始まり、関数名の後に丸かっこの組が続きます。
波かっこが、コンパイラに関数本体の開始と終了の位置を伝えます。
定義した関数は、名前に丸かっこの組を続けることで呼び出すことができます。
another_function
関数がプログラム内で定義されているので、main
関数内から呼び出すことができるわけです。
ソースコード中でanother_function
をmain
関数の後に定義していることに注目してください;
勿論、main関数の前に定義することもできます。コンパイラは、関数がどこで定義されているかは気にしません。
どこかで定義されていることのみ気にします。
functionsという名前の新しいバイナリ生成プロジェクトを始めて、関数についてさらに深く探究していきましょう。
another_function
の例をsrc/main.rsファイルに配置して、走らせてください。
以下のような出力が得られるはずです:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/functions`
Hello, world!
Another function.
行出力は、main
関数内に書かれた順序で実行されています。最初に"Hello, world"メッセージが出て、
それからanother_function
が呼ばれて、こちらのメッセージが出力されています。
関数の引数
関数は、引数を持つようにも定義できます。引数とは、関数シグニチャの一部になる特別な変数のことです。
関数に引数があると、引数の位置に実際の値を与えることができます。技術的にはこの実際の値は
実引数と呼ばれますが、普段の会話では、仮引数("parameter")と実引数("argument")を関数定義の変数と関数呼び出し時に渡す実際の値、
両方の意味に区別なく使います(訳注
: 日本語では、特別区別する意図がない限り、どちらも単に引数と呼ぶことが多いでしょう)。
以下の書き直したanother_function
では、Rustの仮引数がどのようなものかを示しています:
ファイル名: src/main.rs
fn main() { another_function(5); } fn another_function(x: i32) { println!("The value of x is: {}", x); // xの値は{}です }
このプログラムを走らせてみてください; 以下のような出力が得られるはずです:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
another_function
の宣言には、x
という名前の仮引数があります。x
の型は、
i32
と指定されています。値5
がanother_function
に渡されると、println!
マクロにより、
フォーマット文字列中の1組の波かっこがあった位置に値5
が出力されます。
関数シグニチャにおいて、各仮引数の型を宣言しなければなりません。これは、Rustの設計において、 意図的な判断です: 関数定義で型注釈が必要不可欠ということは、コンパイラがその意図するところを推し量るのに、 プログラマがコードの他の箇所で使用する必要がないということを意味します。
関数に複数の仮引数を持たせたいときは、仮引数定義をカンマで区切ってください。 こんな感じです:
ファイル名: src/main.rs
fn main() { print_labeled_measurement(5, 'h'); } fn print_labeled_measurement(value: i32, unit_label: char) { println!("The measurement is: {}{}", value, unit_label); }
この例では、2引数の関数を生成しています。そして、引数はどちらもi32
型です。それからこの関数は、
仮引数の値を両方出力します。関数引数は、全てが同じ型である必要はありません。今回は、
偶然同じになっただけです。
このコードを走らせてみましょう。今、functionプロジェクトのsrc/main.rsファイルに記載されているプログラムを先ほどの例と置き換えて、
cargo run
で走らせてください:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
x
に対して値5
、y
に対して値6
を渡して関数を呼び出したので、この二つの文字列は、
この値で出力されました。
関数本体は、文と式を含む
関数本体は、文が並び、最後に式を置くか文を置くという形で形成されます。現在までには、 式で終わらない関数だけを見てきたわけですが、式が文の一部になっているものなら見かけましたね。Rustは、式指向言語なので、 これは理解しておくべき重要な差異になります。他の言語にこの差異はありませんので、文と式がなんなのかと、 その違いが関数本体にどんな影響を与えるかを見ていきましょう。
実のところ、もう文と式は使っています。文とは、なんらかの動作をして値を返さない命令です。 式は結果値に評価されます。ちょっと例を眺めてみましょう。
let
キーワードを使用して変数を生成し、値を代入することは文になります。
リスト3-1でlet y = 6;
は文です。
ファイル名: src/main.rs
fn main() { let y = 6; }
リスト3-1: 1文を含むmain
関数宣言
関数定義も文になります。つまり、先の例は全体としても文になるわけです。
文は値を返しません。故に、let
文を他の変数に代入することはできません。
以下のコードではそれを試みていますが、エラーになります:
ファイル名: src/main.rs
fn main() {
let x = (let y = 6);
}
このプログラムを実行すると、以下のようなエラーが出るでしょう:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found statement (`let`)
(エラー: 式を予期しましたが、文が見つかりました (`let`))
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^^^^^^^
|
= note: variable declaration using `let` is a statement
(注釈: `let`を使う変数宣言は、文です)
error[E0658]: `let` expressions in this position are experimental
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^^^^^^^
|
= note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information
= help: you can write `matches!(<expr>, <pattern>)` instead of `let <pattern> = <expr>`
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
2 - let x = (let y = 6);
2 + let x = let y = 6;
|
For more information about this error, try `rustc --explain E0658`.
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` due to 2 previous errors; 1 warning emitted
このlet y = 6
という文は値を返さないので、x
に束縛するものがないわけです。これは、
CやRubyなどの言語とは異なる動作です。CやRubyでは、代入は代入値を返します。これらの言語では、
x = y = 6
と書いて、x
もy
も値6になるようにできるのですが、Rustにおいては、
そうは問屋が卸さないわけです。
式は何かに評価され、これからあなたが書くRustコードの多くを構成します。
簡単な数学演算(5 + 6
など)を思い浮かべましょう。この例は、値11
に評価される式です。式は文の一部になりえます:
リスト3-1において、let y = 6
という文の6
は値6
に評価される式です。関数呼び出しも式です。マクロ呼び出しも式です。
新しいスコープを作る際に使用するブロック({}
)も式です:
ファイル名: src/main.rs
fn main() { let y = { let x = 3; x + 1 }; println!("The value of y is: {}", y); }
以下の式:
{
let x = 3;
x + 1
}
は今回の場合、4
に評価されるブロックです。その値が、let
文の一部としてy
に束縛されます。
今まで見かけてきた行と異なり、文末にセミコロンがついていないx + 1
の行に気をつけてください。
式は終端にセミコロンを含みません。式の終端にセミコロンを付けたら、文に変えてしまいます。そして、文は値を返しません。
次に関数の戻り値や式を見ていく際にこのことを肝に銘じておいてください。
戻り値のある関数
関数は、それを呼び出したコードに値を返すことができます。戻り値に名前を付けはしませんが、
矢印(->
)の後に型を書いて確かに宣言します。Rustでは、関数の戻り値は、関数本体ブロックの最後の式の値と同義です。
return
キーワードで関数から早期リターンし、値を指定することもできますが、多くの関数は最後の式を暗黙的に返します。
こちらが、値を返す関数の例です:
ファイル名: src/main.rs
fn five() -> i32 { 5 } fn main() { let x = five(); println!("The value of x is: {}", x); }
five
関数内には、関数呼び出しもマクロ呼び出しも、let
文でさえ存在しません。数字の5が単独であるだけです。
これは、Rustにおいて、完璧に問題ない関数です。関数の戻り値型が-> i32
と指定されていることにも注目してください。
このコードを実行してみましょう; 出力はこんな感じになるはずです:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
The value of x is: 5
five
内の5
が関数の戻り値です。だから、戻り値型がi32
なのです。これについてもっと深く考察しましょう。
重要な箇所は2つあります: まず、let x = five()
という行は、関数の戻り値を使って変数を初期化していることを示しています。
関数five
は5
を返すので、この行は以下のように書くのと同義です:
#![allow(unused)] fn main() { let x = 5; }
2番目に、five
関数は仮引数をもたず、戻り値型を定義していますが、関数本体はセミコロンなしの5
単独です。
なぜなら、これが返したい値になる式だからです。
もう一つ別の例を見ましょう:
ファイル名: src/main.rs
fn main() { let x = plus_one(5); println!("The value of x is: {}", x); } fn plus_one(x: i32) -> i32 { x + 1 }
このコードを走らせると、The value of x is: 6
と出力されるでしょう。しかし、
x + 1
を含む行の終端にセミコロンを付けて、式から文に変えたら、エラーになるでしょう:
ファイル名: src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {}", x);
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
このコードを実行すると、以下のようにエラーが出ます:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
(型が合いません)
--> src/main.rs:7:24
|
7 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8 | x + 1;
| - help: consider removing this semicolon
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` due to previous error
メインのエラーメッセージである「型が合いません」でこのコードの根本的な問題が明らかになるでしょう。
関数plus_one
の定義では、i32
型を返すと言っているのに、文は値に評価されないからです。このことは、
()
、つまり空のタプルとして表現されています。それゆえに、何も戻り値がなく、これが関数定義と矛盾するので、
結果としてエラーになるわけです。この出力内で、コンパイラは問題を修正する手助けになりそうなメッセージも出していますね:
セミコロンを削除するよう提言しています。そして、そうすれば、エラーは直るわけです。
コメント
全プログラマは、自分のコードがわかりやすくなるよう努めますが、時として追加の説明が許されることもあります。 このような場合、プログラマは注釈またはコメントをソースコードに残し、コメントをコンパイラは無視しますが、 ソースコードを読む人間には有益なものと思えるでしょう。
こちらが単純なコメントです:
#![allow(unused)] fn main() { // hello, world }
Rustでは、コメントは2連スラッシュで始め、行の終わりまで続きます。コメントが複数行にまたがる場合、
各行に//
を含める必要があります。こんな感じに:
#![allow(unused)] fn main() { // So we’re doing something complicated here, long enough that we need // multiple lines of comments to do it! Whew! Hopefully, this comment will // explain what’s going on. // ここで何か複雑なことをしていて、長すぎるから複数行のコメントが必要なんだ。 // ふう!願わくば、このコメントで何が起きているか説明されていると嬉しい。 }
コメントは、コードが書かれた行の末尾にも配置することができます:
Filename: src/main.rs
fn main() { let lucky_number = 7; // I’m feeling lucky today(今日はラッキーな気がするよ) }
しかし、こちらの形式のコメントの方が見かける機会は多いでしょう。注釈しようとしているコードの1行上に書く形式です:
ファイル名: src/main.rs
fn main() { // I’m feeling lucky today // 今日はラッキーな気がするよ let lucky_number = 7; }
Rustには他の種類のコメント、ドキュメントコメントもあり、それについては第14章で議論します。
制御フロー
条件が真かどうかによってコードを走らせるかどうかを決定したり、
条件が真の間繰り返しコードを走らせるか決定したりすることは、多くのプログラミング言語において、基本的な構成ブロックです。
Rustコードの実行フローを制御する最も一般的な文法要素は、if
式とループです。
if
式
if式によって、条件に依存して枝分かれをさせることができます。条件を与え、以下のように宣言します。 「もし条件が合ったら、この一連のコードを実行しろ。条件に合わなければ、この一連のコードは実行するな」と。
projectsディレクトリにbranchesという名のプロジェクトを作ってif
式について掘り下げていきましょう。
src/main.rsファイルに、以下のように入力してください:
ファイル名: src/main.rs
fn main() { let number = 3; if number < 5 { println!("condition was true"); // 条件は真でした } else { println!("condition was false"); // 条件は偽でした } }
if
式は全て、キーワードのif
から始め、条件式を続けます。今回の場合、
条件式は変数number
が5未満の値になっているかどうかをチェックします。
条件が真の時に実行したい一連のコードを条件式の直後に波かっこで包んで配置します。if
式の条件式と紐付けられる一連のコードは、
時としてアームと呼ばれることがあります。
第2章の「予想と秘密の数字を比較する」の節で議論したmatch
式のアームと同じです。
オプションとして、else
式を含むこともでき(ここではそうしています)、これによりプログラムは、
条件式が偽になった時に実行するコードを与えられることになります。仮に、else
式を与えずに条件式が偽になったら、
プログラムは単にif
ブロックを飛ばして次のコードを実行しにいきます。
このコードを走らせてみましょう; 以下のような出力を目の当たりにするはずです:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was true
number
の値を条件がfalse
になるような値に変更してどうなるか確かめてみましょう:
fn main() {
let number = 7;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
再度プログラムを実行して、出力に注目してください:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was false
このコード内の条件式は、bool
型でなければならないことにも触れる価値があります。
条件式が、bool
型でない時は、エラーになります。例えば、試しに以下のコードを実行してみてください:
ファイル名: src/main.rs
fn main() {
let number = 3;
if number {
println!("number was three"); // 数値は3です
}
}
今回、if
の条件式は3
という値に評価され、コンパイラがエラーを投げます:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
(型が合いません)
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected `bool`, found integer
| (bool型を予期したのに、整数変数が見つかりました)
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error
このエラーは、コンパイラはbool
型を予期していたのに、整数だったことを示唆しています。
RubyやJavaScriptなどの言語とは異なり、Rustでは、論理値以外の値が、自動的に論理値に変換されることはありません。
明示し、必ずif
には条件式として、論理値
を与えなければなりません。
例えば、数値が0
以外の時だけif
のコードを走らせたいなら、以下のようにif
式を変更することができます:
ファイル名: src/main.rs
fn main() { let number = 3; if number != 0 { println!("number was something other than zero"); // 数値は0以外の何かです } }
このコードを実行したら、number was something other than zero
と表示されるでしょう。
else if
で複数の条件を扱う
if
とelse
を組み合わせてelse if
式にすることで複数の条件を持たせることもできます。例です:
ファイル名: src/main.rs
fn main() { let number = 6; if number % 4 == 0 { // 数値は4で割り切れます println!("number is divisible by 4"); } else if number % 3 == 0 { // 数値は3で割り切れます println!("number is divisible by 3"); } else if number % 2 == 0 { // 数値は2で割り切れます println!("number is divisible by 2"); } else { // 数値は4、3、2で割り切れません println!("number is not divisible by 4, 3, or 2"); } }
このプログラムには、通り道が4つあります。実行後、以下のような出力を目の当たりにするはずです:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
number is divisible by 3
このプログラムを実行すると、if
式が順番に吟味され、最初に条件が真になった本体が実行されます。
6は2で割り切れるものの、number is devisible by 2
や、
else
ブロックのnumber is not divisible by 4, 3, or 2
という出力はされないことに注目してください。
それは、Rustが最初の真条件のブロックのみを実行し、
条件に合ったものが見つかったら、残りはチェックすらしないからです。
else if
式を使いすぎると、コードがめちゃくちゃになってしまうので、1つ以上あるなら、
コードをリファクタリングしたくなるかもしれません。これらのケースに有用なmatch
と呼ばれる、
強力なRustの枝分かれ文法要素については第6章で解説します。
let
文内でif
式を使う
if
は式なので、let
文の右辺に持ってくることができます。リスト3-2のようにですね。
ファイル名: src/main.rs
fn main() { let condition = true; let number = if condition { 5 } else { 6 }; // numberの値は、{}です println!("The value of number is: {}", number); }
リスト3-2: if
式の結果を変数に代入する
このnumber
変数は、if
式の結果に基づいた値に束縛されます。このコードを走らせてどうなるか確かめてください:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
The value of number is: 5
一連のコードは、そのうちの最後の式に評価され、数値はそれ単独でも式になることを思い出してください。
今回の場合、このif
式全体の値は、どのブロックのコードが実行されるかに基づきます。これはつまり、
if
の各アームの結果になる可能性がある値は、同じ型でなければならないということになります;
リスト3-2で、if
アームもelse
アームも結果は、i32
の整数でした。以下の例のように、
型が合わない時には、エラーになるでしょう:
ファイル名: src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {}", number);
}
このコードをコンパイルしようとすると、エラーになります。if
とelse
アームは互換性のない値の型になり、
コンパイラがプログラム内で問題の見つかった箇所をズバリ指摘してくれます:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
(ifとelseの型に互換性がありません)
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| (整数変数を予期しましたが、&strが見つかりました)
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error
if
ブロックの式は整数に評価され、else
ブロックの式は文字列に評価されます。これでは動作しません。
変数は単独の型でなければならないからです。コンパイラは、コンパイル時にnumber
変数の型を確実に把握する必要があるため、
コンパイル時にnumber
が使われている箇所全部で型が有効かどうか検査することができるのです。
number
の型が実行時にしか決まらないのであれば、コンパイラはそれを実行することができなくなってしまいます;
どの変数に対しても、架空の複数の型があることを追いかけなければならないのであれば、コンパイラはより複雑になり、
コードに対して行える保証が少なくなってしまうでしょう。
ループでの繰り返し
一連のコードを1回以上実行できると、しばしば役に立ちます。この作業用に、 Rustにはいくつかのループが用意されています。ループは、本体内のコードを最後まで実行し、 直後にまた最初から処理を開始します。 ループを試してみるのに、loopsという名の新プロジェクトを作りましょう。
Rustには3種類のループが存在します: loop
とwhile
とfor
です。それぞれ試してみましょう。
loop
でコードを繰り返す
loop
キーワードを使用すると、同じコードを何回も何回も永遠に、明示的にやめさせるまで実行します。
例として、loopsディレクトリのsrc/main.rsファイルを以下のような感じに書き換えてください:
ファイル名: src/main.rs
fn main() {
loop {
println!("again!"); // また
}
}
このプログラムを実行すると、プログラムを手動で止めるまで、何度も何度も続けてagain!
と出力するでしょう。
ほとんどの端末でctrl-cというショートカットが使え、
永久ループに囚われてしまったプログラムを終了させられます。試しにやってみましょう:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
^C
という記号が出た場所が、ctrl-cを押した場所です。^C
の後にはagain!
と表示されたり、
されなかったりします。ストップシグナルをコードが受け取った時にループのどこにいたかによります。
幸いなことに、Rustにはループを抜け出す別のより信頼できる手段があります。
ループ内にbreak
キーワードを配置することで、プログラムに実行を終了すべきタイミングを教えることができます。
第2章の「正しい予想をした後に終了する」節の数当てゲーム内でこれをして、ユーザが予想を的中させ、
ゲームに勝った時にプログラムを終了させたことを思い出してください。
数当てゲームでcontinue
を使用しました。continue
はループの中で残っているコードをスキップして次のループに移るためのものです。
ループ内にループがある場合、break
とcontinue
は最も内側のループに適用されます。
ループラベルを使用することで、break
やcontinue
が適用されるループを指定することができます。
以下に例を示します。
fn main() { let mut count = 0; 'counting_up: loop { println!("count = {}", count); let mut remaining = 10; loop { println!("remaining = {}", remaining); if remaining == 9 { break; } if count == 2 { break 'counting_up; } remaining -= 1; } count += 1; } println!("End count = {}", count); }
外側のループには'counting_up
というラベルがついていて、0から2まで数え上げます。
内側のラベルのないループは10から9までカウントダウンします。最初のラベルの無いbreak
は内側のループを終了させます。
break 'counting_up;
は外側のループを終了させます。
このコードは以下のような出力をします。
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
while
で条件付きループ
プログラムにとってループ内で条件式を評価できると、有益なことがしばしばあります。条件が真の間、
ループが走るわけです。条件が真でなくなった時にプログラムはbreak
を呼び出し、ループを終了します。
このタイプのループは、loop
、if
、else
、break
を組み合わせることでも実装できます;
お望みなら、プログラムで今、試してみるのもいいでしょう。
しかし、このパターンは頻出するので、Rustにはそれ用の文法要素が用意されていて、while
ループと呼ばれます。
リスト3-3は、while
を使用しています: プログラムは3回ループし、それぞれカウントダウンします。
それから、ループ後に別のメッセージを表示して終了します:
ファイル名: src/main.rs
fn main() { let mut number = 3; while number != 0 { println!("{}!", number); number -= 1; } // 発射! println!("LIFTOFF!!!"); }
リスト3-3: 条件が真の間、コードを走らせるwhile
ループを使用する
この文法要素により、loop
、if
、else
、break
を使った時に必要になるネストがなくなり、
より明確になります。条件が真の間、コードは実行されます; そうでなければ、ループを抜けます.
for
でコレクションを覗き見る
while
要素を使って配列などのコレクションの要素を覗き見ることができます。例えば、リスト3-4を見ましょう。
ファイル名: src/main.rs
fn main() { let a = [10, 20, 30, 40, 50]; let mut index = 0; while index < 5 { // 値は{}です println!("the value is: {}", a[index]); index += 1; } }
リスト3-4: while
ループでコレクションの各要素を覗き見る
ここで、コードは配列の要素を順番にカウントアップして覗いています。番号0から始まり、
配列の最終番号に到達するまでループします(つまり、index < 5
が真でなくなる時です)。
このコードを走らせると、配列内の全要素が出力されます:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50
予想通り、配列の5つの要素が全てターミナルに出力されています。index
変数の値はどこかで5
という値になるものの、
配列から6番目の値を拾おうとする前にループは実行を終了します。
しかし、このアプローチは間違いが発生しやすいです; 添え字の長さが間違っていれば、 プログラムはパニックしてしまいます。また遅いです。 コンパイラが実行時にループの各回ごとに境界値チェックを行うようなコードを追加するからです。
より効率的な対立案として、for
ループを使ってコレクションの各アイテムに対してコードを実行することができます。
for
ループはリスト3-5のこんな見た目です。
ファイル名: src/main.rs
fn main() { let a = [10, 20, 30, 40, 50]; for element in a { println!("the value is: {}", element); } }
リスト3-5: for
ループを使ってコレクションの各要素を覗き見る
このコードを走らせたら、リスト3-4と同じ出力が得られるでしょう。より重要なのは、 コードの安全性を向上させ、配列の終端を超えてアクセスしたり、 終端に届く前にループを終えてアイテムを見逃してしまったりするバグの可能性を完全に排除したことです。
例えば、リスト3-4のコードで、a
配列からアイテムを1つ削除したのに、条件式をwhile index < 4
にするのを忘れていたら、
コードはパニックします。for
ループを使っていれば、配列の要素数を変えても、
他のコードをいじることを覚えておく必要はなくなるわけです。
for
ループのこの安全性と簡潔性により、Rustで使用頻度の最も高いループになっています。
リスト3-3でwhile
ループを使ったカウントダウンサンプルのように、一定の回数、同じコードを実行したいような状況であっても、
多くのRustaceanは、for
ループを使うでしょう。どうやってやるかといえば、
Range
型を使うのです。Range型は、標準ライブラリで提供される片方の数字から始まって、
もう片方の数字未満の数値を順番に生成する型です。
for
ループと、まだ話していない別のメソッドrev
を使って範囲を逆順にしたカウントダウンはこうなります:
ファイル名: src/main.rs
fn main() { for number in (1..4).rev() { println!("{}!", number); } println!("LIFTOFF!!!"); }
こちらのコードの方が少しいいでしょう?
まとめ
やりましたね!結構長い章でした: 変数、スカラー値と複合データ型、関数、コメント、if
式、そして、ループについて学びました!
この章で議論した概念について経験を積みたいのであれば、以下のことをするプログラムを組んでみてください:
- 温度を華氏と摂氏で変換する。
- フィボナッチ数列のn番目を生成する。
- クリスマスキャロルの定番、"The Twelve Days of Christmas"の歌詞を、 曲の反復性を利用して出力する。
次に進む準備ができたら、他の言語にはあまり存在しないRustの概念について話しましょう: 所有権です。
所有権を理解する
所有権はRustの最もユニークな機能であり、これのおかげでガベージコレクタなしで安全性担保を行うことができるのです。 故に、Rustにおいて、所有権がどう動作するのかを理解するのは重要です。この章では、所有権以外にも、関連する機能を いくつか話していきます: 借用、スライス、そして、コンパイラがデータをメモリにどう配置するかです。
所有権とは?
Rustの中心的な機能は、所有権です。この機能は、説明するのは簡単なのですが、言語の残りの機能全てにかかるほど 深い裏の意味を含んでいるのです。
全てのプログラムは、実行中にコンピュータのメモリの使用方法を管理する必要があります。プログラムが動作するにつれて、 定期的に使用されていないメモリを検索するガベージコレクションを持つ言語もありますが、他の言語では、 プログラマが明示的にメモリを確保したり、解放したりしなければなりません。Rustでは第3の選択肢を取っています: メモリは、コンパイラがコンパイル時にチェックする一定の規則とともに所有権システムを通じて管理されています。 どの所有権機能も、実行中にプログラムの動作を遅くすることはありません。
所有権は多くのプログラマにとって新しい概念なので、慣れるまでに時間がかかります。 嬉しいことに、Rustと、所有権システムの規則の経験を積むと、より自然に安全かつ効率的なコードを構築できるようになります。 その調子でいきましょう!
所有権を理解した時、Rustを際立たせる機能の理解に対する強固な礎を得ることになるでしょう。この章では、 非常に一般的なデータ構造に着目した例を取り扱うことで所有権を学んでいきます: 文字列です。
スタックとヒープ
多くのプログラミング言語において、スタックとヒープについて考える機会はそう多くないでしょう。 しかし、Rustのようなシステムプログラミング言語においては、値がスタックに積まれるかヒープに置かれるかは、 言語の振る舞い方や、特定の決断を下す理由などに影響以上のものを与えるのです。 この章の後半でスタックとヒープを交えて所有権の一部が解説されるので、ここでちょっと予行演習をしておきましょう。
スタックもヒープも、実行時にコードが使用できるメモリの一部になりますが、異なる手段で構成されています。 スタックは、得た順番に値を並べ、逆の順で値を取り除いていきます。これは、 last in, first out(
訳注
: あえて日本語にするなら、「最後に入れたものが最初に出てくる」といったところでしょうか)と呼ばれます。 お皿の山を思い浮かべてください: お皿を追加する時には、山の一番上に置き、お皿が必要になったら、一番上から1枚を取り去りますよね。 途中や一番下に追加したり、取り除いたりすることもできません。データを追加することは、 スタックにpushするといい、データを取り除くことは、スタックからpopすると表現します(訳注
: 日本語では単純に英語をそのまま活用してプッシュ、ポップと表現するでしょう)。データへのアクセス方法のおかげで、スタックは高速です: 新しいデータを置いたり、 データを取得する場所を探す必要が絶対にないわけです。というのも、その場所は常に一番上だからですね。 スタックを高速にする特性は他にもあり、それはスタック上のデータは全て既知の固定サイズでなければならないということです。
コンパイル時にサイズがわからなかったり、サイズが可変のデータについては、代わりにヒープに格納することができます。 ヒープは、もっとごちゃごちゃしています: ヒープにデータを置く時、あるサイズのスペースを求めます。 OSはヒープ上に十分な大きさの空の領域を見つけ、使用中にし、ポインタを返します。ポインタとは、その場所へのアドレスです。 この過程は、ヒープに領域を確保する(allocating on the heap)と呼ばれ、時としてそのフレーズを単にallocateするなどと省略したりします。 (
訳注
: こちらもこなれた日本語訳はないでしょう。allocateは「メモリを確保する」と訳したいところですが) スタックに値を積むことは、メモリ確保とは考えられません。ポインタは、既知の固定サイズなので、 スタックに保管することができますが、実データが必要になったら、ポインタを追いかける必要があります。レストランで席を確保することを考えましょう。入店したら、グループの人数を告げ、 店員が全員座れる空いている席を探し、そこまで誘導します。もしグループの誰かが遅れて来るのなら、 着いた席の場所を尋ねてあなたを発見することができます。
ヒープへのデータアクセスは、スタックのデータへのアクセスよりも低速です。 ポインタを追って目的の場所に到達しなければならないからです。現代のプロセッサは、メモリをあちこち行き来しなければ、 より速くなります。似た例えを続けましょう。レストランで多くのテーブルから注文を受ける給仕人を考えましょう。最も効率的なのは、 次のテーブルに移らずに、一つのテーブルで全部の注文を受け付けてしまうことです。テーブルAで注文を受け、 それからテーブルBの注文、さらにまたA、それからまたBと渡り歩くのは、かなり低速な過程になってしまうでしょう。 同じ意味で、プロセッサは、 データが隔離されている(ヒープではそうなっている可能性がある)よりも近くにある(スタックではこうなる)ほうが、 仕事をうまくこなせるのです。ヒープに大きな領域を確保する行為も時間がかかることがあります。
コードが関数を呼び出すと、関数に渡された値(ヒープのデータへのポインタも含まれる可能性あり)と、 関数のローカル変数がスタックに載ります。関数の実行が終了すると、それらの値はスタックから取り除かれます。
どの部分のコードがどのヒープ上のデータを使用しているか把握すること、ヒープ上の重複するデータを最小化すること、 メモリ不足にならないようにヒープ上の未使用のデータを掃除することは全て、所有権が解決する問題です。 一度所有権を理解したら、あまり頻繁にスタックとヒープに関して考える必要はなくなるでしょうが、 ヒープデータを管理することが所有権の存在する理由だと知っていると、所有権がありのままで動作する理由を 説明するのに役立つこともあります。
所有権規則
まず、所有権のルールについて見ていきましょう。 この規則を具体化する例を扱っていく間もこれらのルールを肝に銘じておいてください:
- Rustの各値は、所有者と呼ばれる変数と対応している。
- いかなる時も所有者は一つである。
- 所有者がスコープから外れたら、値は破棄される。
変数スコープ
第2章で、Rustプログラムの例はすでに見ています。もう基本的な記法は通り過ぎたので、
fn main() {
というコードはもう例に含みません。従って、例をなぞっているなら、
これからの例はmain
関数に手動で入れ込まなければいけなくなるでしょう。結果的に、例は少々簡潔になり、
定型コードよりも具体的な詳細に集中しやすくなります。
所有権の最初の例として、何らかの変数のスコープについて見ていきましょう。スコープとは、 要素が有効になるプログラム内の範囲のことです。以下のような変数があるとしましょう:
#![allow(unused)] fn main() { let s = "hello"; }
変数s
は、文字列リテラルを参照し、ここでは、文字列の値はプログラムのテキストとしてハードコードされています。
この変数は、宣言された地点から、現在のスコープの終わりまで有効になります。リスト4-1には、
変数s
が有効な場所に関する注釈がコメントで付記されています。
#![allow(unused)] fn main() { { // sは、ここでは有効ではない。まだ宣言されていない let s = "hello"; // sは、ここから有効になる // sで作業をする } // このスコープは終わり。もうsは有効ではない }
リスト4-1: 変数と有効なスコープ
言い換えると、ここまでに重要な点は二つあります:
s
がスコープに入ると、有効になる- スコープを抜けるまで、有効なまま
ここで、スコープと変数が有効になる期間の関係は、他の言語に類似しています。さて、この理解のもとに、
String
型を導入して構築していきましょう。
String
型
所有権の規則を具体化するには、第3章の「データ型」節で講義したものよりも、より複雑なデータ型が必要になります。 以前講義した型は全てスタックに保管され、スコープが終わるとスタックから取り除かれますが、 ヒープに確保されるデータ型を観察して、 コンパイラがどうそのデータを掃除すべきタイミングを把握しているかを掘り下げていきたいと思います。
ここでは、例としてString
型を使用し、String
型の所有権にまつわる部分に着目しましょう。
また、この観点は、標準ライブラリや自分で生成する他の複雑なデータ型にも適用されます。
String
型については、第8章でより深く議論します。
既に文字列リテラルは見かけましたね。文字列リテラルでは、文字列の値はプログラムにハードコードされます。
文字列リテラルは便利ですが、テキストを使いたいかもしれない場面全てに最適なわけではありません。一因は、
文字列リテラルが不変であることに起因します。別の原因は、コードを書く際に、全ての文字列値が判明するわけではないからです:
例えば、ユーザ入力を受け付け、それを保持したいとしたらどうでしょうか?このような場面用に、Rustには、
2種類目の文字列型、String
型があります。この型はヒープにメモリを確保するので、
コンパイル時にはサイズが不明なテキストも保持することができるのです。from
関数を使用して、
文字列リテラルからString
型を生成できます。以下のように:
#![allow(unused)] fn main() { let s = String::from("hello"); }
この二重コロンは、string_from
などの名前を使うのではなく、
String
型直下のfrom
関数を特定する働きをする演算子です。この記法について詳しくは、
第5章の「メソッド記法」節と、第7章の「モジュール定義」でモジュールを使った名前空間分けについて話をするときに議論します。
この種の文字列は、可変化することができます:
#![allow(unused)] fn main() { let mut s = String::from("hello"); s.push_str(", world!"); // push_str()関数は、リテラルをStringに付け加える println!("{}", s); // これは`hello, world!`と出力する }
では、ここでの違いは何でしょうか?なぜ、String
型は可変化できるのに、リテラルはできないのでしょうか?
違いは、これら二つの型がメモリを扱う方法にあります。
メモリと確保
文字列リテラルの場合、中身はコンパイル時に判明しているので、テキストは最終的なバイナリファイルに直接ハードコードされます。 このため、文字列リテラルは、高速で効率的になるのです。しかし、これらの特性は、 その文字列リテラルの不変性にのみ端を発するものです。残念なことに、コンパイル時にサイズが不明だったり、 プログラム実行に合わせてサイズが可変なテキスト片用に一塊のメモリをバイナリに確保しておくことは不可能です。
String
型では、可変かつ伸長可能なテキスト破片をサポートするために、コンパイル時には不明な量のメモリを
ヒープに確保して内容を保持します。つまり:
- メモリは、実行時にOSに要求される。
String
型を使用し終わったら、OSにこのメモリを返還する方法が必要である。
この最初の部分は、既にしています: String::from
関数を呼んだら、その実装が必要なメモリを要求するのです。
これは、プログラミング言語において、極めて普遍的です。
しかしながら、2番目の部分は異なります。ガベージコレクタ(GC)付きの言語では、GCがこれ以上、
使用されないメモリを検知して片付けるため、プログラマは、そのことを考慮する必要はありません。
GCがないなら、メモリがもう使用されないことを見計らって、明示的に返還するコードを呼び出すのは、
プログラマの責任になります。ちょうど要求の際にしたようにですね。これを正確にすることは、
歴史的にも難しいプログラミング問題の一つであり続けています。もし、忘れていたら、メモリを無駄にします。
タイミングが早すぎたら、無効な変数を作ってしまいます。2回解放してしまっても、バグになるわけです。
allocate
とfree
は完璧に1対1対応にしなければならないのです。
Rustは、異なる道を歩んでいます: ひとたび、メモリを所有している変数がスコープを抜けたら、
メモリは自動的に返還されます。こちらの例は、
リスト4-1のスコープ例を文字列リテラルからString
型を使うものに変更したバージョンになります:
#![allow(unused)] fn main() { { let s = String::from("hello"); // sはここから有効になる // sで作業をする } // このスコープはここでおしまい。sは // もう有効ではない }
String
型が必要とするメモリをOSに返還することが自然な地点があります: s
変数がスコープを抜ける時です。
変数がスコープを抜ける時、Rustは特別な関数を呼んでくれます。この関数は、drop
と呼ばれ、
ここにString
型の書き手はメモリを返還するコードを配置することができます。Rustは、閉じ波括弧で自動的にdrop
関数を呼び出します。
注釈: C++では、要素の生存期間の終了地点でリソースを解放するこのパターンを時に、 RAII(Resource Aquisition Is Initialization: リソースの獲得は、初期化である)と呼んだりします。 Rustの
drop
関数は、あなたがRAIIパターンを使ったことがあれば、馴染み深いものでしょう。
このパターンは、Rustコードの書かれ方に甚大な影響をもたらします。現状は簡単そうに見えるかもしれませんが、 ヒープ上に確保されたデータを複数の変数に使用させるようなもっと複雑な場面では、コードの振る舞いは、 予期しないものになる可能性もあります。これから、そのような場面を掘り下げてみましょう。
変数とデータの相互作用法: ムーブ
Rustにおいては、複数の変数が同じデータに対して異なる手段で相互作用することができます。 整数を使用したリスト4-2の例を見てみましょう。
#![allow(unused)] fn main() { let x = 5; let y = x; }
リスト4-2: 変数x
の整数値をy
に代入する
もしかしたら、何をしているのか予想することができるでしょう:
「値5
をx
に束縛する; それからx
の値をコピーしてy
に束縛する。」これで、
二つの変数(x
とy
)が存在し、両方、値は5
になりました。これは確かに起こっている現象を説明しています。
なぜなら、整数は既知の固定サイズの単純な値で、これら二つの5
という値は、スタックに積まれるからです。
では、String
バージョンを見ていきましょう:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1; }
このコードは先ほどのコードに酷似していますので、動作方法も同じだと思い込んでしまうかもしれません:
要するに、2行目でs1
の値をコピーし、s2
に束縛するということです。ところが、
これは全く起こることを言い当てていません。
図4-1を見て、ベールの下でString
に何が起きているかを確かめてください。
String
型は、左側に示されているように、3つの部品でできています:
文字列の中身を保持するメモリへのポインタと長さ、そして、許容量です。この種のデータは、スタックに保持されます。
右側には、中身を保持したヒープ上のメモリがあります。
図4-1: s1
に束縛された"hello"
という値を保持するString
のメモリ上の表現
長さは、String
型の中身が現在使用しているメモリ量をバイトで表したものです。許容量は、
String
型がOSから受け取った全メモリ量をバイトで表したものです。長さと許容量の違いは問題になることですが、
この文脈では違うので、とりあえずは、許容量を無視しても構わないでしょう。
s1
をs2
に代入すると、String
型のデータがコピーされます。つまり、スタックにあるポインタ、長さ、
許容量をコピーするということです。ポインタが指すヒープ上のデータはコピーしません。言い換えると、
メモリ上のデータ表現は図4-2のようになるということです。
図4-2: s1
のポインタ、長さ、許容量のコピーを保持する変数s2
のメモリ上での表現
メモリ上の表現は、図4-3のようにはなりません。これは、
Rustが代わりにヒープデータもコピーするという選択をしていた場合のメモリ表現ですね。Rustがこれをしていたら、
ヒープ上のデータが大きい時にs2 = s1
という処理の実行時性能がとても悪くなっていた可能性があるでしょう。
図4-3: Rustがヒープデータもコピーしていた場合にs2 = s1
という処理が行なった可能性のあること
先ほど、変数がスコープを抜けたら、Rustは自動的にdrop
関数を呼び出し、
その変数が使っていたヒープメモリを片付けると述べました。しかし、図4-2は、
両方のデータポインタが同じ場所を指していることを示しています。これは問題です: s2
とs1
がスコープを抜けたら、
両方とも同じメモリを解放しようとします。これは二重解放エラーとして知られ、以前触れたメモリ安全性上のバグの一つになります。
メモリを2回解放することは、memory corruption (訳注
: メモリの崩壊。意図せぬメモリの書き換え) につながり、
セキュリティ上の脆弱性を生む可能性があります。
メモリ安全性を保証するために、Rustにおいてこの場面で起こることの詳細がもう一つあります。
確保されたメモリをコピーしようとする代わりに、コンパイラは、s1
が最早有効ではないと考え、
故にs1
がスコープを抜けた際に何も解放する必要がなくなるわけです。s2
の生成後にs1
を使用しようとしたら、
どうなるかを確認してみましょう。動かないでしょう:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
コンパイラが無効化された参照は使用させてくれないので、以下のようなエラーが出るでしょう:
error[E0382]: use of moved value: `s1`
(ムーブされた値の使用: `s1`)
--> src/main.rs:5:28
|
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{}, world!", s1);
| ^^ value used here after move
| (ムーブ後にここで使用されています)
|
= note: move occurs because `s1` has type `std::string::String`, which does
not implement the `Copy` trait
(注釈: ムーブが起きたのは、`s1`が`std::string::String`という
`Copy`トレイトを実装していない型だからです)
他の言語を触っている間に"shallow copy"と"deep copy"という用語を耳にしたことがあるなら、
データのコピーなしにポインタと長さ、許容量をコピーするという概念は、shallow copyのように思えるかもしれません。
ですが、コンパイラは最初の変数をも無効化するので、shallow copyと呼ばれる代わりに、
ムーブとして知られているわけです。この例では、s1
はs2
にムーブされたと表現するでしょう。
以上より、実際に起きることを図4-4に示してみました。
図4-4: s1
が無効化された後のメモリ表現
これにて一件落着です。s2
だけが有効なので、スコープを抜けたら、それだけがメモリを解放して、
終わりになります。
付け加えると、これにより暗示される設計上の選択があります: Rustでは、 自動的にデータの"deep copy"が行われることは絶対にないわけです。それ故に、あらゆる自動コピーは、実行時性能の観点で言うと、 悪くないと考えてよいことになります。
変数とデータの相互作用法: クローン
仮に、スタック上のデータだけでなく、本当にString
型のヒープデータのdeep copyが必要ならば、
clone
と呼ばれるよくあるメソッドを使うことができます。メソッド記法については第5章で議論しますが、
メソッドは多くのプログラミング言語に見られる機能なので、以前に見かけたこともあるんじゃないでしょうか。
これは、clone
メソッドの動作例です:
#![allow(unused)] fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); }
これは問題なく動作し、図4-3で示した動作を明示的に生み出します。ここでは、 ヒープデータが実際にコピーされています。
clone
メソッドの呼び出しを見かけたら、何らかの任意のコードが実行され、その実行コストは高いと把握できます。
何か違うことが起こっているなと見た目でわかるわけです。
スタックのみのデータ: コピー
まだ話題にしていない別の問題があります。 この整数を使用したコードは、一部をリスト4-2で示しましたが、うまく動作する有効なものです:
#![allow(unused)] fn main() { let x = 5; let y = x; println!("x = {}, y = {}", x, y); }
ですが、このコードは一見、今学んだことと矛盾しているように見えます:
clone
メソッドの呼び出しがないのに、x
は有効で、y
にムーブされませんでした。
その理由は、整数のようなコンパイル時に既知のサイズを持つ型は、スタック上にすっぽり保持されるので、
実際の値をコピーするのも高速だからです。これは、変数y
を生成した後にもx
を無効化したくなる理由がないことを意味します。
換言すると、ここでは、shallow copyとdeep copyの違いがないことになり、
clone
メソッドを呼び出しても、一般的なshallow copy以上のことをしなくなり、
そのまま放置しておけるということです。
RustにはCopy
トレイトと呼ばれる特別な注釈があり、
整数のようなスタックに保持される型に対して配置することができます(トレイトについては第10章でもっと詳しく話します)。
型がCopy
トレイトに適合していれば、代入後も古い変数が使用可能になります。コンパイラは、
型やその一部分でもDrop
トレイトを実装している場合、Copy
トレイトによる注釈をさせてくれません。
型の値がスコープを外れた時に何か特別なことを起こす必要がある場合に、Copy
注釈を追加すると、コンパイルエラーが出ます。
型にCopy
注釈をつける方法について学ぶには、付録Cの「導出可能なトレイト」をご覧ください。
では、どの型がCopy
なのでしょうか?ある型について、ドキュメントをチェックすればいいのですが、
一般規則として、単純なスカラー値の集合は何でもCopy
であり、メモリ確保が必要だったり、
何らかの形態のリソースだったりするものはCopy
ではありません。ここにCopy
の型の一部を並べておきます。
- あらゆる整数型。
u32
など。 - 論理値型である
bool
。true
とfalse
という値がある。 - あらゆる浮動小数点型、
f64
など。 - 文字型である
char
。 - タプル。ただ、
Copy
の型だけを含む場合。例えば、(i32, i32)
はCopy
だが、(i32, String)
は違う。
所有権と関数
意味論的に、関数に値を渡すことと、値を変数に代入することは似ています。関数に変数を渡すと、 代入のようにムーブやコピーされます。リスト4-3は変数がスコープに入ったり、 抜けたりする地点について注釈してある例です。
ファイル名: src/main.rs
fn main() { let s = String::from("hello"); // sがスコープに入る takes_ownership(s); // sの値が関数にムーブされ... // ... ここではもう有効ではない let x = 5; // xがスコープに入る makes_copy(x); // xも関数にムーブされるが、 // i32はCopyなので、この後にxを使っても // 大丈夫 } // ここでxがスコープを抜け、sもスコープを抜ける。ただし、sの値はムーブされているので、何も特別なことは起こらない。 // fn takes_ownership(some_string: String) { // some_stringがスコープに入る。 println!("{}", some_string); } // ここでsome_stringがスコープを抜け、`drop`が呼ばれる。後ろ盾してたメモリが解放される。 // fn makes_copy(some_integer: i32) { // some_integerがスコープに入る println!("{}", some_integer); } // ここでsome_integerがスコープを抜ける。何も特別なことはない。
リスト4-3: 所有権とスコープが注釈された関数群
takes_ownership
の呼び出し後にs
を呼び出そうとすると、コンパイラは、コンパイルエラーを投げるでしょう。
これらの静的チェックにより、ミスを犯さないでいられます。s
やx
を使用するコードをmain
に追加してみて、
どこで使えて、そして、所有権規則により、どこで使えないかを確認してください。
戻り値とスコープ
値を返すことでも、所有権は移動します。リスト4-4は、リスト4-3と似た注釈のついた例です。
ファイル名: src/main.rs
fn main() { let s1 = gives_ownership(); // gives_ownershipは、戻り値をs1に // ムーブする let s2 = String::from("hello"); // s2がスコープに入る let s3 = takes_and_gives_back(s2); // s2はtakes_and_gives_backにムーブされ // 戻り値もs3にムーブされる } // ここで、s3はスコープを抜け、ドロップされる。s2もスコープを抜けるが、ムーブされているので、 // 何も起きない。s1もスコープを抜け、ドロップされる。 fn gives_ownership() -> String { // gives_ownershipは、戻り値を // 呼び出した関数にムーブする let some_string = String::from("hello"); // some_stringがスコープに入る some_string // some_stringが返され、呼び出し元関数に // ムーブされる } // takes_and_gives_backは、Stringを一つ受け取り、返す。 fn takes_and_gives_back(a_string: String) -> String { // a_stringがスコープに入る。 a_string // a_stringが返され、呼び出し元関数にムーブされる }
リスト4-4: 戻り値の所有権を移動する
変数の所有権は、毎回同じパターンを辿っています: 別の変数に値を代入すると、ムーブされます。
ヒープにデータを含む変数がスコープを抜けると、データが別の変数に所有されるようムーブされていない限り、
drop
により片付けられるでしょう。
所有権を取り、またその所有権を戻す、ということを全ての関数でしていたら、ちょっとめんどくさいですね。 関数に値は使わせるものの所有権を取らないようにさせるにはどうするべきでしょうか。 返したいと思うかもしれない関数本体で発生したあらゆるデータとともに、再利用したかったら、渡されたものをまた返さなきゃいけないのは、 非常に煩わしいことです。
タプルで、複数の値を返すことは可能です。リスト4-5のようにですね。
ファイル名: src/main.rs
fn main() { let s1 = String::from("hello"); let (s2, len) = calculate_length(s1); //'{}'の長さは、{}です println!("The length of '{}' is {}.", s2, len); } fn calculate_length(s: String) -> (String, usize) { let length = s.len(); // len()メソッドは、Stringの長さを返します (s, length) }
リスト4-5: 引数の所有権を返す
でも、これでは、大袈裟すぎますし、ありふれているはずの概念に対して、作業量が多すぎます。 私たちにとって幸運なことに、Rustにはこの概念に対する機能があり、参照と呼ばれます。
参照と借用
リスト4-5のタプルコードの問題は、String
型を呼び出し元の関数に戻さないと、calculate_length
を呼び出した後に、
String
オブジェクトが使えなくなることであり、これはString
オブジェクトがcalculate_length
にムーブされてしまうためでした。
ここで、値の所有権をもらう代わりに引数としてオブジェクトへの参照を取るcalculate_length
関数を定義し、
使う方法を見てみましょう:
ファイル名: src/main.rs
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); // '{}'の長さは、{}です println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
まず、変数宣言と関数の戻り値にあったタプルコードは全てなくなったことに気付いてください。
2番目に、&s1
をcalcuate_length
に渡し、その定義では、String
型ではなく、&String
を受け取っていることに注目してください。
これらのアンド記号が参照であり、これのおかげで所有権をもらうことなく値を参照することができるのです。 図4-5はその図解です。
図4-5: String s1
を指す&String s
の図表
注釈:
&
による参照の逆は、参照外しであり、参照外し演算子の*
で達成できます。 第8章で参照外し演算子の使用例を眺め、第15章で参照外しについて詳しく議論します。
ここの関数呼び出しについて、もっと詳しく見てみましょう:
#![allow(unused)] fn main() { fn calculate_length(s: &String) -> usize { s.len() } let s1 = String::from("hello"); let len = calculate_length(&s1); }
この&s1
という記法により、s1
の値を参照する参照を生成することができますが、これを所有することはありません。
所有してないということは、指している値は、参照がスコープを抜けてもドロップされないということです。
同様に、関数のシグニチャでも、&
を使用して引数s
の型が参照であることを示しています。
説明的な注釈を加えてみましょう:
#![allow(unused)] fn main() { fn calculate_length(s: &String) -> usize { // sはStringへの参照 s.len() } // ここで、sはスコープ外になる。けど、参照しているものの所有権を持っているわけではないので // 何も起こらない }
変数s
が有効なスコープは、あらゆる関数の引数のものと同じですが、所有権はないので、s
がスコープを抜けても、
参照が指しているものをドロップすることはありません。関数が実際の値の代わりに参照を引数に取ると、
所有権をもらわないので、所有権を返す目的で値を返す必要はありません。
関数の引数に参照を取ることを借用と呼びます。現実生活のように、誰かが何かを所有していたら、 それを借りることができます。用が済んだら、返さなきゃいけないわけです。
では、借用した何かを変更しようとしたら、どうなるのでしょうか?リスト4-6のコードを試してください。 ネタバレ注意: 動きません!
ファイル名: src/main.rs
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
リスト4-6: 借用した値を変更しようと試みる
これがエラーです:
error[E0596]: cannot borrow immutable borrowed content `*some_string` as mutable
(エラー: 不変な借用をした中身`*some_string`を可変で借用できません)
--> error.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- use `&mut String` here to make mutable
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ cannot borrow as mutable
変数が標準で不変なのと全く同様に、参照も不変なのです。参照している何かを変更することは叶わないわけです。
可変な参照
一捻り加えるだけでリスト4-6のコードのエラーは解決します:
ファイル名: src/main.rs
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
始めに、s
をmut
に変えなければなりませんでした。そして、&mut s
で可変な参照を生成し、
some_string: &mut String
で可変な参照を受け入れなければなりませんでした。
ところが、可変な参照には大きな制約が一つあります: 特定のスコープで、ある特定のデータに対しては、 一つしか可変な参照を持てないことです。こちらのコードは失敗します:
ファイル名: src/main.rs
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
これがエラーです:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
(エラー: 一度に`s`を可変として2回以上借用することはできません)
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
| (最初の可変な参照はここ)
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
| (二つ目の可変な参照はここ)
6 |
7 | println!("{}, {}", r1, r2);
| -- first borrow later used here
error: aborting due to previous error
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership`
To learn more, run the command again with --verbose.
この制約は、可変化を許可するものの、それを非常に統制の取れた形で行えます。これは、新たなRustaceanにとっては、 壁です。なぜなら、多くの言語では、いつでも好きな時に可変化できるからです。
この制約がある利点は、コンパイラがコンパイル時にデータ競合を防ぐことができる点です。 データ競合とは、競合条件と類似していて、これら3つの振る舞いが起きる時に発生します:
- 2つ以上のポインタが同じデータに同時にアクセスする。
- 少なくとも一つのポインタがデータに書き込みを行っている。
- データへのアクセスを同期する機構が使用されていない。
データ競合は未定義の振る舞いを引き起こし、実行時に追いかけようとした時に特定し解決するのが難しい問題です。 しかし、Rustは、データ競合が起こるコードをコンパイルさえしないので、この問題が発生しないようにしてくれるわけです。
いつものように、波かっこを使って新しいスコープを生成し、同時並行なものでなく、複数の可変な参照を作ることができます。
#![allow(unused)] fn main() { let mut s = String::from("hello"); { let r1 = &mut s; } // r1はここでスコープを抜けるので、問題なく新しい参照を作ることができる let r2 = &mut s; }
可変と不変な参照を組み合わせることに関しても、似たような規則が存在しています。このコードはエラーになります:
let mut s = String::from("hello");
let r1 = &s; // 問題なし
let r2 = &s; // 問題なし
let r3 = &mut s; // 大問題!
これがエラーです:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
(エラー: `s`は不変で借用されているので、可変で借用できません)
--> borrow_thrice.rs:6:19
|
4 | let r1 = &s; // no problem
| - immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^ mutable borrow occurs here
7 | }
| - immutable borrow ends here
ふう!さらに不変な参照をしている間は、可変な参照をすることはできません。不変参照の使用者は、 それ以降に値が突然変わることなんて予想してません!しかしながら、複数の不変参照をすることは可能です。 データを読み込んでいるだけの人に、他人がデータを読み込むことに対して影響を与える能力はないからです。
これらのエラーは、時としてイライラするものではありますが、Rustコンパイラがバグの可能性を早期に指摘してくれ(それも実行時ではなくコンパイル時に)、 問題の発生箇所をズバリ示してくれるのだと覚えておいてください。そうして想定通りにデータが変わらない理由を追いかける必要がなくなります。
宙に浮いた参照
ポインタのある言語では、誤ってダングリングポインタを生成してしまいやすいです。ダングリングポインタとは、 他人に渡されてしまった可能性のあるメモリを指すポインタのことであり、その箇所へのポインタを保持している間に、 メモリを解放してしまうことで発生します。対照的にRustでは、コンパイラが、 参照がダングリング参照に絶対ならないよう保証してくれます: つまり、何らかのデータへの参照があったら、 コンパイラは参照がスコープを抜けるまで、データがスコープを抜けることがないよう確認してくれるわけです。
ダングリング参照作りを試してみますが、コンパイラはこれをコンパイルエラーで阻止します:
ファイル名: src/main.rs
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
こちらがエラーです:
error[E0106]: missing lifetime specifier
(エラー: ライフタイム指定子がありません)
--> main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no
value for it to be borrowed from
(助言: この関数の戻り値型は、借用した値を含んでいますが、借用される値がどこにもありません)
= help: consider giving it a 'static lifetime
('staticライフタイムを与えることを考慮してみてください)
このエラーメッセージは、まだ講義していない機能について触れています: ライフタイムです。 ライフタイムについては第10章で詳しく議論しますが、ライフタイムに関する部分を無視すれば、 このメッセージは、確かにこのコードが問題になる理由に関する鍵を握っています:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from.
dangle
コードの各段階で一体何が起きているのかを詳しく見ていきましょう:
ファイル名: src/main.rs
fn dangle() -> &String { // dangleはStringへの参照を返す
let s = String::from("hello"); // sは新しいString
&s // String sへの参照を返す
} // ここで、sはスコープを抜け、ドロップされる。そのメモリは消される。
// 危険だ
s
は、dangle
内で生成されているので、dangle
のコードが終わったら、s
は解放されてしまいますが、
そこへの参照を返そうとしました。つまり、この参照は無効なString
を指していると思われるのです。
よくないことです!コンパイラは、これを阻止してくれるのです。
ここでの解決策は、String
を直接返すことです:
#![allow(unused)] fn main() { fn no_dangle() -> String { let s = String::from("hello"); s } }
これは何の問題もなく動きます。所有権はムーブされ、何も解放されることはありません。
参照の規則
参照について議論したことを再確認しましょう:
- 任意のタイミングで、一つの可変参照か不変な参照いくつでものどちらかを行える。
- 参照は常に有効でなければならない。
次は、違う種類の参照を見ていきましょう: スライスです。
スライス型
所有権のない別のデータ型は、スライスです。スライスにより、コレクション全体ではなく、 その内の一連の要素を参照することができます。
ちょっとしたプログラミングの問題を考えてみましょう: 文字列を受け取って、その文字列中の最初の単語を返す関数を書いてください。 関数が文字列中に空白を見つけられなかったら、文字列全体が一つの単語に違いないので、文字列全体が返されるべきです。
この関数のシグニチャについて考えてみましょう:
fn first_word(s: &String) -> ?
この関数、first_word
は引数に&String
をとります。所有権はいらないので、これで十分です。
ですが、何を返すべきでしょうか?文字列の一部について語る方法が全くありません。しかし、
単語の終端の添え字を返すことができますね。リスト4-7に示したように、その方法を試してみましょう。
ファイル名: src/main.rs
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() {}
リスト4-7: String
引数へのバイト数で表された添え字を返すfirst_word
関数
String
の値を要素ごとに見て、空白かどうかを確かめる必要があるので、
as_bytes
メソッドを使って、String
オブジェクトをバイト配列に変換しています。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
次に、そのバイト配列に対して、iter
メソッドを使用してイテレータを生成しています:
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
イテレータについて詳しくは、第13章で議論します。今は、iter
は、コレクション内の各要素を返すメソッドであること、
enumerate
がiter
の結果をラップして、(結果をそのまま返す)代わりにタプルの一部として各要素を返すことを知っておいてください。
enumerate
から返ってくるタプルの第1要素は、添え字であり、2番目の要素は、(コレクションの)要素への参照になります。
これは、手動で添え字を計算するよりも少しだけ便利です。
enumerate
メソッドがタプルを返すので、Rustのあらゆる場所同様、パターンを使って、そのタプルを分配できます。
従って、for
ループ内で、タプルの添え字に対するi
とタプルの1バイトに対応する&item
を含むパターンを指定しています。
.iter().enumerate()
から要素への参照を取得するので、パターンに&
を使っています。
for
ループ内で、バイトリテラル表記を使用して空白を表すバイトを検索しています。空白が見つかったら、その位置を返します。
それ以外の場合、s.len()
を使って文字列の長さを返します。
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
さて、文字列内の最初の単語の終端の添え字を見つけ出せるようになりましたが、問題があります。
usize
型を単独で返していますが、これは&String
の文脈でのみ意味を持つ数値です。
言い換えると、String
から切り離された値なので、将来的にも有効である保証がないのです。
リスト4-7のfirst_word
関数を使用するリスト4-8のプログラムを考えてください。
ファイル名: src/main.rs
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() } fn main() { let mut s = String::from("hello world"); let word = first_word(&s); // word will get the value 5 // wordの中身は、値5になる s.clear(); // this empties the String, making it equal to "" // Stringを空にする。つまり、""と等しくする // word still has the value 5 here, but there's no more string that // we could meaningfully use the value 5 with. word is now totally invalid! // wordはまだ値5を保持しているが、もうこの値を正しい意味で使用できる文字列は存在しない。 // wordは今や完全に無効なのだ! }
リスト4-8: first_word
関数の呼び出し結果を保持し、String
の中身を変更する
このプログラムは何のエラーもなくコンパイルが通り、word
をs.clear()
の呼び出し後に使用しても、
コンパイルが通ります。word
はs
の状態に全く関連づけられていないので、その中身はまだ値5
のままです。
その値5
を変数s
に使用し、最初の単語を取り出そうとすることはできますが、これはバグでしょう。
というのも、s
の中身は、5
をword
に保存した後変わってしまったからです。
word
内の添え字がs
に格納されたデータと同期されなくなるのを心配することは、面倒ですし間違いになりやすいです!
これらの添え字の管理は、second_word
関数を書いたら、さらに難しくなります。
そのシグニチャは以下のようになるはずです:
fn second_word(s: &String) -> (usize, usize) {
今、私たちは開始と終端の添え字を追うようになりました。特定の状態のデータから計算されたが、 その状態に全く紐付けられていない値がさらに増えました。いつの間にか変わってしまうので、同期を取る必要のある、関連性のない変数が3つになってしまいました。
運のいいことに、Rustにはこの問題への解決策が用意されています: 文字列スライスです。
文字列スライス
文字列スライスとは、String
の一部への参照で、こんな見た目をしています:
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; }
これは、String
全体への参照を取ることに似ていますが、余計な[0..5]
という部分が付いています。
String
全体への参照ではなく、String
の一部への参照です。
[starting_index..ending_index]
と指定することで、角かっこに範囲を使い、スライスを生成できます。
ここで、starting_index
はスライスの最初の位置、ending_index
はスライスの終端位置よりも、
1大きい値です。内部的には、スライスデータ構造は、開始地点とスライスの長さを保持しており、
スライスの長さはending_index
からstarting_index
を引いたものに対応します。以上より、
let world = &s[6..11];
の場合には、world
はs
の添え字6のバイトへのポインタと5という長さを持つスライスになるでしょう。
図4-6は、これを図解しています。
図4-6: String
オブジェクトの一部を参照する文字列スライス
Rustの..
という範囲記法で、最初の番号(ゼロ)から始めたければ、2連ピリオドの前に値を書かなければいいです。
換言すれば、これらは等価です:
#![allow(unused)] fn main() { let s = String::from("hello"); let slice = &s[0..2]; let slice = &s[..2]; }
同様の意味で、String
の最後のバイトをスライスが含むのならば、末尾の数値を書かなければいいです。
つまり、これらは等価になります:
#![allow(unused)] fn main() { let s = String::from("hello"); let len = s.len(); let slice = &s[3..len]; let slice = &s[3..]; }
さらに、両方の値を省略すると、文字列全体のスライスを得られます。故に、これらは等価です:
#![allow(unused)] fn main() { let s = String::from("hello"); let len = s.len(); let slice = &s[0..len]; let slice = &s[..]; }
注釈: 文字列スライスの範囲添え字は、有効なUTF-8文字境界に置かなければなりません。 マルチバイト文字の真ん中で文字列スライスを生成しようとしたら、エラーでプログラムは落ちるでしょう。 この節では文字列スライスを導入することが目的なので、ASCIIのみを想定しています; UTF-8に関するより徹底した議論は、 第8章の「文字列でUTF-8エンコードされたテキストを格納する」節で行います。
これらの情報を念頭に、first_word
を書き直してスライスを返すようにしましょう。
文字列スライスを意味する型は、&str
と記述します:
ファイル名: src/main.rs
fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() {}
リスト4-7で取った方法と同じように、最初の空白を探すことで単語の終端の添え字を取得しています。 空白を発見したら、文字列の最初を開始地点、空白の添え字を終了地点として使用して文字列スライスを返しています。
これで、first_word
を呼び出すと、元のデータに紐付けられた単独の値を得られるようになりました。
この値は、スライスの開始地点への参照とスライス中の要素数から構成されています。
second_word
関数についても、スライスを返すことでうまくいくでしょう:
fn second_word(s: &String) -> &str {
これで、ずっと混乱しにくい素直なAPIになりました。なぜなら、String
への参照が有効なままであることをコンパイラが、
保証してくれるからです。最初の単語の終端添え字を得た時に、
文字列を空っぽにして先ほどの添え字が無効になってしまったリスト4-8のプログラムのバグを覚えていますか?
そのコードは、論理的に正しくないのですが、即座にエラーにはなりませんでした。問題は後になってから発生し、
それは空の文字列に対して、最初の単語の添え字を使用し続けようとした時でした。スライスならこんなバグはあり得ず、
コードに問題があるなら、もっと迅速に判明します。スライスバージョンのfirst_word
を使用すると、
コンパイルエラーが発生します:
ファイル名: src/main.rs
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error! (エラー!)
println!("the first word is: {}", word);
}
こちらがコンパイルエラーです:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
(エラー: 不変として借用されているので、`s`を可変で借用できません)
--> src/main.rs:18:5
|
16 | let word = first_word(&s);
| -- immutable borrow occurs here
| (不変借用はここで発生しています)
17 |
18 | s.clear(); // error!
| ^^^^^^^^^ mutable borrow occurs here
| (可変借用はここで発生しています)
19 |
20 | println!("the first word is: {}", word);
| ---- immutable borrow later used here
(不変借用はその後ここで使われています)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership`.
To learn more, run the command again with --verbose.
借用規則から、何かへの不変な参照がある時、さらに可変な参照を得ることはできないことを思い出してください。
clear
はString
を切り詰める必要があるので、可変な参照を得る必要があります。Rustはこれを認めないので、コンパイルが失敗します。
RustのおかげでAPIが使いやすくなるだけでなく、ある種のエラー全てを完全にコンパイル時に排除してくれるのです!
文字列リテラルはスライスである
文字列は、バイナリに埋め込まれると話したことを思い出してください。今やスライスのことを知ったので、 文字列リテラルを正しく理解することができます。
#![allow(unused)] fn main() { let s = "Hello, world!"; }
ここでのs
の型は、&str
です: バイナリのその特定の位置を指すスライスです。
これは、文字列が不変である理由にもなっています。要するに、&str
は不変な参照なのです。
引数としての文字列スライス
リテラルやString
値のスライスを得ることができると知ると、first_word
に対して、もう一つ改善点を見出すことができます。
シグニチャです:
fn first_word(s: &String) -> &str {
もっと経験を積んだRustaceanなら、代わりにリスト4-9のようなシグニチャを書くでしょう。というのも、こうすると、
同じ関数を&String
値と&str
値両方に使えるようになるからです。
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// first_word works on slices of `String`s
// first_wordは`String`のスライスに対して機能する
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word works on slices of string literals
// first_wordは文字列リテラルのスライスに対して機能する
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
// 文字列リテラルは「それ自体すでに文字列スライスなので」、
// スライス記法なしでも機能するのだ!
let word = first_word(my_string_literal);
}
リスト4-9: s
引数の型に文字列スライスを使用してfirst_word
関数を改善する
もし、文字列スライスがあるなら、それを直接渡せます。String
があるなら、
そのString
全体のスライスを渡せます。String
への参照の代わりに文字列スライスを取るよう関数を定義すると、
何も機能を失うことなくAPIをより一般的で有益なものにできるのです。
Filename: src/main.rs
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("hello world"); // first_word works on slices of `String`s // first_wordは`String`のスライスに対して機能する let word = first_word(&my_string[..]); let my_string_literal = "hello world"; // first_word works on slices of string literals // first_wordは文字列リテラルのスライスに対して機能する let word = first_word(&my_string_literal[..]); // Because string literals *are* string slices already, // this works too, without the slice syntax! // 文字列リテラルは「それ自体すでに文字列スライスなので」、 // スライス記法なしでも機能するのだ! let word = first_word(my_string_literal); }
他のスライス
文字列リテラルは、ご想像通り、文字列に特化したものです。ですが、もっと一般的なスライス型も存在します。 この配列を考えてください:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; }
文字列の一部を参照したくなる可能性があるのと同様、配列の一部を参照したくなる可能性もあります。 以下のようにすれば、参照することができます:
#![allow(unused)] fn main() { let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; }
このスライスは、&[i32]
という型になります。これも文字列スライスと同じように動作します。
つまり、最初の要素への参照と長さを保持するのです。
この種のスライスは、他のすべての種類のコレクションに対して使用することになるでしょう。
それらのコレクションについて、詳しくは、第8章でベクタについて話すときに議論します。
まとめ
所有権、借用、スライスの概念は、Rustプログラムにおいて、コンパイル時にメモリ安全性を保証します。 Rust言語も他のシステムプログラミング言語と同じように、メモリの使用法について制御させてくれるわけですが、 データの所有者がスコープを抜けたときに、所有者に自動的にデータを片付けさせることは、この制御をするために、 余計なコードを書いたりデバッグしたりする必要がないことを意味します。
所有権は、Rustの他のいろんな部分が動作する方法に影響を与えるので、これ以降もこれらの概念についてさらに語っていく予定です。
第5章に移って、struct
でデータをグループ化することについて見ていきましょう。
構造体を使用して関係のあるデータを構造化する
structまたは、構造体は、意味のあるグループを形成する複数の関連した値をまとめ、名前付けできる独自のデータ型です。 あなたがオブジェクト指向言語に造詣が深いなら、structはオブジェクトのデータ属性みたいなものです。 この章では、タプルと構造体を対照的に比較し、構造体の使用法をデモし、メソッドや関連関数を定義して、 構造体のデータに紐付く振る舞いを指定する方法について議論します。構造体とenum(第6章で議論します)は、 自分のプログラム領域で新しい型を定義し、Rustのコンパイル時型精査機能をフル活用する構成要素になります。
構造体を定義し、インスタンス化する
構造体は第3章で議論したタプルと似ています。タプル同様、構造体の一部を異なる型にできます。 一方タプルとは違って、各データ片には名前をつけるので、値の意味が明確になります。 この名前のおかげで、構造体はタプルに比して、より柔軟になるわけです: データの順番に頼って、 インスタンスの値を指定したり、アクセスしたりする必要がないのです。
構造体の定義は、struct
キーワードを入れ、構造体全体に名前を付けます。構造体名は、
一つにグループ化されるデータ片の意義を表すものであるべきです。そして、波かっこ内に、
データ片の名前と型を定義し、これはフィールドと呼ばれます。例えば、リスト5-1では、
ユーザアカウントに関する情報を保持する構造体を示しています。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } }
リスト5-1: User
構造体定義
構造体を定義した後に使用するには、各フィールドに対して具体的な値を指定して構造体のインスタンスを生成します。
インスタンスは、構造体名を記述し、key: value
ペアを含む波かっこを付け加えることで生成します。
ここで、キーはフィールド名、値はそのフィールドに格納したいデータになります。フィールドは、
構造体で宣言した通りの順番に指定する必要はありません。換言すると、構造体定義とは、
型に対する一般的な雛形のようなものであり、インスタンスは、その雛形を特定のデータで埋め、その型の値を生成するわけです。
例えば、リスト5-2で示されたように特定のユーザを宣言することができます。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; }
リスト5-2: User
構造体のインスタンスを生成する
構造体から特定の値を得るには、ドット記法が使えます。このユーザのEメールアドレスだけが欲しいなら、
この値を使いたかった場所全部でuser1.email
が使えます。インスタンスが可変であれば、
ドット記法を使い特定のフィールドに代入することで値を変更できます。リスト5-3では、
可変なUser
インスタンスのemail
フィールド値を変更する方法を示しています。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } let mut user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; user1.email = String::from("anotheremail@example.com"); }
リスト5-3: あるUser
インスタンスのemail
フィールド値を変更する
インスタンス全体が可変でなければならないことに注意してください; Rustでは、一部のフィールドのみを可変にすることはできないのです。 また、あらゆる式同様、構造体の新規インスタンスを関数本体の最後の式として生成して、 そのインスタンスを返すことを暗示できます。
リスト5-4は、与えられたemailとusernameでUser
インスタンスを生成するbuild_user
関数を示しています。
active
フィールドにはtrue
値が入り、sign_in_count
には値1
が入ります。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } fn build_user(email: String, username: String) -> User { User { email: email, username: username, active: true, sign_in_count: 1, } } }
リスト5-4: Eメールとユーザ名を取り、User
インスタンスを返すbuild_user
関数
構造体のフィールドと同じ名前を関数の引数にもつけることは筋が通っていますが、
email
とusername
というフィールド名と変数を繰り返さなきゃいけないのは、ちょっと面倒です。
構造体にもっとフィールドがあれば、名前を繰り返すことはさらに煩わしくなるでしょう。
幸運なことに、便利な省略記法があります!
フィールドと変数が同名の時にフィールド初期化省略記法を使う
仮引数名と構造体のフィールド名がリスト5-4では、全く一緒なので、フィールド初期化省略記法を使ってbuild_user
を書き換えても、
振る舞いは全く同じにしつつ、リスト5-5に示したようにemail
とusername
を繰り返さなくてもよくなります。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } fn build_user(email: String, username: String) -> User { User { email, username, active: true, sign_in_count: 1, } } }
リスト5-5: email
とusername
引数が構造体のフィールドと同名なので、
フィールド初期化省略法を使用するbuild_user
関数
ここで、email
というフィールドを持つUser
構造体の新規インスタンスを生成しています。
email
フィールドをbuild_user
関数のemail
引数の値にセットしたいわけです。
email
フィールドとemail
引数は同じ名前なので、email: email
と書くのではなく、
email
と書くだけで済むのです。
構造体更新記法で他のインスタンスからインスタンスを生成する
多くは前のインスタンスの値を使用しつつ、変更する箇所もある形で新しいインスタンスを生成できるとしばしば有用です。 構造体更新記法でそうすることができます。
まず、リスト5-6では、更新記法なしでuser2
に新しいUser
インスタンスを生成する方法を示しています。
email
とusername
には新しい値をセットしていますが、それ以外にはリスト5-2で生成したuser1
の値を使用しています。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let user2 = User { email: String::from("another@example.com"), username: String::from("anotherusername567"), active: user1.active, sign_in_count: user1.sign_in_count, }; }
リスト5-6: user1
の一部の値を使用しつつ、新しいUser
インスタンスを生成する
構造体更新記法を使用すると、リスト5-7に示したように、コード量を減らしつつ、同じ効果を達成できます。..
という記法により、
明示的にセットされていない残りのフィールドが、与えられたインスタンスのフィールドと同じ値になるように指定します。
#![allow(unused)] fn main() { struct User { username: String, email: String, sign_in_count: u64, active: bool, } let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let user2 = User { email: String::from("another@example.com"), username: String::from("anotherusername567"), ..user1 }; }
リスト5-7: 構造体更新記法を使用して、新しいUser
インスタンス用の値に新しいemail
とusername
をセットしつつ、
残りの値は、user1
変数のフィールド値を使う
リスト5-7のコードも、email
とusername
については異なる値、active
とsign_in_count
フィールドについては、
user1
と同じ値になるインスタンスをuser2
に生成します。
異なる型を生成する名前付きフィールドのないタプル構造体を使用する
構造体名により追加の意味を含むものの、フィールドに紐づけられた名前がなく、むしろフィールドの型だけのタプル構造体と呼ばれる、 タプルに似た構造体を定義することもできます。タプル構造体は、構造体名が提供する追加の意味は含むものの、 フィールドに紐付けられた名前はありません; むしろ、フィールドの型だけが存在します。タプル構造体は、タプル全体に名前をつけ、 そのタプルを他のタプルとは異なる型にしたい場合に有用ですが、普通の構造体のように各フィールド名を与えるのは、 冗長、または余計になるでしょう。
タプル構造体を定義するには、struct
キーワードの後に構造体名、さらにタプルに含まれる型を続けます。
例えば、こちらは、Color
とPoint
という2種類のタプル構造体の定義と使用法です:
#![allow(unused)] fn main() { struct Color(i32, i32, i32); struct Point(i32, i32, i32); let black = Color(0, 0, 0); let origin = Point(0, 0, 0); }
black
とorigin
の値は、違う型であることに注目してください。これらは、異なるタプル構造体のインスタンスだからですね。
定義された各構造体は、構造体内のフィールドが同じ型であっても、それ自身が独自の型になります。
例えば、Color
型を引数に取る関数は、Point
を引数に取ることはできません。たとえ、両者の型が、
3つのi32
値からできていてもです。それ以外については、タプル構造体のインスタンスは、
タプルと同じように振る舞います: 分配して個々の部品にしたり、.
と添え字を使用して個々の値にアクセスするなどです。
フィールドのないユニット様構造体
また、一切フィールドのない構造体を定義することもできます!これらは、()
、ユニット型と似たような振る舞いをすることから、
ユニット様構造体と呼ばれます。ユニット様構造体は、ある型にトレイトを実装するけれども、
型自体に保持させるデータは一切ない場面に有効になります。トレイトについては第10章で議論します。
構造体データの所有権
リスト5-1の
User
構造体定義において、&str
文字列スライス型ではなく、所有権のあるString
型を使用しました。 これは意図的な選択です。というのも、この構造体のインスタンスには全データを所有してもらう必要があり、 このデータは、構造体全体が有効な間はずっと有効である必要があるのです。構造体に、他の何かに所有されたデータへの参照を保持させることもできますが、 そうするにはライフタイムという第10章で議論するRustの機能を使用しなければなりません。 ライフタイムのおかげで構造体に参照されたデータが、構造体自体が有効な間、ずっと有効であることを保証してくれるのです。 ライフタイムを指定せずに構造体に参照を保持させようとしたとしましょう。以下の通りですが、これは動きません:
ファイル名: src/main.rs
struct User { username: &str, email: &str, sign_in_count: u64, active: bool, } fn main() { let user1 = User { email: "someone@example.com", username: "someusername123", active: true, sign_in_count: 1, }; }
コンパイラは、ライフタイム指定子が必要だと怒るでしょう:
error[E0106]: missing lifetime specifier (エラー: ライフタイム指定子がありません) --> | 2 | username: &str, | ^ expected lifetime parameter (ライフタイム引数を予期しました) error[E0106]: missing lifetime specifier --> | 3 | email: &str, | ^ expected lifetime parameter
第10章で、これらのエラーを解消して構造体に参照を保持する方法について議論しますが、 当面、今回のようなエラーは、
&str
のような参照の代わりに、String
のような所有された型を使うことで修正します。
構造体を使ったプログラム例
構造体を使用したくなる可能性のあるケースを理解するために、長方形の面積を求めるプログラムを書きましょう。 単一の変数から始め、代わりに構造体を使うようにプログラムをリファクタリングします。
Cargoでrectanglesという新規バイナリプロジェクトを作成しましょう。このプロジェクトは、 長方形の幅と高さをピクセルで指定し、その面積を求めます。リスト5-8に、プロジェクトのsrc/main.rsで、 正にそうする一例を短いプログラムとして示しました。
ファイル名: src/main.rs
fn main() { let width1 = 30; let height1 = 50; println!( // 長方形の面積は、{}平方ピクセルです "The area of the rectangle is {} square pixels.", area(width1, height1) ); } fn area(width: u32, height: u32) -> u32 { width * height }
リスト5-8: 個別の幅と高さ変数を指定して長方形の面積を求める
では、cargo run
でこのプログラムを走らせてください:
The area of the rectangle is 1500 square pixels.
(長方形の面積は、1500平方ピクセルです)
タプルでリファクタリングする
リスト5-8のコードはうまく動き、各寸法を与えてarea
関数を呼び出すことで長方形の面積を割り出しますが、
改善点があります。幅と高さは、組み合わせると一つの長方形を表すので、相互に関係があるわけです。
このコードの問題点は、area
のシグニチャから明らかです:
fn area(width: u32, height: u32) -> u32 {
area
関数は、1長方形の面積を求めるものと考えられますが、今書いた関数には、引数が2つあります。
引数は関連性があるのに、このプログラム内のどこにもそのことは表現されていません。
幅と高さを一緒にグループ化する方が、より読みやすく、扱いやすくなるでしょう。
それをする一つの方法については、第3章の「タプル型」節ですでに議論しました: タプルを使うのです。
タプルでリファクタリングする
リスト5-9は、タプルを使う別バージョンのプログラムを示しています。
ファイル名: src/main.rs
fn main() { let rect1 = (30, 50); println!( "The area of the rectangle is {} square pixels.", area(rect1) ); } fn area(dimensions: (u32, u32)) -> u32 { dimensions.0 * dimensions.1 }
リスト5-9: タプルで長方形の幅と高さを指定する
ある意味では、このプログラムはマシです。タプルのおかげで少し構造的になり、一引数を渡すだけになりました。 しかし別の意味では、このバージョンは明確性を失っています: タプルは要素に名前を付けないので、 計算が不明瞭になったのです。なぜなら、タプルの一部に添え字アクセスする必要があるからです。
面積計算で幅と高さを混在させるのなら問題はないのですが、長方形を画面に描画したいとなると、問題になるのです!
タプルの添え字0
が幅
で、添え字1
が高さ
であることを肝に銘じておかなければなりません。
他人がこのコードをいじることになったら、このことを割り出し、同様に肝に銘じなければならないでしょう。
容易く、このことを忘れたり、これらの値を混ぜこぜにしたりしてエラーを発生させてしまうでしょう。
データの意味をコードに載せていないからです。
構造体でリファクタリングする: より意味付けする
データのラベル付けで意味を付与するために構造体を使います。現在使用しているタプルを全体と一部に名前のあるデータ型に、 変形することができます。そう、リスト5-10に示したように。
ファイル名: src/main.rs
struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; println!( "The area of the rectangle is {} square pixels.", area(&rect1) ); } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height }
リスト5-10: Rectangle
構造体を定義する
ここでは、構造体を定義し、Rectangle
という名前にしています。波括弧の中でwidth
とheight
というフィールドを定義し、
u32
という型にしました。それからmain
内でRectangle
の特定のインスタンスを生成し、
幅を30、高さを50にしました。
これでarea
関数は引数が一つになり、この引数は名前がrectangle
、型はRectangle
構造体インスタンスへの不変借用になりました。
第4章で触れたように、構造体の所有権を奪うよりも借用する必要があります。こうすることでmain
は所有権を保って、
rect1
を使用し続けることができ、そのために関数シグニチャと関数呼び出し時に&
を使っているわけです。
area
関数は、Rectangle
インスタンスのwidth
とheight
フィールドにアクセスしています。
これで、area
の関数シグニチャは、我々の意図をズバリ示すようになりました: width
とheight
フィールドを使って、
Rectangle
の面積を計算します。これにより、幅と高さが相互に関係していることが伝わり、
タプルの0
や1
という添え字を使うよりも、これらの値に説明的な名前を与えられるのです。プログラムの意図が明瞭になりました。
トレイトの導出で有用な機能を追加する
プログラムのデバッグをしている間に、Rectangle
のインスタンスを出力し、フィールドの値を確認できると、
素晴らしいわけです。リスト5-11では、以前の章のように、println!
マクロを試しに使用しようとしていますが、動きません。
ファイル名: src/main.rs
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
// rect1は{}です
println!("rect1 is {}", rect1);
}
リスト5-11: Rectangle
のインスタンスを出力しようとする
このコードを走らせると、こんな感じのエラーが出ます:
error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied
(エラー: トレイト境界`Rectangle: std::fmt::Display`が満たされていません)
println!
マクロには、様々な整形があり、標準では、波括弧はDisplay
として知られる整形をするよう、
println!
に指示するのです: 直接エンドユーザ向けの出力です。これまでに見てきた基本型は、
標準でDisplay
を実装しています。というのも、1
や他の基本型をユーザに見せる方法は一つしかないからです。
しかし構造体では、println!
が出力を整形する方法は自明ではなくなります。出力方法がいくつもあるからです:
カンマは必要なの?波かっこを出力する必要はある?全フィールドが見えるべき?この曖昧性のため、
Rustは必要なものを推測しようとせず、構造体にはDisplay
実装が提供されないのです。
エラーを読み下すと、こんな有益な注意書きがあります:
`Rectangle` cannot be formatted with the default formatter; try using
`:?` instead if you are using a format string
(注釈: `Rectangle`は、デフォルト整形機では、整形できません; フォーマット文字列を使うのなら
代わりに`:?`を試してみてください)
試してみましょう!pritnln!
マクロ呼び出しは、println!("rect1 is {:?}", rect1);
という見た目になるでしょう。
波括弧内に:?
という指定子を書くと、println!
にDebug
と呼ばれる出力整形を使いたいと指示するのです。
Debug
トレイトは、開発者にとって有用な方法で構造体を出力させてくれるので、
コードをデバッグしている最中に、値を確認することができます。
変更してコードを走らせてください。なに!まだエラーが出ます:
error[E0277]: the trait bound `Rectangle: std::fmt::Debug` is not satisfied
(エラー: トレイト境界`Rectangle: std::fmt::Debug`が満たされていません)
しかし今回も、コンパイラは有益な注意書きを残してくれています:
`Rectangle` cannot be formatted using `:?`; if it is defined in your
crate, add `#[derive(Debug)]` or manually implement it
(注釈: `Rectangle`は`:?`を使って整形できません; 自分のクレートで定義しているのなら
`#[derive(Debug)]`を追加するか、手動で実装してください)
確かにRustにはデバッグ用の情報を出力する機能が備わっていますが、この機能を構造体で使えるようにするには、
明示的な選択をしなければならないのです。そうするには、構造体定義の直前に#[derive(Debug)]
という注釈を追加します。
そう、リスト5-12で示されている通りです。
ファイル名: src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; println!("rect1 is {:?}", rect1); }
リスト5-12: Debug
トレイトを導出する注釈を追加し、
Rectangle
インスタンスをデバッグ用整形機で出力する
これでプログラムを実行すれば、エラーは出ず、以下のような出力が得られるでしょう:
rect1 is Rectangle { width: 30, height: 50 }
素晴らしい!最善の出力ではないものの、このインスタンスの全フィールドの値を出力しているので、
デバッグ中には間違いなく役に立つでしょう。より大きな構造体があるなら、もう少し読みやすい出力の方が有用です;
そのような場合には、println!
文字列中の{:?}
の代わりに{:#?}
を使うことができます。
この例で{:#?}
というスタイルを使用したら、出力は以下のようになるでしょう:
rect1 is Rectangle {
width: 30,
height: 50
}
Rustには、derive
注釈で使えるトレイトが多く提供されており、独自の型に有用な振る舞いを追加することができます。
そのようなトレイトとその振る舞いは、付録Cで一覧になっています。
これらのトレイトを独自の動作とともに実装する方法だけでなく、独自のトレイトを生成する方法については、第10章で解説します。
area
関数は、非常に特殊です: 長方形の面積を算出するだけです。Rectangle
構造体とこの動作をより緊密に結び付けられると、
役に立つでしょう。なぜなら、他のどんな型でもうまく動作しなくなるからです。
area
関数をRectangle
型に定義されたarea
メソッドに変形することで、
このコードをリファクタリングし続けられる方法について見ていきましょう。
メソッド記法
メソッドは関数に似ています: fn
キーワードと名前で宣言されるし、引数と返り値があるし、
どこか別の場所で呼び出された時に実行されるコードを含みます。ところが、
メソッドは構造体の文脈(あるいはenumかトレイトオブジェクトの。これらについては各々第6章と17章で解説します)で定義されるという点で、
関数とは異なり、最初の引数は必ずself
になり、これはメソッドが呼び出されている構造体インスタンスを表します。
メソッドを定義する
Rectangle
インスタンスを引数に取るarea
関数を変え、代わりにRectangle
構造体上にarea
メソッドを作りましょう。
リスト5-13に示した通りですね。
ファイル名: src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; println!( "The area of the rectangle is {} square pixels.", rect1.area() ); }
リスト5-13: Rectangle
構造体上にarea
メソッドを定義する
Rectangle
の文脈内で関数を定義するには、impl
(implementation; 実装)ブロックを始めます。
それからarea
関数をimpl
の波かっこ内に移動させ、最初の(今回は唯一の)引数をシグニチャ内と本体内全てでself
に変えます。
area
関数を呼び出し、rect1
を引数として渡すmain
では、代替としてメソッド記法を使用して、
Rectangle
インスタンスのarea
メソッドを呼び出せます。メソッド記法は、インスタンスの後に続きます:
ドット、メソッド名、かっこ、そして引数と続くわけです。
area
のシグニチャでは、rectangle: &Rectangle
の代わりに&self
を使用しています。
というのも、コンパイラは、このメソッドがimpl Rectangle
という文脈内に存在するために、
self
の型がRectangle
であると把握しているからです。&Rectangle
と同様に、
self
の直前に&
を使用していることに注意してください。メソッドは、self
の所有権を奪ったり、
ここでしているように不変でself
を借用したり、可変でself
を借用したりできるのです。
他の引数と全く同じですね。
ここで&self
を選んでいるのは、関数バージョンで&Rectangle
を使用していたのと同様の理由です:
所有権はいらず、構造体のデータを読み込みたいだけで、書き込む必要はないわけです。
メソッドの一部でメソッドを呼び出したインスタンスを変更したかったら、第1引数に&mut self
を使用するでしょう。
self
だけを第1引数にしてインスタンスの所有権を奪うメソッドを定義することは稀です; このテクニックは通常、
メソッドがself
を何か別のものに変形し、変形後に呼び出し元が元のインスタンスを使用できないようにしたい場合に使用されます。
関数の代替としてメソッドを使う主な利点は、メソッド記法を使用して全メソッドのシグニチャでself
の型を繰り返す必要がなくなる以外だと、
体系化です。コードの将来的な利用者にRectangle
の機能を提供しているライブラリ内の各所でその機能を探させるのではなく、
この型のインスタンスでできることを一つのimpl
ブロックにまとめあげています。
->
演算子はどこに行ったの?CとC++では、メソッド呼び出しには2種類の異なる演算子が使用されます: オブジェクトに対して直接メソッドを呼び出すのなら、
.
を使用するし、オブジェクトのポインタに対してメソッドを呼び出し、 先にポインタを参照外しする必要があるなら、->
を使用するわけです。 言い換えると、object
がポインタなら、object->something()
は、(*object).something()
と同等なのです。Rustには
->
演算子の代わりとなるようなものはありません; その代わり、Rustには、 自動参照および参照外しという機能があります。Rustにおいてメソッド呼び出しは、 この動作が行われる数少ない箇所なのです。動作方法はこうです:
object.something()
とメソッドを呼び出すと、 コンパイラはobject
がメソッドのシグニチャと合致するように、自動で&
か&mut
、*
を付与するのです。 要するに、以下のコードは同じものです:#![allow(unused)] fn main() { #[derive(Debug,Copy,Clone)] struct Point { x: f64, y: f64, } impl Point { fn distance(&self, other: &Point) -> f64 { let x_squared = f64::powi(other.x - self.x, 2); let y_squared = f64::powi(other.y - self.y, 2); f64::sqrt(x_squared + y_squared) } } let p1 = Point { x: 0.0, y: 0.0 }; let p2 = Point { x: 5.0, y: 6.5 }; p1.distance(&p2); (&p1).distance(&p2); }
前者の方がずっと明確です。メソッドには自明な受け手(
self
の型)がいるので、この自動参照機能は動作するのです。 受け手とメソッド名が与えられれば、コンパイラは確実にメソッドが読み込み専用(&self
)か、書き込みもする(&mut self
)のか、 所有権を奪う(self
)のか判断できるわけです。メソッドの受け手に関して借用が明示されないというのが、 所有権を実際に使うのがRustにおいて簡単である大きな理由です。
より引数の多いメソッド
Rectangle
構造体に2番目のメソッドを実装して、メソッドを使う鍛錬をしましょう。今回は、Rectangle
のインスタンスに、
別のRectangle
のインスタンスを取らせ、2番目のRectangle
がself
に完全にはめ込まれたら、true
を返すようにしたいのです;
そうでなければ、false
を返すべきです。つまり、一旦can_hold
メソッドを定義したら、
リスト5-14のようなプログラムを書けるようになりたいのです。
ファイル名: src/main.rs
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 10, height: 40 };
let rect3 = Rectangle { width: 60, height: 45 };
// rect1にrect2ははまり込む?
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
リスト5-14: まだ書いていないcan_hold
メソッドを使用する
そして、予期される出力は以下のようになります。なぜなら、rect2
の各寸法はrect1
よりも小さいものの、
rect3
はrect1
より幅が広いからです:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
メソッドを定義したいことはわかっているので、impl Rectangle
ブロック内での話になります。
メソッド名は、can_hold
になり、引数として別のRectangle
を不変借用で取るでしょう。
メソッドを呼び出すコードを見れば、引数の型が何になるかわかります: rect1.can_hold(&rect2)
は、
&rect2
、Rectangle
のインスタンスであるrect2
への不変借用を渡しています。
これは道理が通っています。なぜなら、rect2
を読み込む(書き込みではなく。この場合、可変借用が必要になります)だけでよく、
can_hold
メソッドを呼び出した後にもrect2
が使えるよう、所有権をmain
に残したままにしたいからです。
can_hold
の返り値は、booleanになり、メソッドの中身は、self
の幅と高さがもう一つのRectangle
の幅と高さよりも、
それぞれ大きいことを確認します。リスト5-13のimpl
ブロックに新しいcan_hold
メソッドを追記しましょう。
リスト5-15に示した通りです。
ファイル名: src/main.rs
#![allow(unused)] fn main() { #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } }
リスト5-15: 別のRectangle
のインスタンスを引数として取るcan_hold
メソッドを、
Rectangle
に実装する
このコードをリスト5-14のmain
関数と合わせて実行すると、望み通りの出力が得られます。
メソッドは、self
引数の後にシグニチャに追加した引数を複数取ることができ、
その引数は、関数の引数と同様に動作するのです。
関連関数
impl
ブロックの別の有益な機能は、impl
ブロック内にself
を引数に取らない関数を定義できることです。
これは、構造体に関連付けられているので、関連関数と呼ばれます。それでも、関連関数は関数であり、メソッドではありません。
というのも、対象となる構造体のインスタンスが存在しないからです。もうString::from
という関連関数を使用したことがありますね。
関連関数は、構造体の新規インスタンスを返すコンストラクタによく使用されます。例えば、一つの寸法を引数に取り、
長さと幅両方に使用する関連関数を提供することができ、その結果、同じ値を2回指定する必要なく、
正方形のRectangle
を生成しやすくすることができます。
ファイル名: src/main.rs
#![allow(unused)] fn main() { #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size } } } }
この関連関数を呼び出すために、構造体名と一緒に::
記法を使用します; 一例はlet sq = Rectangle::square(3);
です。
この関数は、構造体によって名前空間分けされています: ::
という記法は、関連関数とモジュールによって作り出される名前空間両方に使用されます。
モジュールについては第7章で議論します。
複数のimpl
ブロック
各構造体には、複数のimpl
ブロックを存在させることができます。例えば、リスト5-15はリスト5-16に示したコードと等価で、
リスト5-16では、各メソッドごとにimpl
ブロックを用意しています。
#![allow(unused)] fn main() { #[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } }
リスト5-16: 複数のimpl
ブロックを使用してリスト5-15を書き直す
ここでこれらのメソッドを個々のimpl
ブロックに分ける理由はないのですが、合法な書き方です。
複数のimpl
ブロックが有用になるケースは第10章で見ますが、そこではジェネリック型と、トレイトについて議論します。
まとめ
構造体により、自分の領域で意味のある独自の型を作成することができます。構造体を使用することで、 関連のあるデータ片を相互に結合させたままにし、各部品に名前を付け、コードを明確にすることができます。 メソッドにより、構造体のインスタンスが行う動作を指定することができ、関連関数により、 構造体に特有の機能をインスタンスを利用することなく、名前空間分けすることができます。
しかし、構造体だけが独自の型を作成する手段ではありません: Rustのenum機能に目を向けて、 別の道具を道具箱に追加しましょう。
Enumとパターンマッチング
この章では、列挙型について見ていきます。列挙型は、enumとも称されます。enumは、取りうる値を列挙することで、
型を定義させてくれます。最初に、enumを定義し、使用して、enumがデータとともに意味をコード化する方法を示します。
次に、特別に有用なenumであるOption
について掘り下げていきましょう。この型は、
値が何かかなんでもないかを表現します。それから、match
式のパターンマッチングにより、
どうenumの色々な値に対して異なるコードを走らせやすくなるかを見ます。最後に、if let
文法要素も、
如何にenumをコードで扱う際に使用可能な便利で簡潔な慣用句であるかを解説します。
enumは多くの言語に存在する機能ですが、その能力は言語ごとに異なります。Rustのenumは、F#、OCaml、Haskellなどの、 関数型言語に存在する代数的データ型に最も酷似しています。
Enumを定義する
コードで表現したくなるかもしれない場面に目を向けて、enumが有用でこの場合、構造体よりも適切である理由を確認しましょう。 IPアドレスを扱う必要が出たとしましょう。現在、IPアドレスの規格は二つあります: バージョン4とバージョン6です。 これらは、プログラムが遭遇するIPアドレスのすべての可能性です: 列挙型は、取りうる値をすべて列挙でき、 これが列挙型の名前の由来です。
どんなIPアドレスも、バージョン4かバージョン6のどちらかになりますが、同時に両方にはなり得ません。 IPアドレスのその特性により、enumデータ構造が適切なものになります。というのも、 enumの値は、その列挙子のいずれか一つにしかなり得ないからです。バージョン4とバージョン6のアドレスは、 どちらも根源的にはIPアドレスですから、コードがいかなる種類のIPアドレスにも適用される場面を扱う際には、 同じ型として扱われるべきです。
この概念をコードでは、IpAddrKind
列挙型を定義し、IPアドレスがなりうる種類、V4
とV6
を列挙することで、
表現できます。これらは、enumの列挙子として知られています:
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } }
これで、IpAddrKind
はコードの他の場所で使用できる独自のデータ型になります。
Enumの値
以下のようにして、IpAddrKind
の各列挙子のインスタンスは生成できます:
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } let four = IpAddrKind::V4; let six = IpAddrKind::V6; }
enumの列挙子は、その識別子の元に名前空間分けされていることと、
2連コロンを使ってその二つを区別していることに注意してください。
これが有効な理由は、こうすることで、値IpAddrKind::V4
とIpAddrKind::V6
という値は両方とも、
同じ型IpAddrKind
になったからです。そうしたら、例えば、どんなIpAddrKind
を取る関数も定義できるようになります。
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } fn route(ip_type: IpAddrKind) { } }
そして、この関数をどちらの列挙子に対しても呼び出せます:
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } fn route(ip_type: IpAddrKind) { } route(IpAddrKind::V4); route(IpAddrKind::V6); }
enumの利用には、さらなる利点さえもあります。このIPアドレス型についてもっと考えてみると、現状では、 実際のIPアドレスのデータを保持する方法がありません。つまり、どんな種類であるかを知っているだけです。 構造体について第5章で学んだばっかりとすると、この問題に対して、あなたはリスト6-1のように対処するかもしれません。
#![allow(unused)] fn main() { enum IpAddrKind { V4, V6, } struct IpAddr { kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1"), }; let loopback = IpAddr { kind: IpAddrKind::V6, address: String::from("::1"), }; }
リスト6-1: IPアドレスのデータとIpAddrKind
の列挙子をstruct
を使って保持する
ここでは、二つのフィールドを持つIpAddr
という構造体を定義しています: IpAddrKind
型(先ほど定義したenumですね)のkind
フィールドと、
String
型のaddress
フィールドです。この構造体のインスタンスが2つあります。最初のインスタンス、
home
にはkind
としてIpAddrKind::V4
があり、紐付けられたアドレスデータは127.0.0.1
です。
2番目のインスタンス、loopback
には、kind
の値として、IpAddrKind
のもう一つの列挙子、V6
があり、
アドレス::1
が紐付いています。構造体を使ってkind
とaddress
値を一緒に包んだので、
もう列挙子は値と紐付けられています。
各enumの列挙子に直接データを格納して、enumを構造体内に使うというよりもenumだけを使って、
同じ概念をもっと簡潔な方法で表現することができます。この新しいIpAddr
の定義は、
V4
とV6
列挙子両方にString
値が紐付けられていることを述べています。
#![allow(unused)] fn main() { enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V6(String::from("::1")); }
enumの各列挙子にデータを直接添付できるので、余計な構造体を作る必要は全くありません。
構造体よりもenumを使うことには、別の利点もあります: 各列挙子に紐付けるデータの型と量は、異なってもいいのです。
バージョン4のIPアドレスには、常に0から255の値を持つ4つの数値があります。V4
のアドレスは、4つのu8
型の値として格納するけれども、
V6
のアドレスは引き続き、単独のString
型の値で格納したかったとしても、構造体では不可能です。
enumなら、こんな場合も容易に対応できます:
#![allow(unused)] fn main() { enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1")); }
バージョン4とバージョン6のIPアドレスを格納するデータ構造を定義する複数の異なる方法を示してきました。
しかしながら、蓋を開けてみれば、IPアドレスを格納してその種類をコード化したくなるということは一般的なので、
標準ライブラリに使用可能な定義があります! 標準ライブラリでのIpAddr
の定義のされ方を見てみましょう:
私たちが定義し、使用したのと全く同じenumと列挙子がありますが、アドレスデータを二種の異なる構造体の形で列挙子に埋め込み、
この構造体は各列挙子用に異なる形で定義されています。
#![allow(unused)] fn main() { struct Ipv4Addr { // 省略 } struct Ipv6Addr { // 省略 } enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr), } }
このコードは、enum列挙子内にいかなる種類のデータでも格納できることを描き出しています: 例を挙げれば、文字列、数値型、構造体などです。他のenumを含むことさえできます!また、 標準ライブラリの型は、あなたの想像するよりも複雑ではないことがしばしばあります。
標準ライブラリにIpAddr
に対する定義は含まれるものの、標準ライブラリの定義をまだ我々のスコープに導入していないので、
干渉することなく自分自身の定義を生成して使用できることに注意してください。型をスコープに導入することについては、
第7章でもっと詳しく言及します。
リスト6-2でenumの別の例を見てみましょう: 今回のコードは、幅広い種類の型が列挙子に埋め込まれています。
#![allow(unused)] fn main() { enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } }
リスト6-2: 列挙子各々が異なる型と量の値を格納するMessage
enum
このenumには、異なる型の列挙子が4つあります:
Quit
には紐付けられたデータは全くなし。Move
は、中に匿名構造体を含む。Write
は、単独のString
オブジェクトを含む。ChangeColor
は、3つのi32
値を含む。
リスト6-2のような列挙子を含むenumを定義することは、enumの場合、struct
キーワードを使わず、
全部の列挙子がMessage
型の元に分類される点を除いて、異なる種類の構造体定義を定義するのと類似しています。
以下の構造体も、先ほどのenumの列挙子が保持しているのと同じデータを格納することができるでしょう:
#![allow(unused)] fn main() { struct QuitMessage; // ユニット構造体 struct MoveMessage { x: i32, y: i32, } struct WriteMessage(String); // タプル構造体 struct ChangeColorMessage(i32, i32, i32); // タプル構造体 }
ですが、異なる構造体を使っていたら、各々、それ自身の型があるので、単独の型になるリスト6-2で定義したMessage
enumほど、
これらの種のメッセージいずれもとる関数を簡単に定義することはできないでしょう。
enumと構造体にはもう1点似通っているところがあります: impl
を使って構造体にメソッドを定義できるのと全く同様に、
enumにもメソッドを定義することができるのです。こちらは、Message
enum上に定義できるcall
という名前のメソッドです:
#![allow(unused)] fn main() { enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } impl Message { fn call(&self) { // method body would be defined here // メソッド本体はここに定義される } } let m = Message::Write(String::from("hello")); m.call(); }
メソッドの本体では、self
を使用して、メソッドを呼び出した相手の値を取得できるでしょう。この例では、
Message::Write(String::from("hello"))
という値を持つ、変数m
を生成したので、これがm.call()
を走らせた時に、
call
メソッドの本体内でself
が表す値になります。
非常に一般的で有用な別の標準ライブラリのenumを見てみましょう: Option
です。
Option
enumとNull値に勝る利点
前節で、IpAddr
enumがRustの型システムを使用して、プログラムにデータ以上の情報をコード化できる方法を目撃しました。
この節では、Option
のケーススタディを掘り下げていきます。この型も標準ライブラリにより定義されているenumです。
このOption
型はいろんな箇所で使用されます。なぜなら、値が何かかそうでないかという非常に一般的な筋書きをコード化するからです。
この概念を型システムの観点で表現することは、コンパイラが、プログラマが処理すべき場面全てを処理していることをチェックできることを意味します;
この機能は、他の言語において、究極的にありふれたバグを阻止することができます。
プログラミング言語のデザインは、しばしばどの機能を入れるかという観点で考えられるが、 除いた機能も重要なのです。Rustには、他の多くの言語にはあるnull機能がありません。 nullとはそこに何も値がないことを意味する値です。nullのある言語において、 変数は常に二者択一どちらかの状態になります: nullかそうでないかです。
nullの開発者であるトニー・ホーア(Tony Hoare)の2009年のプレゼンテーション、 "Null References: The Billion Dollar Mistake"(Null参照: 10億ドルの間違い)では、こんなことが語られています。
私はそれを10億ドルの失敗と呼んでいます。その頃、私は、オブジェクト指向言語の参照に対する、 最初のわかりやすい型システムを設計していました。私の目標は、 どんな参照の使用も全て完全に安全であるべきことを、コンパイラにそのチェックを自動で行ってもらって保証することだったのです。 しかし、null参照を入れるという誘惑に打ち勝つことができませんでした。それは、単純に実装が非常に容易だったからです。 これが無数のエラーや脆弱性、システムクラッシュにつながり、過去40年で10億ドルの苦痛や損害を引き起こしたであろうということなのです。
null値の問題は、nullの値をnullでない値のように使用しようとしたら、何らかの種類のエラーが出ることです。 このnullかそうでないかという特性は広く存在するので、この種の間違いを大変犯しやすいのです。
しかしながら、nullが表現しようとしている概念は、それでも役に立つものです: nullは、 何らかの理由で現在無効、または存在しない値のことなのです。
問題は、全く概念にあるのではなく、特定の実装にあるのです。そんな感じなので、Rustにはnullがありませんが、
値が存在するか不在かという概念をコード化するenumならあります。このenumがOption<T>
で、
以下のように標準ライブラリに定義されています。
#![allow(unused)] fn main() { enum Option<T> { Some(T), None, } }
Option<T>
は有益すぎて、初期化処理(prelude)にさえ含まれています。つまり、明示的にスコープに導入する必要がないのです。
さらに、列挙子もそうなっています: Some
とNone
をOption::
の接頭辞なしに直接使えるわけです。
ただ、Option<T>
はそうは言っても、普通のenumであり、Some(T)
とNone
もOption<T>
型のただの列挙子です。
<T>
という記法は、まだ語っていないRustの機能です。これは、ジェネリック型引数であり、ジェネリクスについて詳しくは、
第10章で解説します。とりあえず、知っておく必要があることは、<T>
は、Option
enumのSome
列挙子が、
あらゆる型のデータを1つだけ持つことができることを意味していることだけです。こちらは、
Option
値を使って、数値型や文字列型を保持する例です。
#![allow(unused)] fn main() { let some_number = Some(5); let some_string = Some("a string"); let absent_number: Option<i32> = None; }
Some
ではなく、None
を使ったら、コンパイラにOption<T>
の型が何になるかを教えなければいけません。
というのも、None
値を見ただけでは、Some
列挙子が保持する型をコンパイラが推論できないからです。
Some
値がある時、値が存在するとわかり、その値は、Some
に保持されています。None
値がある場合、
ある意味、nullと同じことを意図します: 有効な値がないのです。では、なぜOption<T>
の方が、
nullよりも少しでも好ましいのでしょうか?
簡潔に述べると、Option<T>
とT
(ここでT
はどんな型でもいい)は異なる型なので、
コンパイラがOption<T>
値を確実に有効な値かのようには使用させてくれません。
例えば、このコードはi8
をOption<i8>
に足そうとしているので、コンパイルできません。
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
このコードを動かしたら、以下のようなエラーメッセージが出ます。
error[E0277]: the trait bound `i8: std::ops::Add<std::option::Option<i8>>` is
not satisfied
(エラー: `i8: std::ops::Add<std::option::Option<i8>>`というトレイト境界が満たされていません)
-->
|
5 | let sum = x + y;
| ^ no implementation for `i8 + std::option::Option<i8>`
|
なんて強烈な!実際に、このエラーメッセージは、i8
とOption<i8>
が異なる型なので、
足し合わせる方法がコンパイラにはわからないことを意味します。Rustにおいて、i8
のような型の値がある場合、
コンパイラが常に有効な値であることを確認してくれます。この値を使う前にnullであることをチェックする必要なく、
自信を持って先に進むことができるのです。Option<i8>
がある時(あるいはどんな型を扱おうとしていても)のみ、
値を保持していない可能性を心配する必要があるわけであり、
コンパイラはプログラマが値を使用する前にそのような場面を扱っているか確かめてくれます。
言い換えると、T
型の処理を行う前には、Option<T>
をT
に変換する必要があるわけです。一般的に、
これにより、nullの最もありふれた問題の一つを捕捉する一助になります: 実際にはnullなのに、
そうでないかのように想定することです。
不正確にnullでない値を想定する心配をしなくてもよいということは、コード内でより自信を持てることになります。
nullになる可能性のある値を保持するには、その値の型をOption<T>
にすることで明示的に同意しなければなりません。
それからその値を使用する際には、値がnullである場合を明示的に処理する必要があります。
値がOption<T>
以外の型であるところ全てにおいて、値がnullでないと安全に想定することができます。
これは、Rustにとって、意図的な設計上の決定であり、nullの普遍性を制限し、Rustコードの安全性を向上させます。
では、Option<T>
型の値がある時、その値を使えるようにするには、どのようにSome
列挙子からT
型の値を取り出せばいいのでしょうか?
Option<T>
には様々な場面で有効に活用できる非常に多くのメソッドが用意されています;
ドキュメントでそれらを確認できます。Option<T>
のメソッドに馴染むと、
Rustの旅が極めて有益になるでしょう。
一般的に、Option<T>
値を使うには、各列挙子を処理するコードが欲しくなります。
Some(T)
値がある時だけ走る何らかのコードが欲しくなり、このコードが内部のT
を使用できます。
None
値があった場合に走る別のコードが欲しくなり、そちらのコードはT
値は使用できない状態になります。
match
式が、enumとともに使用した時にこれだけの動作をする制御フロー文法要素になります:
enumの列挙子によって、違うコードが走り、そのコードがマッチした値の中のデータを使用できるのです。
match
制御フロー演算子
Rustには、一連のパターンに対して値を比較し、マッチしたパターンに応じてコードを実行させてくれるmatch
と呼ばれる、
非常に強力な制御フロー演算子があります。パターンは、リテラル値、変数名、ワイルドカードやその他多数のもので構成することができます;
第18章で、全ての種類のパターンと、その目的については解説します。match
のパワーは、
パターンの表現力とコンパイラが全てのありうるパターンを処理しているかを確認してくれるという事実に由来します。
match
式をコイン並べ替え装置のようなものと考えてください: コインは、様々なサイズの穴が空いた通路を流れ落ち、
各コインは、サイズのあった最初の穴に落ちます。同様に、値はmatch
の各パターンを通り抜け、値が「適合する」最初のパターンで、
値は紐付けられたコードブロックに落ち、実行中に使用されるわけです。
コインについて話したので、それをmatch
を使用する例にとってみましょう!数え上げ装置と同じ要領で未知のアメリカコインを一枚取り、
どの種類のコインなのか決定し、その価値をセントで返す関数をリスト6-3で示したように記述することができます。
#![allow(unused)] fn main() { enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } }
リスト6-3: enumとそのenumの列挙子をパターンにしたmatch
式
value_in_cents
関数内のmatch
を噛み砕きましょう。まず、match
キーワードに続けて式を並べています。
この式は今回の場合、値coin
です。if
で使用した式と非常に酷似しているみたいですね。しかし、大きな違いがあります:
if
では、式は論理値を返す必要がありますが、ここでは、どんな型でも構いません。この例におけるcoin
の型は、
1行目で定義したCoin
enumです。
次は、match
アームです。一本のアームには2つの部品があります: パターンと何らかのコードです。
今回の最初のアームはCoin::Penny
という値のパターンであり、パターンと動作するコードを区別する=>
演算子が続きます。
この場合のコードは、ただの値1
です。各アームは次のアームとカンマで区切られています。
このmatch
式が実行されると、結果の値を各アームのパターンと順番に比較します。パターンに値がマッチしたら、
そのコードに紐付けられたコードが実行されます。パターンが値にマッチしなければ、コイン並べ替え装置と全く同じように、
次のアームが継続して実行されます。必要なだけパターンは存在できます: リスト6-3では、match
には4本のアームがあります。
各アームに紐付けられるコードは式であり、マッチしたアームの式の結果がmatch
式全体の戻り値になります。
典型的に、アームのコードが短い場合、波かっこは使用されません。リスト6-3では、各アームが値を返すだけなので、
これに倣っています。マッチのアームで複数行のコードを走らせたいのなら、波かっこを使用することができます。
例えば、以下のコードは、メソッドがCoin::Penny
とともに呼び出されるたびに「Lucky penny!」と表示しつつ、
ブロックの最後の値、1
を返すでしょう。
#![allow(unused)] fn main() { enum Coin { Penny, Nickel, Dime, Quarter, } fn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => { println!("Lucky penny!"); 1 }, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } }
値に束縛されるパターン
マッチのアームの別の有益な機能は、パターンにマッチした値の一部に束縛できる点です。こうして、 enumの列挙子から値を取り出すことができます。
例として、enumの列挙子の一つを中にデータを保持するように変えましょう。1999年から2008年まで、
アメリカは、片側に50の州それぞれで異なるデザインをしたクォーターコインを鋳造していました。
他のコインは州のデザインがなされることはなかったので、クォーターだけがこのおまけの値を保持します。
Quarter
列挙子を変更して、UsState
値が中に保持されるようにすることでenum
にこの情報を追加でき、
それをしたのがリスト6-4のコードになります。
#![allow(unused)] fn main() { #[derive(Debug)] // すぐに州を点検できるように enum UsState { Alabama, Alaska, // ... などなど } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } }
リスト6-4: Quarter
列挙子がUsState
の値も保持するCoin
enum
友人の一人が50州全部のクォーターコインを収集しようとしているところを想像しましょう。コインの種類で小銭を並べ替えつつ、 友人が持っていない種類だったら、コレクションに追加できるように、各クォーターに関連した州の名前を出力します。
このコードのmatch式では、Coin::Quarter
列挙子の値にマッチするstate
という名の変数をパターンに追加します。
Coin::Quarter
がマッチすると、state
変数はそのクォーターのstateの値に束縛されます。それから、
state
をそのアームのコードで使用できます。以下のようにですね:
#![allow(unused)] fn main() { #[derive(Debug)] enum UsState { Alabama, Alaska, } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } fn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!("State quarter from {:?}!", state); 25 }, } } }
value_in_cents(Coin::Quarter(UsState::Alaska))
と呼び出すつもりだったなら、coin
は
Coin::Quarter(UsState::Alaska)
になります。その値をmatchの各アームと比較すると、
Coin::Quarter(state)
に到達するまで、どれにもマッチしません。その時に、state
に束縛されるのは、
UsState::Alaska
という値です。そして、println!
式でその束縛を使用することができ、
そのため、Coin
enumの列挙子からQuarter
に対する中身のstateの値を取得できたわけです。
Option<T>
とのマッチ
前節では、Option<T>
を使用する際に、Some
ケースから中身のT
の値を取得したくなりました。要するに、
Coin
enumに対して行ったように、match
を使ってOption<T>
を扱うこともできるというわけです!
コインを比較する代わりに、Option<T>
の列挙子を比較するのですが、match
式の動作の仕方は同じままです。
Option<i32>
を取る関数を書きたくなったとし、中に値があったら、その値に1を足すことにしましょう。
中に値がなければ、関数はNone
値を返し、何も処理を試みるべきではありません。
match
のおかげで、この関数は大変書きやすく、リスト6-5のような見た目になります。
#![allow(unused)] fn main() { fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), } } let five = Some(5); let six = plus_one(five); let none = plus_one(None); }
リスト6-5: Option<i32>
にmatch
式を使う関数
plus_one
の最初の実行についてもっと詳しく検証しましょう。plus_one(five)
と呼び出した時、
plus_one
の本体の変数x
はSome(5)
になります。そして、これをマッチの各アームと比較します。
None => None,
Some(5)
という値は、None
というパターンにはマッチしませんので、次のアームに処理が移ります。
Some(i) => Some(i + 1),
Some(5)
はSome(i)
にマッチしますか?なんと、します!列挙子が同じです。i
はSome
に含まれる値に束縛されるので、
i
は値5
になります。それから、このマッチのアームのコードが実行されるので、i
の値に1を足し、
合計の6
を中身にした新しいSome
値を生成します。
さて、x
がNone
になるリスト6-5の2回目のplus_one
の呼び出しを考えましょう。match
に入り、
最初のアームと比較します。
None => None,
マッチします!足し算する値がないので、プログラムは停止し、=>
の右辺にあるNone
値が返ります。
最初のアームがマッチしたため、他のアームは比較されません。
match
とenumの組み合わせは、多くの場面で有効です。Rustコードにおいて、このパターンはよく見かけるでしょう:
enumに対しmatch
し、内部のデータに変数を束縛させ、それに基づいたコードを実行します。最初はちょっと巧妙ですが、
一旦慣れてしまえば、全ての言語にあってほしいと願うことになるでしょう。一貫してユーザのお気に入りなのです。
マッチは包括的
もう一つ議論する必要のあるmatch
の観点があります。一点バグがありコンパイルできないこんなバージョンのplus_one
関数を考えてください:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
None
の場合を扱っていないため、このコードはバグを生みます。幸い、コンパイラが捕捉できるバグです。
このコードのコンパイルを試みると、こんなエラーが出ます:
error[E0004]: non-exhaustive patterns: `None` not covered
(エラー: 包括的でないパターン: `None`がカバーされてません)
-->
|
6 | match x {
| ^ pattern `None` not covered
全可能性を網羅していないことをコンパイラは検知しています。もっと言えば、どのパターンを忘れているかさえ知っているのです。
Rustにおけるマッチは、包括的です: 全てのあらゆる可能性を網羅し尽くさなければ、コードは有効にならないのです。
特にOption<T>
の場合には、私達が明示的にNone
の場合を処理するのを忘れないようにしてくれます。
nullになるかもしれないのに値があると思い込まないよう、すなわち前に議論した10億ドルの失敗を犯さないよう、
コンパイラが保護してくれるわけです。
_
というプレースホルダー
Rustには、全ての可能性を列挙したくない時に使用できるパターンもあります。例えば、u8
は、有効な値として、
0から255までを取ります。1、3、5、7の値にだけ興味があったら、0、2、4、6、8、9と255までの数値を列挙する必要に迫られたくはないです。
幸運なことに、する必要はありません: 代わりに特別なパターンの_
を使用できます:
#![allow(unused)] fn main() { let some_u8_value = 0u8; match some_u8_value { 1 => println!("one"), 3 => println!("three"), 5 => println!("five"), 7 => println!("seven"), _ => (), } }
_
というパターンは、どんな値にもマッチします。他のアームの後に記述することで、_
は、
それまでに指定されていない全ての可能性にマッチします。()
は、ただのユニット値なので、_
の場合には、
何も起こりません。結果として、_
プレースホルダーの前に列挙していない可能性全てに対しては、
何もしたくないと言えるわけです。
ですが、一つのケースにしか興味がないような場面では、match
式はちょっと長ったらしすぎます。
このような場面用に、Rustには、if let
が用意されています。
if let
で簡潔な制御フロー
if let
記法でif
とlet
をより冗長性の少ない方法で組み合わせ、残りを無視しつつ、一つのパターンにマッチする値を扱うことができます。
Option<u8>
にマッチするけれど、値が3の時にだけコードを実行したい、リスト6-6のプログラムを考えてください。
#![allow(unused)] fn main() { let some_u8_value = Some(0u8); match some_u8_value { Some(3) => println!("three"), _ => (), } }
リスト6-6: 値がSome(3)
の時だけコードを実行するmatch
Some(3)
にマッチした時だけ何かをし、他のSome<u8>
値やNone
値の時には何もしたくありません。
match
式を満たすためには、列挙子を一つだけ処理した後に_ => ()
を追加しなければなりません。
これでは、追加すべき定型コードが多すぎます。
その代わり、if let
を使用してもっと短く書くことができます。以下のコードは、
リスト6-6のmatch
と同じように振る舞います:
#![allow(unused)] fn main() { let some_u8_value = Some(0u8); if let Some(3) = some_u8_value { println!("three"); } }
if let
という記法は等号記号で区切られたパターンと式を取り、式がmatch
に与えられ、パターンが最初のアームになったmatch
と、
同じ動作をします。
if let
を使うと、タイプ数が減り、インデントも少なくなり、定型コードも減ります。しかしながら、
match
では強制された包括性チェックを失ってしまいます。match
かif let
かの選択は、
特定の場面でどんなことをしたいかと簡潔性を得ることが包括性チェックを失うのに適切な代償となるかによります。
言い換えると、if let
は値が一つのパターンにマッチした時にコードを走らせ、
他は無視するmatch
への糖衣構文と考えることができます。
if let
では、else
を含むこともできます。else
に入るコードブロックは、
if let
とelse
に等価なmatch
式の_
の場合に入るコードブロックと同じになります。
リスト6-4のCoin
enum定義を思い出してください。ここでは、Quarter
列挙子は、
UsState
の値も保持していましたね。クォーターコインの状態を告げつつ、
見かけたクォーター以外のコインの枚数を数えたいなら、以下のようにmatch
式で実現することができるでしょう:
#![allow(unused)] fn main() { #[derive(Debug)] enum UsState { Alabama, Alaska, } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } let coin = Coin::Penny; let mut count = 0; match coin { // {:?}州のクォーターコイン Coin::Quarter(state) => println!("State quarter from {:?}!", state), _ => count += 1, } }
または、以下のようにif let
とelse
を使うこともできるでしょう:
#![allow(unused)] fn main() { #[derive(Debug)] enum UsState { Alabama, Alaska, } enum Coin { Penny, Nickel, Dime, Quarter(UsState), } let coin = Coin::Penny; let mut count = 0; if let Coin::Quarter(state) = coin { println!("State quarter from {:?}!", state); } else { count += 1; } }
match
を使って表現するには冗長的すぎるロジックがプログラムにあるようなシチュエーションに遭遇したら、
if let
もRust道具箱にあることを思い出してください。
まとめ
これで、enumを使用してワンセットの列挙された値のどれかになりうる独自の型を生成する方法を講義しました。
標準ライブラリのOption<T>
が型システムを使用して、エラーを回避する際に役立つ方法についても示しました。
enumの値がデータを内部に含む場合、処理すべきケースの数に応じて、match
かif let
を使用して値を取り出し、
使用できます。
もうRustプログラムで構造体とenumを使用して、自分の領域の概念を表現できます。API内で使用するために独自の型を生成することで、 型安全性を保証することができます: コンパイラが、各関数の予期する型の値のみを関数が得ることを確かめてくれるのです。
使用するのに率直な整理整頓されたAPIをユーザに提供し、ユーザが必要とするものだけを公開するために、 今度は、Rustのモジュールに目を向けてみましょう。
肥大化していくプロジェクトをパッケージ、クレート、モジュールを利用して管理する
大きなプログラムを書く時、そのすべてを頭の中に入れておくのは不可能になるため、コードのまとまりを良くすることが重要になります。 関係した機能をまとめ、異なる特徴を持つコードを分割することにより、特定の機能を実装しているコードを見つけたり、機能を変更したりするためにどこを探せば良いのかを明確にできます。
私達がこれまでに書いてきたプログラムは、一つのファイル内の一つのモジュール内にありました。 プロジェクトが大きくなるにつれて、これを複数のモジュールに、ついで複数のファイルに分割することで、プログラムを整理することができます。 パッケージは複数のバイナリクレートからなり、またライブラリクレートを1つもつこともできます。 パッケージが大きくなるにつれて、その一部を抜き出して分離したクレートにし、外部依存とするのもよいでしょう。 この章ではそれらのテクニックすべてを学びます。 相互に関係し合い、同時に成長するパッケージの集まりからなる巨大なプロジェクトには、 Cargoがワークスペースという機能を提供します。これは14章のCargoワークスペースで解説します。
機能をグループにまとめられることに加え、実装の詳細がカプセル化されることにより、コードをより高いレベルで再利用できるようになります: 手続きを実装し終えてしまえば、他のコードはそのコードの公開されたインターフェースを通じて、実装の詳細を知ることなくそのコードを呼び出すことができるのです。 コードをどう書くかによって、どの部分が他のコードにも使える公開のものになるのか、それとも自分だけが変更できる非公開のものになるのかが決定されます。 これもまた、記憶しておくべき細部を制限してくれる方法のひとつです。
関係する概念にスコープがあります: コードが記述されているネストされた文脈には、「スコープ内」として定義される名前の集合があります。 コードを読んだり書いたりコンパイルしたりする時には、プログラマーやコンパイラは特定の場所にある特定の名前が、変数・関数・構造体・enum・モジュール・定数・その他のどの要素を表すのか、そしてその要素は何を意味するのかを知る必要があります。 そこでスコープを作り、どの名前がスコープ内/スコープ外にあるのかを変更することができます。 同じ名前のものを2つ同じスコープ内に持つことはできません。そこで、名前の衝突を解決するための方法があります。
Rustには、どの詳細を公開するか、どの詳細を非公開にするか、どの名前がプログラムのそれぞれのスコープにあるか、といったコードのまとまりを保つためのたくさんの機能があります。 これらの機能は、まとめて「モジュールシステム」と呼ばれることがあり、以下のようなものが含まれます。
- パッケージ: クレートをビルドし、テストし、共有することができるCargoの機能
- クレート: ライブラリか実行可能ファイルを生成する、木構造をしたモジュール群
- モジュール と use: これを使うことで、パスの構成、スコープ、公開するか否かを決定できます
- パス: 要素(例えば構造体や関数やモジュール)に名前をつける方法
この章では、これらの機能をすべて学び、これらがどう相互作用するかについて議論し、これらをどう使ってスコープを制御するのかについて説明します。 この章を読み終わる頃には、モジュールシステムをしっかりと理解し、熟練者のごとくスコープを扱うことができるようになっているでしょう!
パッケージとクレート
最初に学ぶモジュールシステムの要素は、パッケージとクレートです。 クレートはバイナリかライブラリのどちらかです。 クレートルート (crate root) とは、Rustコンパイラの開始点となり、クレートのルートモジュールを作るソースファイルのことです(モジュールについて詳しくは「モジュールを定義して、スコープとプライバシーを制御する」のセクションで説明します)。 パッケージ はある機能群を提供する1つ以上のクレートです。 パッケージは Cargo.toml という、それらのクレートをどのようにビルドするかを説明するファイルを持っています。
パッケージが何を持ってよいかはいくつかのルールで決まっています。 パッケージは0個か1個のライブラリクレートを持っていないといけません。それ以上は駄目です。 バイナリクレートはいくらでも持って良いですが、少なくとも(ライブラリでもバイナリでも良いですが)1つのクレートを持っていないといけません。
パッケージを作る時に何が起こるか見てみましょう。
まず、cargo new
というコマンドを入力します:
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
このコマンドを入力したとき、Cargoは Cargo.toml ファイルを作り、パッケージを作ってくれました。
Cargo.toml の中身を見ても、src/main.rs については何も書いてありません。これは、Cargoは src/main.rs が、パッケージと同じ名前を持つバイナリクレートのクレートルートであるという慣習に従っているためです。
同じように、Cargoはパッケージディレクトリに src/lib.rs が含まれていたら、パッケージにはパッケージと同じ名前のライブラリクレートが含まれており、src/lib.rs がそのクレートルートなのだと判断します。
Cargoはクレートルートファイルを rustc
に渡し、ライブラリやバイナリをビルドします。
今、このパッケージには src/main.rs しか含まれておらず、つまりこのパッケージはmy-project
という名前のバイナリクレートのみを持っているということです。
もしパッケージが src/main.rs と src/lib.rs を持っていたら、クレートは2つになります:どちらもパッケージと同じ名前を持つ、ライブラリクレートとバイナリクレートです。
ファイルを src/bin ディレクトリに置くことで、パッケージは複数のバイナリクレートを持つことができます。それぞれのファイルが別々のバイナリクレートになります。
クレートは、関連した機能を一つのスコープにまとめることで、その機能が複数のプロジェクト間で共有しやすいようにします。
例えば、2章で使ったrand
クレートは、乱数を生成する機能を提供します。
rand
クレートを私達のプロジェクトのスコープに持ち込むことで、この機能を私達のプロジェクトで使うことができます。
rand
クレートが提供する機能にはすべて、クレートの名前rand
を使ってアクセスできます。
クレートの機能をそれ自身のスコープの中に入れたままにしておくことは、ある機能が私達のクレートで定義されたのかrand
クレートで定義されたのかを明確にし、名前の衝突を予防してくれます。
例えば、rand
クレートはRng
という名前のトレイトを提供しています。
更に、私達のクレートでRng
という名前のstruct
を定義することもできます。
クレートの機能はそのスコープ内の名前空間に位置づけられているので、rand
を依存先として追加しても、コンパイラはRng
という名前が何を意味するのかについて混乱することはないのです。
私達のクレートでは、私達の定義したstruct Rng
のことであり、rand
クレートのRng
トレイトにはrand::Rng
でアクセスするというわけです。
では、モジュールシステムの話に移りましょう!
モジュールを定義して、スコープとプライバシーを制御する
この節では、モジュールと、その他のモジュールシステムの要素
――すなわち、要素に名前をつけるための パス 、パスをスコープに持ち込むuse
キーワード、要素を公開するpub
キーワード――
について学びます。
また、as
キーワード、外部パッケージ、glob演算子についても話します。
とりあえず、今はモジュールに集中しましょう!
モジュール はクレート内のコードをグループ化し、可読性と再利用性を上げるのに役に立ちます。 モジュールは要素の プライバシー も制御できます。プライバシーとは、要素がコードの外側で使える (公開 public) のか、内部の実装の詳細であり外部では使えない (非公開 private) のかです。
例えば、レストランの機能を提供するライブラリクレートを書いてみましょう。 実際にレストランを実装することではなく、コードの関係性に注目したいので、関数にシグネチャをつけますが中身は空白のままにします。
レストラン業界では、レストランの一部を 接客部門 (front of house) といい、その他を 後方部門 (back of house) といいます。 接客部門とはお客さんがいるところです。接客係がお客様を席に案内し、給仕係が注文と支払いを受け付け、バーテンダーが飲み物を作ります。 後方部門とはシェフや料理人がキッチンで働き、皿洗い係が食器を片付け、マネージャが管理業務をする場所です。
私達のクレートを現実のレストランと同じような構造にするために、関数をネストしたモジュールにまとめましょう。
restaurant
という名前の新しいライブラリをcargo new --lib restaurant
と実行することで作成し、Listing 7-1 のコードを src/lib.rs に書き込み、モジュールと関数のシグネチャを定義してください。
ファイル名: src/lib.rs
mod front_of_house { mod hosting { fn add_to_waitlist() {} fn seat_at_table() {} } mod serving { fn take_order() {} fn serve_order() {} fn take_payment() {} } } fn main() {}
Listing 7-1: front_of_house
モジュールにその他のモジュールが含まれ、さらにそれらが関数を含んでいる
モジュールは、mod
キーワードを書き、次にモジュールの名前(今回の場合、front_of_house
)を指定することで定義されます。
モジュールの中には、今回だとhosting
とserving
のように、他のモジュールをおくこともできます。
モジュールにはその他の要素の定義も置くことができます。例えば、構造体、enum、定数、トレイト、そして(Listing 7-1のように)関数です。
モジュールを使うことで、関連する定義を一つにまとめ、関連する理由を名前で示せます。 このコードを使うプログラマーは、定義を全部読むことなく、グループ単位でコードを読み進められるので、欲しい定義を見つけ出すのが簡単になるでしょう。 このコードに新しい機能を付け加えるプログラマーは、プログラムのまとまりを保つために、どこにその機能のコードを置けば良いのかがわかるでしょう。
以前、 src/main.rs と src/lib.rs はクレートルートと呼ばれていると言いました。
この名前のわけは、 モジュールツリー と呼ばれるクレートのモジュール構造の根っこ (ルート)にこれら2つのファイルの中身がcrate
というモジュールを形成するからです。
Listing 7-2は、Listing 7-1の構造のモジュールツリーを示しています。
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
Listing 7-2: Listing 7-1 のコードのモジュールツリー
このツリーを見ると、どのモジュールがどのモジュールの中にネストしているのかがわかります(例えば、hosting
はfront_of_house
の中にネストしています)。
また、いくつかのモジュールはお互いに 兄弟 の関係にある、つまり、同じモジュール内で定義されていることもわかります(例えばhosting
とserving
はfront_of_house
で定義されています)。
他にも、家族関係の比喩を使って、モジュールAがモジュールBの中に入っている時、AはBの 子 であるといい、BはAの 親 であるといいます。
モジュールツリー全体が、暗黙のうちに作られたcrate
というモジュールの下にあることにも注目してください。
モジュールツリーを見ていると、コンピュータのファイルシステムのディレクトリツリーを思い出すかもしれません。その喩えはとても適切です! ファイルシステムのディレクトリのように、モジュールはコードをまとめるのに使われるのです。 そしてディレクトリからファイルを見つけるように、目的のモジュールを見つけ出す方法が必要になります。
モジュールツリーの要素を示すためのパス
ファイルシステムの中を移動する時と同じように、Rustにモジュールツリー内の要素を見つけるためにはどこを探せばいいのか教えるためにパスを使います。 関数を呼び出したいなら、そのパスを知っていなければなりません。
パスは2つの形を取ることができます:
- 絶対パス は、クレートの名前か
crate
という文字列を使うことで、クレートルートからスタートします。 - 相対パス は、
self
、super
または今のモジュール内の識別子を使うことで、現在のモジュールからスタートします。
絶対パスも相対パスも、その後に一つ以上の識別子がダブルコロン(::
)で仕切られて続きます。
Listing 7-1の例に戻ってみましょう。
add_to_waitlist
関数をどうやって呼べばいいでしょうか?
すなわち、add_to_waitlist
のパスは何でしょうか?
Listing 7-3 は、モジュールと関数をいくつか取り除いてコードをやや簡潔にしています。
これを使って、クレートルートに定義された新しいeat_at_restaurant
という関数から、add_to_waitlist
関数を呼びだす2つの方法を示しましょう。
eat_at_restaurant
関数はこのライブラリクレートの公開 (public) APIの1つなので、pub
キーワードをつけておきます。
pub
については、パスをpub
キーワードで公開するの節でより詳しく学びます。
この例はまだコンパイルできないことに注意してください。理由はすぐに説明します。
ファイル名: src/lib.rs
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
// 絶対パス
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
// 相対パス
front_of_house::hosting::add_to_waitlist();
}
Listing 7-3: add_to_waitlist
関数を絶対パスと相対パスで呼び出す
eat_at_restaurant
で最初にadd_to_waitlist
関数を呼び出す時、絶対パスを使っています。
add_to_waitlist
関数はeat_at_restaurant
と同じクレートで定義されているので、crate
キーワードで絶対パスを始めることができます。
crate
の後は、add_to_waitlist
にたどり着くまで、後に続くモジュールを書き込んでいます。
同じ構造のファイルシステムを想像すれば、/front_of_house/hosting/add_to_waitlist
とパスを指定してadd_to_waitlist
を実行していることに相当します。
crate
という名前を使ってクレートルートからスタートするというのは、/
を使ってファイルシステムのルートからスタートするようなものです。
eat_at_restaurant
で2回目にadd_to_waitlist
関数を呼び出す時、相対パスを使っています。
パスは、モジュールツリーにおいてeat_at_restaurant
と同じ階層で定義されているモジュールであるfront_of_house
からスタートします。
これはファイルシステムでfront_of_house/hosting/add_to_waitlist
というパスを使っているのに相当します。
名前から始めるのは、パスが相対パスであることを意味します。
相対パスを使うか絶対パスを使うかは、プロジェクトによって決めましょう。
要素を定義するコードを、その要素を使うコードと別々に動かすか一緒に動かすか、どちらが起こりそうかによって決めるのが良いです。
例えば、front_of_house
モジュールとeat_at_restaurant
関数をcustomer_experience
というモジュールに移動させると、add_to_waitlist
への絶対パスを更新しないといけませんが、相対パスは有効なままです。
しかし、eat_at_restaurant
関数だけをdining
というモジュールに移動させると、add_to_waitlist
への絶対パスは同じままですが、相対パスは更新しないといけないでしょう。
コードの定義と、その要素の呼び出しは独立に動かしそうなので、絶対パスのほうが好ましいです。
では、Listing 7-3 をコンパイルしてみて、どうしてこれはまだコンパイルできないのか考えてみましょう! エラーをListing 7-4 に示しています。
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^
error[E0603]: module `hosting` is private
--> src/lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant`.
To learn more, run the command again with --verbose.
Listing 7-4: Listing 7-3のコードをビルドしたときのコンパイルエラー
エラーメッセージは、hosting
は非公開 (private) だ、と言っています。
言い換えるなら、hosting
モジュールとadd_to_waitlist
関数へのパスは正しいが、非公開な部分へのアクセスは許可されていないので、Rustがそれを使わせてくれないということです。
モジュールはコードの整理に役立つだけではありません。 モジュールはRustの プライバシー境界 も定義します。これは、外部のコードが知ったり、呼び出したり、依存したりしてはいけない実装の詳細をカプセル化する線引きです。 なので、関数や構造体といった要素を非公開にしたければ、モジュールに入れればよいのです。
Rustにおけるプライバシーは、「あらゆる要素(関数、メソッド、構造体、enum、モジュールおよび定数)は標準では非公開」というやり方で動いています。 親モジュールの要素は子モジュールの非公開要素を使えませんが、子モジュールの要素はその祖先モジュールの要素を使えます。 これは、子モジュールは実装の詳細を覆い隠しますが、子モジュールは自分の定義された文脈を見ることができるためです。 レストランの喩えを続けるなら、レストランの後方部門になったつもりでプライバシーのルールを考えてみてください。レストランの顧客にはそこで何が起こっているのかは非公開ですが、そこで働くオフィスマネージャには、レストランのことは何でも見えるし何でもできるのです。
Rustは、内部実装の詳細を隠すことが標準であるようにモジュールシステムを機能させることを選択しました。
こうすることで、内部のコードのどの部分が、外部のコードを壊すことなく変更できるのかを知ることができます。
しかし、pub
キーワードを使って要素を公開することで、子モジュールの内部部品を外部の祖先モジュールに見せることができます。
パスをpub
キーワードで公開する
Listing 7-4の、hosting
モジュールが非公開だと言ってきていたエラーに戻りましょう。
親モジュールのeat_at_restaurant
関数が子モジュールのadd_to_waitlist
関数にアクセスできるようにしたいので、hosting
モジュールにpub
キーワードをつけます。Listing 7-5のようになります。
ファイル名: src/lib.rs
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
// 絶対パス
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
// 相対パス
front_of_house::hosting::add_to_waitlist();
}
Listing 7-5: hosting
モジュールを pub
として宣言することでeat_at_restaurant
から使う
残念ながら、Listing 7-5 のコードもListing 7-6 に示されるようにエラーとなります。
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:9:37
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:12:30
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant`.
To learn more, run the command again with --verbose.
Listing 7-6: Listing 7-5 のコードをビルドしたときのコンパイルエラー
何が起きたのでしょう?pub
キーワードをmod hosting
の前に追加したことで、このモジュールは公開されました。
この変更によって、front_of_house
にアクセスできるなら、hosting
にもアクセスできるようになりました。
しかしhosting
の 中身 はまだ非公開です。モジュールを公開してもその中身は公開されないのです。
モジュールにpub
キーワードがついていても、祖先モジュールのコードはモジュールを参照できるようになるだけです。
Listing 7-6 のエラーはadd_to_waitlist
関数が非公開だと言っています。
プライバシーのルールは、モジュール同様、構造体、enum、関数、メソッドにも適用されるのです。
add_to_waitlist
の定義の前にpub
キーワードを追加して、これも公開しましょう。
ファイル名: src/lib.rs
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } pub fn eat_at_restaurant() { // Absolute path // 絶対パス crate::front_of_house::hosting::add_to_waitlist(); // Relative path // 相対パス front_of_house::hosting::add_to_waitlist(); } fn main() {}
Listing 7-7: pub
キーワードをmod hosting
とfn add_to_waitlist
に追加することで、eat_at_restaurant
からこの関数を呼べるようになる
これでこのコードはコンパイルできます!
絶対パスと相対パスをもう一度確認して、どうしてpub
キーワードを追加することでadd_to_waitlist
のそれらのパスを使えるようになるのか、プライバシールールの観点からもう一度確認してみてみましょう。
絶対パスは、クレートのモジュールツリーのルートであるcrate
から始まります。
クレートルートの中にfront_of_house
が定義されています。
front_of_house
は公開されていませんが、eat_at_restaurant
関数はfront_of_house
と同じモジュール内で定義されている(つまり、eat_at_restaurant
とfront_of_house
は兄弟な)ので、eat_at_restaurant
からfront_of_house
を参照することができます。
次はpub
の付いたhosting
モジュールです。
hosting
の親モジュールにアクセスできるので、hosting
にもアクセスできます。
最後に、add_to_waitlist
関数はpub
が付いており、私達はその親モジュールにアクセスできるので、この関数呼び出しはうまく行くというわけです。
相対パスについても、最初のステップを除けば同じ理屈です。パスをクレートルートから始めるのではなくて、front_of_house
から始めるのです。
front_of_house
モジュールはeat_at_restaurant
と同じモジュールで定義されているので、eat_at_restaurant
が定義されている場所からの相対パスが使えます。
そして、hosting
とadd_to_waitlist
はpub
が付いていますから、残りのパスについても問題はなく、この関数呼び出しは有効というわけです。
相対パスをsuper
で始める
親モジュールから始まる相対パスなら、super
を最初につけることで構成できます。
ファイルシステムパスを..
構文で始めるのに似ています。
どのようなときにこの機能が使いたくなるのでしょう?
シェフが間違った注文を修正し、自分でお客さんに持っていくという状況をモデル化している、Listing 7-8 を考えてみてください。
fix_incorrect_order
関数はserve_order
関数を呼び出すために、super
から始まるserve_order
関数へのパスを使っています。
ファイル名: src/lib.rs
fn serve_order() {} mod back_of_house { fn fix_incorrect_order() { cook_order(); super::serve_order(); } fn cook_order() {} } fn main() {}
Listing 7-8: super
で始まる相対パスを使って関数を呼び出す
fix_incorrect_order
関数はback_of_house
モジュールの中にあるので、super
を使ってback_of_house
の親モジュールにいけます。親モジュールは、今回の場合ルートであるcrate
です。
そこから、serve_order
を探し、見つけ出します。
成功!
もしクレートのモジュールツリーを再編成することにした場合でも、back_of_house
モジュールとserve_order
関数は同じ関係性で有り続け、一緒に動くように思われます。
そのため、super
を使うことで、将来このコードが別のモジュールに移動するとしても、更新する場所が少なくて済むようにしました。
構造体とenumを公開する
構造体やenumもpub
を使って公開するよう指定できますが、追加の細目がいくつかあります。
構造体定義の前にpub
を使うと、構造体は公開されますが、構造体のフィールドは非公開のままなのです。
それぞれのフィールドを公開するか否かを個々に決められます。
Listing 7-9 では、公開のtoast
フィールドと、非公開のseasonal_fruit
フィールドをもつ公開のback_of_house::Breakfast
構造体を定義しました。
これは、例えば、レストランで、お客さんが食事についてくるパンの種類は選べるけれど、食事についてくるフルーツは季節と在庫に合わせてシェフが決める、という状況をモデル化しています。
提供できるフルーツはすぐに変わるので、お客さんはフルーツを選ぶどころかどんなフルーツが提供されるのか知ることもできません。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { mod back_of_house { pub struct Breakfast { pub toast: String, seasonal_fruit: String, } impl Breakfast { pub fn summer(toast: &str) -> Breakfast { Breakfast { toast: String::from(toast), seasonal_fruit: String::from("peaches"), } } } } pub fn eat_at_restaurant() { // Order a breakfast in the summer with Rye toast // 夏 (summer) にライ麦 (Rye) パン付き朝食を注文 let mut meal = back_of_house::Breakfast::summer("Rye"); // Change our mind about what bread we'd like // やっぱり別のパンにする meal.toast = String::from("Wheat"); println!("I'd like {} toast please", meal.toast); // The next line won't compile if we uncomment it; we're not allowed // to see or modify the seasonal fruit that comes with the meal // 下の行のコメントを外すとコンパイルできない。食事についてくる // 季節のフルーツを知ることも修正することも許されていないので // meal.seasonal_fruit = String::from("blueberries"); } }
Listing 7-9: 公開のフィールドと非公開のフィールドとを持つ構造体
back_of_house::Breakfast
のtoast
フィールドは公開されているので、eat_at_restaurant
においてtoast
をドット記法を使って読み書きできます。
seasonal_fruit
は非公開なので、eat_at_restaurant
においてseasonal_fruit
は使えないということに注意してください。
seasonal_fruit
を修正している行のコメントを外して、どのようなエラーが得られるか試してみてください!
また、back_of_house::Breakfast
は非公開のフィールドを持っているので、Breakfast
のインスタンスを作成 (construct) する公開された関連関数が構造体によって提供されている必要があります(ここではsummer
と名付けました)。
もしBreakfast
にそのような関数がなかったら、eat_at_restaurant
において非公開であるseasonal_fruit
の値を設定できないので、Breakfast
のインスタンスを作成できません。
一方で、enumを公開すると、そのヴァリアントはすべて公開されます。
Listing 7-10 に示されているように、pub
はenum
キーワードの前にだけおけばよいのです。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { mod back_of_house { pub enum Appetizer { Soup, Salad, } } pub fn eat_at_restaurant() { let order1 = back_of_house::Appetizer::Soup; let order2 = back_of_house::Appetizer::Salad; } }
Listing 7-10: enumを公開に指定することはそのヴァリアントをすべて公開にする
Appetizer
というenumを公開したので、Soup
とSalad
というヴァリアントもeat_at_restaurant
で使えます。
enumはヴァリアントが公開されてないとあまり便利ではないのですが、毎回enumのすべてのヴァリアントにpub
をつけるのは面倒なので、enumのヴァリアントは標準で公開されるようになっているのです。
構造体はフィールドが公開されていなくても便利なことが多いので、構造体のフィールドは、pub
がついてない限り標準で非公開という通常のルールに従うわけです。
まだ勉強していない、pub
の関わるシチュエーションがもう一つあります。モジュールシステムの最後の機能、use
キーワードです。
use
自体の勉強をした後、pub
とuse
を組み合わせる方法についてお見せします。
use
キーワードでパスをスコープに持ち込む
これまで関数呼び出しのために書いてきたパスは、長く、繰り返しも多くて不便なものでした。
例えば、Listing 7-7 においては、絶対パスを使うか相対パスを使うかにかかわらず、add_to_waitlist
関数を呼ぼうと思うたびにfront_of_house
とhosting
も指定しないといけませんでした。
ありがたいことに、この手続きを簡単化する方法があります。
use
キーワードを使うことで、パスを一度スコープに持ち込んでしまえば、それ以降はパス内の要素がローカルにあるかのように呼び出すことができるのです。
Listing 7-11 では、crate::front_of_house::hosting
モジュールをeat_at_restaurant
関数のスコープに持ち込むことで、eat_at_restaurant
において、hosting::add_to_waitlist
と指定するだけでadd_to_waitlist
関数を呼び出せるようにしています。
ファイル名: src/lib.rs
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); } fn main() {}
Listing 7-11: use
でモジュールをスコープに持ち込む
use
とパスをスコープに追加することは、ファイルシステムにおいてシンボリックリンクを張ることに似ています。
use crate::front_of_house::hosting
をクレートルートに追加することで、hosting
はこのスコープで有効な名前となり、まるでhosting
はクレートルートで定義されていたかのようになります。
スコープにuse
で持ち込まれたパスも、他のパスと同じようにプライバシーがチェックされます。
use
と相対パスで要素をスコープに持ち込むこともできます。
Listing 7-12 はListing 7-11 と同じふるまいを得るためにどう相対パスを書けば良いかを示しています。
ファイル名: src/lib.rs
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use self::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); } fn main() {}
Listing 7-12: モジュールをuse
と相対パスを使ってスコープに持ち込む
慣例に従ったuse
パスを作る
Listing 7-11 を見て、なぜuse crate::front_of_house::hosting
と書いてeat_at_restaurant
内でhosting::add_to_waitlist
と呼び出したのか不思議に思っているかもしれません。Listing 7-13 のように、use
でadd_to_waitlist
までのパスをすべて指定しても同じ結果が得られるのに、と。
ファイル名: src/lib.rs
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } use crate::front_of_house::hosting::add_to_waitlist; pub fn eat_at_restaurant() { add_to_waitlist(); add_to_waitlist(); add_to_waitlist(); } fn main() {}
Listing 7-13: add_to_waitlist
関数をuse
でスコープに持ち込む。このやりかたは慣例的ではない
Listing 7-11 も 7-13 もおなじ仕事をしてくれますが、関数をスコープにuse
で持ち込む場合、Listing 7-11 のほうが慣例的なやり方です。
関数の親モジュールをuse
で持ち込むことで、関数を呼び出す際、毎回親モジュールを指定しなければならないようにすれば、フルパスを繰り返して書くことを抑えつつ、関数がローカルで定義されていないことを明らかにできます。
Listing 7-13 のコードではどこでadd_to_waitlist
が定義されたのかが不明瞭です。
一方で、構造体やenumその他の要素をuse
で持ち込むときは、フルパスを書くのが慣例的です。
Listing 7-14 は標準ライブラリのHashMap
構造体をバイナリクレートのスコープに持ち込む慣例的なやり方を示しています。
ファイル名: src/main.rs
use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2); }
Listing 7-14: HashMap
を慣例的なやり方でスコープに持ち込む
こちらの慣例の背後には、はっきりとした理由はありません。自然に発生した慣習であり、みんなRustのコードをこのやり方で読み書きするのに慣れてしまったというだけです。
同じ名前の2つの要素をuse
でスコープに持ち込むのはRustでは許されないので、そのときこの慣例は例外的に不可能です。
Listing 7-15は、同じ名前を持つけれど異なる親モジュールを持つ2つのResult
型をスコープに持ち込み、それらを参照するやり方を示しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::fmt; use std::io; fn function1() -> fmt::Result { // --snip-- // (略) Ok(()) } fn function2() -> io::Result<()> { // --snip-- // (略) Ok(()) } }
Listing 7-15: 同じ名前を持つ2つの型を同じスコープに持ち込むには親モジュールを使わないといけない。
このように、親モジュールを使うことで2つのResult
型を区別できます。
もしuse std::fmt::Result
と use std::io::Result
と書いていたとしたら、2つのResult
型が同じスコープに存在することになり、私達がResult
を使ったときにどちらのことを意味しているのかRustはわからなくなってしまいます。
新しい名前をas
キーワードで与える
同じ名前の2つの型をuse
を使って同じスコープに持ち込むという問題には、もう一つ解決策があります。パスの後に、as
と型の新しいローカル名、即ちエイリアスを指定すればよいのです。
Listing 7-16 は、Listing 7-15 のコードを、2つのResult
型のうち一つをas
を使ってリネームするという別のやり方で書いたものを表しています。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::fmt::Result; use std::io::Result as IoResult; fn function1() -> Result { // --snip-- Ok(()) } fn function2() -> IoResult<()> { // --snip-- Ok(()) } }
Listing 7-16: 型がスコープに持ち込まれた時、as
キーワードを使ってその名前を変えている
2つめのuse
文では、std::io::Result
に、IoResult
という新たな名前を選んでやります。std::fmt
のResult
もスコープに持ち込んでいますが、この名前はこれとは衝突しません。
Listing 7-15もListing 7-16も慣例的とみなされているので、どちらを使っても構いませんよ!
pub use
を使って名前を再公開する
use
キーワードで名前をスコープに持ちこんだ時、新しいスコープで使用できるその名前は非公開です。
私達のコードを呼び出すコードが、まるでその名前が私達のコードのスコープで定義されていたかのように参照できるようにするためには、pub
とuse
を組み合わせればいいです。
このテクニックは、要素を自分たちのスコープに持ち込むだけでなく、他の人がその要素をその人のスコープに持ち込むことも可能にすることから、再公開 (re-exporting) と呼ばれています。
Listing 7-17 は Listing 7-11 のコードのルートモジュールでのuse
をpub use
に変更したものを示しています。
ファイル名: src/lib.rs
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } pub use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); hosting::add_to_waitlist(); hosting::add_to_waitlist(); } fn main() {}
Listing 7-17: pub use
で、新たなスコープのコードがその名前を使えるようにする
pub use
を使うことで、外部のコードがhosting::add_to_waitlist
を使ってadd_to_waitlist
関数を呼び出せるようになりました。
pub use
を使っていなければ、eat_at_restaurant
関数はhosting::add_to_waitlist
を自らのスコープ内で使えるものの、外部のコードはこの新しいパスを利用することはできないでしょう。
再公開は、あなたのコードの内部構造と、あなたのコードを呼び出すプログラマーたちのその領域に関しての見方が異なるときに有用です。
例えば、レストランの比喩では、レストランを経営している人は「接客部門 (front of house)」と「後方部門 (back of house)」のことについて考えるでしょう。
しかし、レストランを訪れるお客さんは、そのような観点からレストランの部門について考えることはありません。
pub use
を使うことで、ある構造でコードを書きつつも、別の構造で公開するということが可能になります。
こうすることで、私達のライブラリを、ライブラリを開発するプログラマにとっても、ライブラリを呼び出すプログラマにとっても、よく整理されたものとすることができます。
外部のパッケージを使う
2章で、乱数を得るためにrand
という外部パッケージを使って、数当てゲームをプログラムしました。
rand
を私達のプロジェクトで使うために、次の行を Cargo.toml に書き加えましたね:
ファイル名: Cargo.toml
rand = "0.8.3"
rand
を依存 (dependency) として Cargo.toml に追加すると、rand
パッケージとそのすべての依存をcrates.ioからダウンロードして、私達のプロジェクトでrand
が使えるようにするようCargoに命令します。
そして、rand
の定義を私達のパッケージのスコープに持ち込むために、クレートの名前であるrand
から始まるuse
の行を追加し、そこにスコープに持ち込みたい要素を並べました。
2章の乱数を生成するの節で、Rng
トレイトをスコープに持ち込みrand::thread_rng
関数を呼び出したことを思い出してください。
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..101);
println!("The secret number is: {}", secret_number); //秘密の数字は次の通り: {}
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
Rustコミュニティに所属する人々がcrates.ioでたくさんのパッケージを利用できるようにしてくれており、上と同じステップを踏めばそれらをあなたのパッケージに取り込むことができます:あなたのパッケージの Cargo.toml ファイルにそれらを書き並べ、use
を使って要素をクレートからスコープへと持ち込めばよいのです。
標準ライブラリ (std
) も、私達のパッケージの外部にあるクレートだということに注意してください。
標準ライブラリはRust言語に同梱されているので、 Cargo.toml を std
を含むように変更する必要はありません。
しかし、その要素をそこから私達のパッケージのスコープに持ち込むためには、use
を使って参照する必要はあります。
例えば、HashMap
には次の行を使います。
#![allow(unused)] fn main() { use std::collections::HashMap; }
これは標準ライブラリクレートの名前std
から始まる絶対パスです。
巨大なuse
のリストをネストしたパスを使って整理する
同じクレートか同じモジュールで定義された複数の要素を使おうとする時、それぞれの要素を一行一行並べると、縦に大量のスペースを取ってしまいます。
例えば、Listing 2-4の数当てゲームで使った次の2つのuse
文がstd
からスコープへ要素を持ち込みました。
ファイル名: src/main.rs
use rand::Rng;
// --snip--
// (略)
use std::cmp::Ordering;
use std::io;
// --snip--
// (略)
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("The secret number is: {}", secret_number);
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
代わりに、ネストしたパスを使うことで、同じ一連の要素を1行でスコープに持ち込めます。 これをするには、Listing 7-18 に示されるように、パスの共通部分を書き、2つのコロンを続け、そこで波括弧で互いに異なる部分のパスのリストを囲みます。
ファイル名: src/main.rs
use rand::Rng;
// --snip--
// (略)
use std::{cmp::Ordering, io};
// --snip--
// (略)
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("The secret number is: {}", secret_number);
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
Listing 7-18: 同じプレフィックスをもつ複数の要素をスコープに持ち込むためにネストしたパスを指定する
大きなプログラムにおいては、同じクレートやモジュールからのたくさんの要素をネストしたパスで持ち込むようにすれば、独立したuse
文の数を大きく減らすことができます!
ネストしたパスはパスのどの階層においても使うことができます。これはサブパスを共有する2つのuse
文を合体させるときに有用です。
例えば、Listing 7-19 は2つのuse
文を示しています:1つはstd::io
をスコープに持ち込み、もう一つはstd::io::Write
をスコープに持ち込んでいます。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::io; use std::io::Write; }
Listing 7-19: 片方がもう片方のサブパスである2つのuse
文
これらの2つのパスの共通部分はstd::io
であり、そしてこれは最初のパスにほかなりません。これらの2つのパスを1つのuse
文へと合体させるには、Listing 7-20 に示されるように、ネストしたパスにself
を使いましょう。
ファイル名: src/lib.rs
#![allow(unused)] fn main() { use std::io::{self, Write}; }
Listing 7-20: Listing 7-19 のパスを一つの use
文に合体させる
この行は std::io
とstd::io::Write
をスコープに持ち込みます。
glob演算子
パスにおいて定義されているすべての公開要素をスコープに持ち込みたいときは、glob演算子 *
をそのパスの後ろに続けて書きましょう:
#![allow(unused)] fn main() { use std::collections::*; }
このuse
文はstd::collections
のすべての公開要素を現在のスコープに持ち込みます。
glob演算子を使う際にはご注意を!
globをすると、どの名前がスコープ内にあり、プログラムで使われている名前がどこで定義されたのか分かりづらくなります。
glob演算子はしばしば、テストの際、テストされるあらゆるものをtests
モジュールに持ち込むために使われます。これについては11章テストの書き方の節で話します。
glob演算子はプレリュードパターンの一部としても使われることがあります:そのようなパターンについて、より詳しくは標準ライブラリのドキュメントをご覧ください。
モジュールを複数のファイルに分割する
この章のすべての例において、今までのところ、複数のモジュールを一つのファイルに定義していました。 モジュールが大きくなる時、コードを読み進めやすくするため、それらの定義を別のファイルへ移動させたくなるかもしれません。
例えば、Listing 7-17 のコードからはじめましょう。クレートルートのファイルをListing 7-21 のコードを持つように変更して、front_of_house
モジュールをそれ専用のファイルsrc/front_of_house.rs
に動かしましょう。
今回、クレートルートファイルはsrc/lib.rs
ですが、この手続きはクレートルートファイルがsrc/main.rs
であるバイナリクレートでもうまく行きます。
ファイル名: src/lib.rs
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
Listing 7-21: front_of_house
モジュールを宣言する。その中身はsrc/front_of_house.rs
内にある
そして、 Listing 7-22 のように、src/front_of_house.rs にはfront_of_house
モジュールの中身の定義を与えます。
ファイル名: src/front_of_house.rs
pub mod hosting {
pub fn add_to_waitlist() {}
}
Listing 7-22: src/front_of_house.rsにおける、front_of_house
モジュール内部の定義
mod front_of_house
の後にブロックではなくセミコロンを使うと、Rustにモジュールの中身をモジュールと同じ名前をした別のファイルから読み込むように命令します。
私達の例で、つづけてhosting
モジュールをそれ専用のファイルに抽出するには、src/front_of_house.rs
がhosting
モジュールの宣言のみを含むように変更します:
ファイル名: src/front_of_house.rs
pub mod hosting;
さらにsrc/front_of_house ディレクトリとsrc/front_of_house/hosting.rs ファイルを作って、hosting
モジュール内でなされていた定義を持つようにします。
ファイル名: src/front_of_house/hosting.rs
#![allow(unused)] fn main() { pub fn add_to_waitlist() {} }
定義は別のファイルにあるにもかかわらず、モジュールツリーは同じままであり、eat_at_restaurant
内での関数呼び出しもなんの変更もなくうまく行きます。
このテクニックのおかげで、モジュールが大きくなってきた段階で新しいファイルへ動かす、ということができます。
src/lib.rs におけるpub use crate::front_of_house::hosting
という文も変わっていないし、use
はどのファイルがクレートの一部としてコンパイルされるかになんの影響も与えないということに注意してください。
mod
キーワードがモジュールを宣言したなら、Rustはそのモジュールに挿入するためのコードを求めて、モジュールと同じ名前のファイルの中を探すというわけです。
まとめ
Rustでは、パッケージを複数のクレートに、そしてクレートを複数のモジュールに分割して、あるモジュールで定義された要素を他のモジュールから参照することができます。
これは絶対パスか相対パスを指定することで行なえます。
これらのパスはuse
文でスコープに持ち込むことができ、こうすると、そのスコープで要素を複数回使う時に、より短いパスで済むようになります。
モジュールのコードは標準では非公開ですが、pub
キーワードを追加することで定義を公開することができます。
次の章では、きちんと整理されたあなたのコードで使うことができる、標準ライブラリのいくつかのコレクションデータ構造を見ていきます。
一般的なコレクション
Rustの標準ライブラリは、コレクションと呼ばれる多くの非常に有益なデータ構造を含んでいます。他の多くのデータ型は、 ある一つの値を表しますが、コレクションは複数の値を含むことができます。組み込みの配列とタプル型とは異なり、 これらのコレクションが指すデータはヒープに確保され、データ量はコンパイル時にわかる必要はなく、 プログラムの実行にあわせて、伸縮可能であることになります。各種のコレクションには異なる能力とコストが存在し、 自分の現在の状況に最適なものを選び取るスキルは、時間とともに育っていきます。この章では、 Rustのプログラムにおいて、非常に頻繁に使用される3つのコレクションについて議論しましょう。
- ベクタ型は、可変長の値を並べて保持できる。
- 文字列は、文字のコレクションである。以前、
String
型について触れたが、 この章ではより掘り下げていく。 - ハッシュマップは、値を特定のキーと紐付けさせてくれる。より一般的なデータ構造である、 マップの特定の実装である。
標準ライブラリで提供されている他の種のコレクションについて学ぶには、 ドキュメントを参照されたし。
ベクタ型、文字列、ハッシュマップの生成と更新方法や、各々が特別な点について議論していきましょう。
ベクタで値のリストを保持する
最初に見るコレクション型はVec<T>
であり、これはベクタとしても知られています。
ベクタは単体のデータ構造でありながら複数の値を保持でき、それらの値をメモリ上に隣り合わせに並べます。
ベクタには同じ型の値しか保持できません。
要素のリストがある場合にベクタは有用です。
例えば、テキストファイルの各行とか、ショッピングカートのアイテムの価格などです。
新しいベクタを生成する
空のベクタを新たに作るには、リスト8-1に示すようにVec::new
関数を呼びます。
fn main() { let v: Vec<i32> = Vec::new(); }
リスト8-1:新しい空のベクタを生成してi32
型の値を保持する
ここで、型注釈を付けていることに注目してください。
なぜなら、このベクタに対して何も値を挿入していないので、コンパイラには私たちがどんなデータを保持させるつもりか推測できないからです。
これは重要な点です。
ベクタはジェネリクスを使用して実装されています。
あなた自身の型でどうジェネリクスを使用するかついては第10章で解説します。
現時点では標準ライブラリで提供されるVec<T>
型は、どんな型でも保持でき、ある特定のベクタがある型を保持するとき、その型は山かっこ内に指定されることを知っておいてください。
リスト8-1では、コンパイラにv
のVec<T>
はi32
型の要素を保持すると指示しました。
いったん値を挿入すると、多くの場合、コンパイラは保持させたい値の型を推論できるようになります。
ですから、より現実的なコードでは、型注釈を付ける必要はあまりないでしょう。
また、初期値を持つVec<T>
を生成する方が一般的ですし、Rustにはvec!
という便利なマクロも用意されています。
このマクロは与えた値を保持する新しいベクタを生成します。
リスト8-2では、1
、2
、3
という値を持つ新しいVec<i32>
を生成しています。
整数型をi32
にしているのは、3章の「データ型」節で学んだように、これが標準の整数型だからです。
fn main() { let v = vec![1, 2, 3]; }
リスト8-2: 値を含む新しいベクタを生成する
初期値のi32
値を与えたので、コンパイラはv
の型がVec<i32>
であると推論でき、型注釈は不要になりました。
次はベクタを変更する方法を見ましょう。
ベクタを更新する
ベクタを生成し、それから要素を追加するには、リスト8-3に示すようにpush
メソッドを使います。
fn main() { let mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8); }
リスト8-3:push
メソッドを使用してベクタに値を追加する
第3章で説明したとおり、どんな変数でも、その値を変更したかったらmut
キーワードで可変にする必要があります。
中に配置する数値は全てi32
型であり、Rustはこのことをデータから推論するので、Vec<i32>
という注釈は不要です。
ベクタをドロップすれば、要素もドロップする
他のあらゆるstruct
(構造体)と同様に、ベクタもスコープを抜ければ解放されます。
その様子をリスト8-4に示します。
fn main() { { let v = vec![1, 2, 3, 4]; // vで作業をする } // <- vはここでスコープを抜け、解放される }
リスト8-4:ベクタとその要素がドロップされる箇所を示す
ベクタがドロップされると、その中身もドロップされます。 つまり、保持されていた整数値が片付けられるということです。 これは一見単純そうですが、ベクタの要素に対する参照を使い始めると少し複雑になり得ます。 次はそれに挑戦しましょう!
ベクタの要素を読む
ベクタを生成し、更新し、破棄する方法がわかったので、次のステップでは中身を読む方法について学ぶのが良いでしょう。 ベクタに保持された値を参照する方法は2つあります。 これから示す例では、理解を助けるために、それらの関数からの戻り値型を注釈しています。
リスト8-5はベクタの値にアクセスする両方の方法として、添え字記法とget
メソッドが示されています。
fn main() { let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; println!("The third element is {}", third); match v.get(2) { // "3つ目の要素は{}です" Some(third) => println!("The third element is {}", third), // "3つ目の要素はありません。" None => println!("There is no third element."), } }
リスト8-5:添え字記法かget
メソッドを使用してベクタの要素にアクセスする
ここでは2つのことに注目してください。
1つ目は、3番目の要素を得るのに2
という添え字の値を使用していることです。
ベクタは番号で索引化されますが、その番号は0から始まります。
2つ目は、3番目の要素を得る2つの方法とは、&
と[]
を使用して参照を得るものと、get
メソッドに引数として添え字を渡してOption<&T>
を得るものだということです。
Rustのベクタには要素を参照する方法が2通りあるので、ベクタに含まれない要素の添え字を使おうとしたときのプログラムの振る舞いを選択できます。 例として、ベクタに5つ要素があるとして、添え字100の要素にアクセスを試みた場合、プログラムがどうなるのか確認しましょう。 リスト8-6に示します。
fn main() { let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100]; let does_not_exist = v.get(100); }
リスト8-6:5つの要素を含むベクタの添え字100の要素にアクセスしようとする
このコードを走らせると、最初の[]
メソッドはプログラムをパニックさせます。
なぜなら存在しない要素を参照しているからです。
このメソッドは、ベクタの終端を超えて要素にアクセスしようとしたときにプログラムをクラッシュさせたい場合に最適です。
get
メソッドにベクタ外の添え字を渡すと、パニックすることなくNone
を返します。
普通の状況でもベクタの範囲外にアクセスする可能性があるなら、このメソッドを使用することになるでしょう。
その場合、第6章で説明したように、コードはSome(&element)
かNone
を扱うロジックを持つことになります。
例えば、誰かが入力した数値が添え字になるかもしれません。
もし誤って大きすぎる値を入力し、プログラムがNone
値を得たなら、いまベクタに何要素あるかをユーザに教え、正しい値を再入力してもらうこともできます。
その方が、ただのタイプミスでプログラムをクラッシュさせるより、ユーザに優しいといえそうです。
プログラムに有効な参照がある場合、借用チェッカー (borrow checker) は、(第4章で解説しましたが)所有権と借用規則を強制し、ベクタの中身へのこの参照や他のいかなる参照も有効であり続けることを保証してくれます。
同一スコープ上では、可変と不変な参照を同時には存在させられないというルールを思い出してください。
このルールはリスト8-7でも適用されています。
リスト8-7ではベクタの最初の要素への不変参照を保持しつつ、終端に要素を追加しようとしています。
関数内のここ以降で、この要素(訳注:first
のこと)を参照しようとすると失敗します。
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);
}
リスト8-7:要素への参照を保持しつつ、ベクタに要素を追加しようとする
このコードをコンパイルすると、こんなエラーになります。
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
(エラー: 不変としても借用されているので、`v`を可変で借用できません)
--> src/main.rs:6:5
|
4 | let first = &v[0];
| - immutable borrow occurs here
| (不変借用はここで発生しています)
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
| (可変借用はここで発生しています)
7 |
8 | println!("The first element is: {}", first);
| ----- immutable borrow later used here
| (その後、不変借用はここで使われています)
error: aborting due to previous error
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections`.
To learn more, run the command again with --verbose.
リスト8-7のコードは、一見動きそうに見えるかもしれません。 なぜ最初の要素への参照が、ベクタの終端への変更を気にかける必要があるのでしょうか? このエラーはベクタが動作するしくみによるものです。 新たな要素をベクタの終端に追加するとき、いまベクタのある場所に全要素を隣り合わせに配置するだけのスペースがないなら、新しいメモリを割り当て、古い要素を新しいスペースにコピーする必要があります。 その場合、最初の要素を指す参照は、解放されたメモリを指すことになるでしょう。 借用規則がそのような状況に陥らないよう防いでくれるのです。
注釈:
Vec<T>
の実装に関する詳細については、“The Rustonomicon”を参照してください (訳注:日本語版はこちらです)。
ベクタ内の値を順に処理する
ベクタの要素に順番にアクセスしたいなら、添え字で1要素ごとにアクセスするのではなく、全要素を走査することができます。
リスト8-8でfor
ループを使い、i32
のベクタの各要素に対する不変な参照を得て、それらを表示する方法を示します。
fn main() { let v = vec![100, 32, 57]; for i in &v { println!("{}", i); } }
リスト8-8:for
ループで要素を走査し、ベクタの各要素を表示する
また、全要素に変更を加えるために、可変なベクタの各要素への可変な参照を走査することもできます。
リスト8-9のfor
ループでは各要素に50
を足しています。
fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } }
リスト8-9:ベクタの要素への可変な参照を走査する
可変参照が参照している値を変更するには、+=
演算子を使用する前に、参照外し演算子(*
)を使用してi
の値にたどり着かないといけません。
参照外し演算子については、第15章の「参照外し演算子で値までポインタを追いかける」節でより詳しく扱います。
Enumを使って複数の型を保持する
この章の冒頭で、ベクタは同じ型の値しか保持できないと述べました。 これは不便なこともあります。 異なる型の要素を保持する必要のあるユースケースは必ず存在します。 幸運なことに、enumの列挙子は同じenumの型の中に定義されるので、ベクタに異なる型の要素を保持する必要が出たら、enumを定義して使用すればよいのです!
例えば、スプレッドシートのある行から値を得ることを考えます。 ここで、その行の中の列には、整数を含むもの、浮動小数点数を含むもの、文字列を含むものがあるとします。 列挙子ごとに異なる値の型を保持するenumが定義できます。 そして、このenumの列挙子は全て同じ型、つまりenumの型、と考えられるわけです。 ですから、そのenumを保持するベクタを作成でき、結果的に異なる型を保持できるようになるわけです。 リスト8-10でこれを実演しています。
fn main() { enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from("blue")), SpreadsheetCell::Float(10.12), ]; }
リスト8-10:enum
を定義して、一つのベクタに異なる型の値を保持する
個々の要素を格納するのにヒープ上で必要となるメモリの量を正確に把握するめに、Rustコンパイラはコンパイル時にベクタに入る型を知る必要があります。
また、このベクタではどんな型が許容されるのか明示できるという副次的な利点があります。
もしRustが、ベクタにどんな型でも保持できることを許していたら、ベクタの要素に対して行われる処理に対して、いくつかの型がエラーを引き起こすかもしれません。
enumに加えてmatch
式を使うことで、第6章で説明したとおり、あらゆるケースが処理できることを、Rustがコンパイル時に保証することになります。
プログラムを書いている時点で、プログラムが実行時に取得し、ベクタに格納し得る全ての型を網羅できない場合には、このenumを使ったテクニックはうまくいかないでしょう。 代わりにトレイトオブジェクトを使用できます。 こちらは第17章で取り上げます。
これまでにベクタの代表的な使い方をいくつか紹介しました。
標準ライブラリでVec<T>
に定義されている多くの有益なメソッドについて、APIドキュメントを必ず確認するようにしてください。
例えば、push
に加えて、pop
というメソッドがあり、これは最後の要素を削除して返します。
それでは次のコレクション型であるString
に移りましょう!
文字列でUTF-8でエンコードされたテキストを保持する
第4章で文字列について語りましたが、今度はより掘り下げていきましょう。新参者のRustaceanは、 3つの概念の組み合わせにより、文字列でよく行き詰まります: Rustのありうるエラーを晒す性質、 多くのプログラマが思っている以上に文字列が複雑なデータ構造であること、そしてUTF-8です。 これらの要因が、他のプログラミング言語から移ってきた場合、一見困難に見えるように絡み合うわけです。
コレクションの文脈で文字列を議論することは、有用なことです。なぜなら、文字列はテキストとして解釈された時に有用になる機能を提供するメソッドと、
バイトのコレクションで実装されているからです。この節では、生成、更新、読み込みのような全コレクションが持つString
の処理について語ります。
また、String
が他のコレクションと異なる点についても議論します。具体的には、人間とコンピュータがString
データを解釈する方法の差異により、
String
に添え字アクセスする方法がどう複雑なのかということです。
文字列とは?
まずは、文字列という用語の意味を定義しましょう。Rustには、言語の核として1種類しか文字列型が存在しません。
文字列スライスのstr
で、通常借用された形態&str
で見かけます。第4章で、文字列スライスについて語りました。
これは、別の場所に格納されたUTF-8エンコードされた文字列データへの参照です。例えば、文字列リテラルは、
プログラムのバイナリ出力に格納されるので、文字列スライスになります。
String
型は、言語の核として組み込まれるのではなく、Rustの標準ライブラリで提供されますが、伸長可能、
可変、所有権のあるUTF-8エンコードされた文字列型です。RustaceanがRustにおいて「文字列」を指したら、
どちらかではなく、String
と文字列スライスの&str
のことを通常意味します。この節は、大方、
String
についてですが、どちらの型もRustの標準ライブラリで重宝されており、
どちらもUTF-8エンコードされています。
また、Rustの標準ライブラリには、他の文字列型も含まれています。OsString
、OsStr
、CString
、CStr
などです。
ライブラリクレートにより、文字列データを格納する選択肢はさらに増えます。
それらの名前が全てString
かStr
で終わっているのがわかりますか?所有権ありと借用されたバージョンを指しているのです。
ちょうど以前見かけたString
と&str
のようですね。例えば、これらの文字列型は、異なるエンコード方法でテキストを格納していたり、
メモリ上の表現が異なったりします。この章では、これらの他の種類の文字列については議論しません;
使用方法やどれが最適かについては、APIドキュメントを参照してください。
新規文字列を生成する
Vec<T>
で使用可能な処理の多くがString
でも使用できます。文字列を生成するnew
関数から始めましょうか。
リスト8-11に示したようにですね。
#![allow(unused)] fn main() { let mut s = String::new(); }
リスト8-11: 新しい空のString
を生成する
この行は、新しい空のs
という文字列を生成しています。それからここにデータを読み込むことができるわけです。
だいたい、文字列の初期値を決めるデータがあるでしょう。そのために、to_string
メソッドを使用します。
このメソッドは、文字列リテラルのように、Display
トレイトを実装する型ならなんでも使用できます。
リスト8-12に2例、示しています。
#![allow(unused)] fn main() { let data = "initial contents"; let s = data.to_string(); // the method also works on a literal directly: let s = "initial contents".to_string(); }
リスト8-12: to_string
メソッドを使用して文字列リテラルからString
を生成する
このコードは、initial contents
(初期値)を含む文字列を生成します。
さらに、String::from
関数を使っても、文字列リテラルからString
を生成することができます。
リスト8-13のコードは、to_string
を使用するリスト8-12のコードと等価です。
#![allow(unused)] fn main() { let s = String::from("initial contents"); }
リスト8-13: String::from
関数を使って文字列リテラルからString
を作る
文字列は、非常に多くのものに使用されるので、多くの異なる一般的なAPIを使用でき、たくさんの選択肢があるわけです。
冗長に思われるものもありますが、適材適所です!今回の場合、String::from
とto_string
は全く同じことをします。
従って、どちらを選ぶかは、スタイル次第です。
文字列はUTF-8エンコードされていることを覚えていますか?要するに文字列には、適切にエンコードされていればどんなものでも含めます。 リスト8-14に示したように。
#![allow(unused)] fn main() { let hello = String::from("السلام عليكم"); let hello = String::from("Dobrý den"); let hello = String::from("Hello"); let hello = String::from("שָׁלוֹם"); let hello = String::from("नमस्ते"); let hello = String::from("こんにちは"); let hello = String::from("안녕하세요"); let hello = String::from("你好"); let hello = String::from("Olá"); let hello = String::from("Здравствуйте"); let hello = String::from("Hola"); }
リスト8-14: いろんな言語の挨拶を文字列に保持する
これらは全て、有効なString
の値です。
文字列を更新する
String
は、サイズを伸ばすことができ、Vec<T>
の中身のように、追加のデータをプッシュすれば、中身も変化します。
付け加えると、String
値を連結する+
演算子や、format!
マクロを便利に使用することができます。
push_str
とpush
で文字列に追加する
push_str
メソッドで文字列スライスを追記することで、String
を伸ばすことができます。
リスト8-15の通りです。
#![allow(unused)] fn main() { let mut s = String::from("foo"); s.push_str("bar"); }
リスト8-15: push_str
メソッドでString
に文字列スライスを追記する
この2行の後、s
はfoobar
を含むことになります。push_str
メソッドは、必ずしも引数の所有権を得なくていいので、
文字列スライスを取ります。例えば、リスト8-16のコードは、中身をs1
に追加した後、
s2
を使えなかったら残念だということを示しています。
#![allow(unused)] fn main() { let mut s1 = String::from("foo"); let s2 = "bar"; s1.push_str(s2); println!("s2 is {}", s2); }
リスト8-16: 中身をString
に追加した後に、文字列スライスを使用する
もし、push_str
メソッドがs2
の所有権を奪っていたら、最後の行でその値を出力することは不可能でしょう。
ところが、このコードは予想通りに動きます!
push
メソッドは、1文字を引数として取り、String
に追加します。リスト8-15は、
push
メソッドでlをString
に追加するコードを呈示しています。
#![allow(unused)] fn main() { let mut s = String::from("lo"); s.push('l'); }
リスト8-17: push
でString
値に1文字を追加する
このコードの結果、s
はlol
を含むことになるでしょう。
編者注:
lol
はlaughing out loud
(大笑いする)の頭文字からできたスラングです。 日本語のwww
みたいなものですね。
+
演算子、またはformat!
マクロで連結
2つのすでにある文字列を組み合わせたくなることがよくあります。リスト8-18に示したように、
一つ目の方法は、+
演算子を使用することです。
#![allow(unused)] fn main() { let s1 = String::from("Hello, "); let s2 = String::from("world!"); let s3 = s1 + &s2; // s1はムーブされ、もう使用できないことに注意 }
リスト8-18: +
演算子を使用して二つのString
値を新しいString
値にする
このコードの結果、s3
という文字列は、Hello, world!
を含むことになるでしょう。
追記の後、s1
がもう有効でなくなった理由と、s2
への参照を使用した理由は、
+
演算子を使用した時に呼ばれるメソッドのシグニチャと関係があります。+
演算子は、add
メソッドを使用し、
そのシグニチャは以下のような感じです:
fn add(self, s: &str) -> String {
これは、標準ライブラリにあるシグニチャそのものではありません: 標準ライブラリでは、add
はジェネリクスで定義されています。
ここでは、ジェネリックな型を具体的な型に置き換えたadd
のシグニチャを見ており、これは、
このメソッドをString
値とともに呼び出した時に起こることです。ジェネリクスについては、第10章で議論します。
このシグニチャが、+
演算子の巧妙な部分を理解するのに必要な手がかりになるのです。
まず、s2
には&
がついてます。つまり、add
関数のs
引数のために最初の文字列に2番目の文字列の参照を追加するということです:
String
には&str
を追加することしかできません。要するに2つのString
値を追加することはできないのです。
でも待ってください。add
の第2引数で指定されているように、&s2
の型は、&str
ではなく、
&String
ではないですか。では、なぜ、リスト8-18は、コンパイルできるのでしょうか?
add
呼び出しで&s2
を使える理由は、コンパイラが&String
引数を&str
に型強制してくれるためです。
add
メソッド呼び出しの際、コンパイラは、参照外し型強制というものを使用し、ここでは、
&s2
を&s2[..]
に変えるものと考えることができます。参照外し型強制について詳しくは、第15章で議論します。
add
がs
引数の所有権を奪わないので、この処理後もs2
が有効なString
になるわけです。
2番目に、シグニチャからadd
はself
の所有権をもらうことがわかります。self
には&
がついていないからです。
これはつまり、リスト8-18においてs1
はadd
呼び出しにムーブされ、その後は有効ではなくなるということです。
故に、s3 = s1 + &s2;
は両文字列をコピーして新しいものを作るように見えますが、
この文は実際にはs1
の所有権を奪い、s2
の中身のコピーを追記し、結果の所有権を返すのです。言い換えると、
たくさんのコピーをしているように見えますが、違います; 実装は、コピーよりも効率的です。
複数の文字列を連結する必要が出ると、+
演算子の振る舞いは扱いにくくなります:
#![allow(unused)] fn main() { let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = s1 + "-" + &s2 + "-" + &s3; }
ここで、s
はtic-tac-toe
になるでしょう。+
と"
文字のせいで何が起きているのかわかりにくいです。
もっと複雑な文字列の連結には、format!
マクロを使用することができます:
#![allow(unused)] fn main() { let s1 = String::from("tic"); let s2 = String::from("tac"); let s3 = String::from("toe"); let s = format!("{}-{}-{}", s1, s2, s3); }
このコードでも、s
はtic-tac-toe
になります。format!
マクロは、println!
と同様の動作をしますが、
出力をスクリーンに行う代わりに、中身をString
で返すのです。format!
を使用したコードの方がはるかに読みやすく、
引数の所有権を奪いません。
文字列に添え字アクセスする
他の多くのプログラミング言語では、文字列中の文字に、添え字で参照してアクセスすることは、有効なコードであり、
一般的な処理です。しかしながら、Rustにおいて、添え字記法でString
の一部にアクセスしようとすると、
エラーが発生するでしょう。リスト8-19の非合法なコードを考えてください。
let s1 = String::from("hello");
let h = s1[0];
リスト8-19: 文字列に対して添え字記法を試みる
このコードは、以下のようなエラーに落ち着きます:
error[E0277]: the trait bound `std::string::String: std::ops::Index<{Integer}>` is not satisfied
(エラー: トレイト境界`std::string::String: std::ops::Index<{Integer}>`が満たされていません)
|>
3 |> let h = s1[0];
|> ^^^^^ the type `std::string::String` cannot be indexed by `{Integer}`
|> (型`std::string::String`は`{Integer}`で添え字アクセスできません)
= help: the trait `std::ops::Index<{Integer}>` is not implemented for `std::string::String`
(ヘルプ: `std::ops::Index<{Integer}>`というトレイトが`std::string::String`に対して実装されていません)
エラーと注釈が全てを物語っています: Rustの文字列は、添え字アクセスをサポートしていないのです。 でも、なぜでしょうか?その疑問に答えるには、Rustがメモリにどのように文字列を保持しているかについて議論する必要があります。
内部表現
String
はVec<u8>
のラッパです。リスト8-14から適切にUTF-8でエンコードされた文字列の例をご覧ください。
まずは、これ:
#![allow(unused)] fn main() { let len = String::from("Hola").len(); }
この場合、len
は4になり、これは、文字列"Hola"を保持するベクタの長さが4バイトであることを意味します。
これらの各文字は、UTF-8でエンコードすると、1バイトになるのです。しかし、以下の行ではどうでしょうか?
(この文字列は大文字のキリル文字Zeで始まり、アラビア数字の3では始まっていないことに注意してください)
#![allow(unused)] fn main() { let len = String::from("Здравствуйте").len(); }
文字列の長さはと問われたら、あなたは12と答えるかもしれません。ところが、Rustの答えは、24です: “Здравствуйте”をUTF-8でエンコードすると、この長さになります。各Unicodeスカラー値は、2バイトの領域を取るからです。 それ故に、文字列のバイトの添え字は、必ずしも有効なUnicodeのスカラー値とは相互に関係しないのです。 デモ用に、こんな非合法なRustコードを考えてください:
let hello = "Здравствуйте";
let answer = &hello[0];
answer
の値は何になるべきでしょうか?最初の文字のЗ
になるべきでしょうか?UTF-8エンコードされた時、
З
の最初のバイトは208
、2番目は151
になるので、answer
は実際、208
になるべきですが、
208
は単独では有効な文字ではありません。この文字列の最初の文字を求めている場合、208
を返すことは、
ユーザの望んでいるものではないでしょう; しかしながら、Rustには、バイト添え字0の位置には、そのデータしかないのです。
文字列がラテン文字のみを含む場合でも、ユーザは一般的にバイト値が返ることを望みません:
&"hello"[0]
がバイト値を返す有効なコードだったら、h
ではなく、104
を返すでしょう。
予期しない値を返し、すぐには判明しないバグを引き起こさないために、Rustはこのコードを全くコンパイルせず、
開発過程の早い段階で誤解を防いでくれるのです。
バイトとスカラー値と書記素クラスタ!なんてこった!
UTF-8について別の要点は、実際Rustの観点から文字列を見るには3つの関連した方法があるということです: バイトとして、スカラー値として、そして、書記素クラスタ(人間が文字と呼ぶものに一番近い)としてです。
ヒンディー語の単語、“नमस्ते”をデーヴァナーガリー(訳注
: サンスクリット語とヒンディー語を書くときに使われる書記法)で表記したものを見たら、
以下のような見た目のu8
値のベクタとして保持されます:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
18バイトになり、このようにしてコンピュータは最終的にこのデータを保持しているわけです。これをUnicodeスカラー値として見たら
(Rustのchar
型はこれなのですが)このバイトは以下のような見た目になります:
['न', 'म', 'स', '्', 'त', 'े']
ここでは、6つchar
値がありますが、4番目と6番目は文字ではありません: 単独では意味をなさないダイアクリティックです。
最後に、書記素クラスタとして見たら、このヒンディー語の単語を作り上げる人間が4文字と呼ぶであろうものが得られます:
["न", "म", "स्", "ते"]
Rustには、データが表す自然言語に関わらず、各プログラムが必要な解釈方法を選択できるように、 コンピュータが保持する生の文字列データを解釈する方法がいろいろ用意されています。
Rustで文字を得るのにString
に添え字アクセスすることが許されない最後の理由は、
添え字アクセスという処理が常に定数時間(O(1))になると期待されるからです。
しかし、String
でそのパフォーマンスを保証することはできません。というのも、
合法な文字がいくつあるか決定するのに、最初から添え字まで中身を走査する必要があるからです。
文字列をスライスする
文字列に添え字アクセスするのは、しばしば悪い考えです。文字列添え字処理の戻り値の型が明瞭ではないからです:
バイト値、文字、書記素クラスタ、あるいは文字列スライスにもなります。故に、文字列スライスを生成するのに、
添え字を使う必要が本当に出た場合にコンパイラは、もっと特定するよう求めてきます。添え字アクセスを特定し、
文字列スライスが欲しいと示唆するためには、[]
で1つの数値により添え字アクセスするのではなく、
範囲とともに[]
を使って、特定のバイトを含む文字列スライスを作ることができます:
#![allow(unused)] fn main() { let hello = "Здравствуйте"; let s = &hello[0..4]; }
ここで、s
は文字列の最初の4バイトを含む&str
になります。先ほど、これらの文字は各々2バイトになると指摘しましたから、
s
はЗд
になります。
&hello[0..1]
と使用したら、何が起きるでしょうか?答え: Rustはベクタの非合法な添え字にアクセスしたかのように、
実行時にパニックするでしょう:
thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/libcore/str/mod.rs:2188:4
('main'スレッドは「バイト添え字1は文字の境界ではありません; `Здравствуйте`の'З'(バイト番号0から2)の中です」でパニックしました)
範囲を使用して文字列スライスを作る際にはプログラムをクラッシュさせることがあるので、気をつけるべきです。
文字列を走査するメソッド群
幸いなことに、他の方法でも文字列の要素にアクセスすることができます。
もし、個々のUnicodeスカラー値に対して処理を行う必要があったら、最適な方法はchars
メソッドを使用するものです。
“नमस्ते”に対してchars
を呼び出したら、分解して6つのchar
型の値を返すので、各要素にアクセスするには、
その結果を走査すればいいわけです:
#![allow(unused)] fn main() { for c in "नमस्ते".chars() { println!("{}", c); } }
このコードは、以下のように出力します:
न
म
स
्
त
े
bytes
メソッドは、各バイトをそのまま返すので、最適になることもあるかもしれません:
#![allow(unused)] fn main() { for b in "नमस्ते".bytes() { println!("{}", b); } }
このコードは、String
をなす18バイトを出力します:
224
164
// --snip--
165
135
ですが、合法なUnicodeスカラー値は、2バイト以上からなる場合もあることは心得ておいてください。
書記素クラスタを文字列から得る方法は複雑なので、この機能は標準ライブラリでは提供されていません。 この機能が必要なら、crates.ioでクレートを入手可能です。
文字列はそう単純じゃない
まとめると、文字列は込み入っています。プログラミング言語ごとにこの複雑性をプログラマに提示する方法は違います。
Rustでは、String
データを正しく扱うことが、全てのRustプログラムにとっての既定動作になっているわけであり、
これは、プログラマがUTF-8データを素直に扱う際に、よりしっかり考えないといけないことを意味します。
このトレードオフにより、他のプログラミング言語で見えるよりも文字列の複雑性がより露出していますが、
ASCII以外の文字に関するエラーを開発の後半で扱わなければならない可能性が排除されているのです。
もう少し複雑でないものに切り替えていきましょう: ハッシュマップです!
キーとそれに紐づいた値をハッシュマップに格納する
一般的なコレクションのトリを飾るのは、ハッシュマップです。型HashMap<K, V>
は、
K
型のキーとV
型の値の対応関係を保持します。これをハッシュ関数を介して行います。
ハッシュ関数は、キーと値のメモリ配置方法を決めるものです。多くのプログラミング言語でもこの種のデータ構造はサポートされていますが、
しばしば名前が違います。hash、map、object、ハッシュテーブル、連想配列など、枚挙に暇はありません。
ハッシュマップは、ベクタのように番号ではなく、どんな型にもなりうるキーを使ってデータを参照したいときに有用です。 例えば、ゲームにおいて、各チームのスコアをハッシュマップで追いかけることができます。ここで、各キーはチーム名、 値が各チームのスコアになります。チーム名が与えられれば、スコアを扱うことができるわけです。
この節でハッシュマップの基礎的なAPIを見ていきますが、より多くのグッズが標準ライブラリにより、
HashMap<K, V>
上に定義された関数に隠されています。いつものように、
もっと情報が欲しければ、標準ライブラリのドキュメントをチェックしてください。
新規ハッシュマップを生成する
空のハッシュマップをnew
で作り、要素をinsert
で追加することができます。リスト8-20では、
名前がブルーとイエローの2チームのスコアを追いかけています。ブルーチームは10点から、イエローチームは50点から始まります。
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); }
リスト8-20: ハッシュマップを生成してキーと値を挿入する
最初に標準ライブラリのコレクション部分からHashMap
をuse
する必要があることに注意してください。
今までの3つの一般的なコレクションの内、これが最も使用頻度が低いので、初期化処理で自動的にスコープに導入される機能には含まれていません。
また、標準ライブラリからのサポートもハッシュマップは少ないです; 例えば、生成するための組み込みマクロがありません。
ベクタと全く同様に、ハッシュマップはデータをヒープに保持します。このHashMap
はキーがString
型、
値はi32
型です。ベクタのように、ハッシュマップは均質です: キーは全て同じ型でなければならず、
値も全て同じ型でなければなりません。
ハッシュマップを生成する別の方法は、タプルのベクタに対してcollect
メソッドを使用するものです。
ここで、各タプルは、キーと値から構成されています。collect
メソッドはいろんなコレクション型にデータをまとめ上げ、
そこにはHashMap
も含まれています。例として、チーム名と初期スコアが別々のベクタに含まれていたら、
zip
メソッドを使ってタプルのベクタを作り上げることができ、そこでは「ブルー」は10とペアになるなどします。
リスト8-21に示したように、それからcollect
メソッドを使って、そのタプルのベクタをハッシュマップに変換することができるわけです。
#![allow(unused)] fn main() { use std::collections::HashMap; let teams = vec![String::from("Blue"), String::from("Yellow")]; let initial_scores = vec![10, 50]; let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect(); }
リスト8-21: チームのリストとスコアのリストからハッシュマップを作る
ここでは、HashMap<_, _>
という型注釈が必要になります。なぜなら、いろんなデータ構造にまとめ上げる
ことができ、
コンパイラは指定しない限り、どれを所望なのかわからないからです。ところが、キーと値の型引数については、
アンダースコアを使用しており、コンパイラはベクタのデータ型に基づいてハッシュマップが含む型を推論することができるのです。
ハッシュマップと所有権
i32
のようなCopy
トレイトを実装する型について、値はハッシュマップにコピーされます。
String
のような所有権のある値なら、値はムーブされ、リスト8-22でデモされているように、
ハッシュマップはそれらの値の所有者になるでしょう。
#![allow(unused)] fn main() { use std::collections::HashMap; let field_name = String::from("Favorite color"); let field_value = String::from("Blue"); let mut map = HashMap::new(); map.insert(field_name, field_value); // field_name and field_value are invalid at this point, try using them and // see what compiler error you get! // field_nameとfield_valueはこの時点で無効になる。試しに使ってみて // どんなコンパイルエラーが出るか確認してみて! }
リスト8-22: 一旦挿入されたら、キーと値はハッシュマップに所有されることを示す
insert
を呼び出してfield_name
とfield_value
がハッシュマップにムーブされた後は、
これらの変数を使用することは叶いません。
値への参照をハッシュマップに挿入したら、値はハッシュマップにムーブされません。参照が指している値は、 最低でもハッシュマップが有効な間は、有効でなければなりません。これらの問題について詳細には、 第10章の「ライフタイムで参照を有効化する」節で語ります。
ハッシュマップの値にアクセスする
リスト8-23に示したように、キーをget
メソッドに提供することで、ハッシュマップから値を取り出すことができます。
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); let team_name = String::from("Blue"); let score = scores.get(&team_name); }
リスト8-23: ハッシュマップに保持されたブルーチームのスコアにアクセスする
ここで、score
はブルーチームに紐づけられた値になり、結果はSome(&10)
となるでしょう。
結果はSome
に包まれます。というのも、get
はOption<&V>
を返すからです; キーに対応する値がハッシュマップになかったら、
get
はNone
を返すでしょう。プログラムは、このOption
を第6章で講義した方法のどれかで扱う必要があるでしょう。
ベクタのように、for
ループでハッシュマップのキーと値のペアを走査することができます:
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Yellow"), 50); for (key, value) in &scores { println!("{}: {}", key, value); } }
このコードは、各ペアを任意の順番で出力します:
Yellow: 50
Blue: 10
ハッシュマップを更新する
キーと値の数は伸長可能なものの、各キーには1回に1つの値しか紐づけることができません。 ハッシュマップ内のデータを変えたい時は、すでにキーに値が紐づいている場合の扱い方を決めなければなりません。 古い値を新しい値で置き換えて、古い値を完全に無視することもできます。古い値を保持して、 新しい値を無視し、キーにまだ値がない場合に新しい値を追加するだけにすることもできます。 あるいは、古い値と新しい値を組み合わせることもできます。各方法について見ていきましょう!
値を上書きする
キーと値をハッシュマップに挿入し、同じキーを異なる値で挿入したら、そのキーに紐づけられている値は置換されます。
リスト8-24のコードは、insert
を二度呼んでいるものの、ハッシュマップには一つのキーと値の組しか含まれません。
なぜなら、ブルーチームキーに対する値を2回とも挿入しているからです。
#![allow(unused)] fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from("Blue"), 10); scores.insert(String::from("Blue"), 25); println!("{:?}", scores); }
リスト8-24: 特定のキーで保持された値を置き換える
このコードは、{"Blue": 25}
と出力するでしょう。10
という元の値は上書きされたのです。
キーに値がなかった時のみ値を挿入する
特定のキーに値があるか確認することは一般的であり、存在しない時に値を挿入することも一般的です。
ハッシュマップには、これを行うentry
と呼ばれる特別なAPIがあり、これは、引数としてチェックしたいキーを取ります。
このentry
メソッドの戻り値は、Entry
と呼ばれるenumであり、これは存在したりしなかったりする可能性のある値を表します。
イエローチームに対するキーに値が紐づけられているか否か確認したくなったとしましょう。存在しなかったら、
50という値を挿入したく、ブルーチームに対しても同様です。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 --v