まえがき

すぐにはわかりにくいかもしれませんが、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][nsprust]と、 [No Starch Press][nsp]のebook形式と同じです。

The Rust Programming Languageへようこそ。Rustに関する入門書です。

Rustプログラミング言語は、高速で信頼できるソフトウェアを書く手助けをしてくれます。 高レベルのエルゴノミクス(訳注: ergonomicsとは、人間工学的という意味。砕いて言えば、人間に優しいということ)と低レベルの制御は、 しばしばプログラミング言語の設計においてトレードオフの関係になります; Rustは、その衝突に挑戦しています。バランスのとれた強力な技術の許容量と素晴らしい開発者経験を通して、 Rustは伝統的にそれらの制御と紐付いていた困難全てなしに低レベルの詳細(メモリ使用など)を制御する選択肢を与えてくれます。

Rustは誰のためのものなの

Rustは、様々な理由により多くの人にとって理想的です。いくつか最も重要なグループを見ていきましょう。

開発者チーム

Rustは、いろんなレベルのシステムプログラミングの知識を持つ開発者の巨大なチームとコラボするのに生産的なツールであると証明してきています。 低レベルコードは様々な種類の微細なバグを抱える傾向があり、そのようなバグは他の言語だと広範なテストと、 経験豊富な開発者による注意深いコードレビューによってのみ捕捉されるものです。Rustにおいては、 コンパイラが並行性のバグも含めたこのようなとらえどころのないバグのあるコードをコンパイルするのを拒むことで、 門番の役割を担います。コンパイラとともに取り組むことで、チームはバグを追いかけるよりもプログラムのロジックに集中することに、 時間を費やせるのです。

Rustはまた、現代的な開発ツールをシステムプログラミング世界に導入します。

  • Cargoは、付属の依存マネージャ兼ビルドツールで、依存を追加、コンパイル、管理することを楽かつ、 Rustエコシステムを通じて矛盾させません。
  • Rustfmtは開発者の間で矛盾のないコーディングスタイルを保証します。
  • Rust Language ServerはIDE(Intefrated Development Environment)にコード補完とインラインのエラーメッセージの統合の源となります。

これらや他のツールを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はマクロを講義します。

この本を読む間違った方法なんてありません: 飛ばしたければ、どうぞご自由に! 混乱したら、前の章に戻らなければならない可能性もあります。ですが、自分に合った方法でどうぞ。

Rustを学ぶ過程で重要な部分は、コンパイラが表示するエラーメッセージを読む方法を学ぶことです: それは動くコードへと導いてくれます。そのため、各場面でコンパイラが表示するエラーメッセージとともに、 コンパイルできないコードの例を多く提供します。適当に例を選んで走らせたら、コンパイルできないかもしれないことを知ってください! 周りのテキストを読んで実行しようとしている例がエラーになることを意図しているのか確認することを確かめてください。 ほとんどの場合、コンパイルできないあらゆるコードの正しいバージョンへと導きます。

ソースコード

この本が生成されるソースファイルは、GitHubで見つかります。

事始め

Rustの旅を始めましょう!学ぶべきことはたくさんありますが、いかなる旅もどこかから始まります。 この章では、以下のことを議論します:

  • RustをLinux、macOS、Windowsにインストールする
  • Hello, world!と出力するプログラムを書く
  • cargoというRustのパッケージマネージャ兼ビルドシステムを使用する

インストール

最初の手順は、Rustをインストールすることです。Rustは、rustupというRustのバージョンと関連するツールを管理するコマンドラインツールを使用して、 ダウンロードします。ダウンロードするには、インターネット接続が必要でしょう。

注釈: なんらかの理由でrustupを使用しないことを好むのなら、Rustインストールページで、 他の選択肢をご覧になってください。

以下の手順で最新の安定版のRustコンパイラをインストールします。この本の例と出力は全て、安定版のRust1.21.0を使用しています。 Rustの安定性保証により、現在この本の例でコンパイルできるものは、新しいバージョンになってもコンパイルでき続けることを保証します。 出力は、バージョンによって多少異なる可能性があります。Rustは頻繁にエラーメッセージと警告を改善しているからです。 言い換えると、どんな新しいバージョンでもこの手順に従ってインストールした安定版なら、 この本の内容で想定通りに動くはずです。

コマンドライン表記

この章及び、本を通して、端末で使用するなんらかのコマンドを示すことがあります。読者が入力するべき行は、 全て$で始まります。$文字を入れる必要はありません; 各コマンドの開始を示しているだけです。 $で始まらない行は、典型的には直前のコマンドの出力を示します。また、PowerShell限定の例は、 $ではなく、>を使用します。

LinuxとmacOSにrustupをインストールする

LinuxかmacOSを使用しているなら、端末を開き、以下のコマンドを入力してください:

$ curl https://sh.rustup.rs -sSf | sh

このコマンドはスクリプトをダウンロードし、rustupツールのインストールを開始し、Rustの最新の安定版をインストールします。 パスワードを求められる可能性があります。インストールがうまく行けば、以下の行が出現するでしょう:

Rust is installed now. Great!

お好みでご自由にスクリプトをダウンロードし、実行前に調査することもできます。

インストールスクリプトは、次回のログイン後にRustをシステムのPATHに自動的に追加します。端末を再起動するのではなく、 いますぐにRustを使用し始めたいのなら、シェルで以下のコマンドを実行してRustをシステムのPATHに手動で追加します:

$ source $HOME/.cargo/env

また、以下の行を ~/.bash_profileに追加することもできます:

$ export PATH="$HOME/.cargo/bin:$PATH"

さらに、なんらかの類のリンカが必要になるでしょう。既にインストールされている可能性が高いものの、 Rustプログラムのコンパイルを試みて、リンカが実行できないというエラーが出たら、 システムにリンカがインストールされていないということなので、手動でインストールする必要があるでしょう。 Cコンパイラは通常正しいリンカとセットになっています。 自分のプラットフォームのドキュメンテーションを見てCコンパイラのインストール方法を確認してください。 一般的なRustパッケージの中には、Cコードに依存し、Cコンパイラが必要になるものもあります。 故に今インストールする価値はあるかもしれません。

Windowsでrustupをインストールする

Windowsでは、https://www.rust-lang.org/install.htmlに行き、手順に従ってRustをインストールしてください。 インストールの途中で、Visual Studio2013以降用のC++ビルドツールも必要になるという旨のメッセージが出るでしょう。 ビルドツールを取得する最も簡単な方法は、Visual Studio 2017用のビルドツールをインストールすることです。 ツールは、他のツール及びフレームワークのセクションにあります。

これ以降、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が動かないなら、 助力を得られる場所はたくさんあります。最も簡単なのがirc.mozilla.orgの#rust IRCチャンネルで、 Mibbitを通してアクセスできます。そのアドレスで、助けてくれる他のRustacean(自分たちを呼ぶバカなニックネーム)とチャットできます。 他の素晴らしいリソースには、ユーザ・フォーラムStack Overflowが含まれます。

Rustacean: いらないかもしれない補足です。Rustaceanは公式にcrustaceans(甲殻類)から来ているそうです。 そのため、Rustのマスコットは非公式らしいですが、カニ。上の会話でCの欠点を削ぎ落としているからcを省いてるの?みたいなことを聞いてますが、 違うそうです。検索したら、堅牢性が高いから甲殻類という意見もありますが、真偽は不明です。 明日使えるかもしれないトリビアでした。

ローカルのドキュメンテーション

インストーラは、ドキュメンテーションの複製もローカルに含んでいるので、オフラインで閲覧することができます。 ブラウザでローカルのドキュメンテーションを開くには、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つあけて、開き波括弧を関数宣言と同じ行に配置するのがいいスタイルです。

これを執筆している時点では、rustfmtと呼ばれる自動整形ツールは開発中です。複数のRustプロジェクトに渡って、 標準的なスタイルに固執したいなら、rustfmtは特定のスタイルにコードを整形してくれます。Rustチームは、 最終的にrustcのように標準的なRustの配布にこのツールを含むことを計画しています。従って、この本を読んだ時期によっては、 既にコンピュータにインストールされている可能性もあります!詳細は、オンラインのドキュメンテーションを確認してください。

main関数内には、こんなコードがあります:


# #![allow(unused_variables)]
#fn main() {
    println!("Hello, world!");
#}

この行が、この小さなプログラムの全作業をしています: テキストを画面に出力するのです。 ここで気付くべき重要な詳細が4つあります。まず、Rustのスタイルは、タブではなく、4スペースでインデントするということです。

2番目にprintln!はRustのマクロを呼び出すということです。代わりに関数を呼んでいたら、 println(!なし)と入力されているでしょう。Rustのマクロについて詳しくは、付録Dで議論します。 とりあえず、!を使用すると、普通の関数ではなくマクロを呼んでいるのだということを知っておくだけでいいでしょう。

3番目に、"Hello, world!"文字列が見えます。この文字列を引数としてprintln!に渡し、 この文字列が画面に表示されているのです。

4番目にこの行をセミコロン(;)で終え、この式が終わり、次の式の準備ができていると示唆していることです。 Rustコードのほとんどの行は、セミコロンで終わります。

コンパイルと実行は個別のステップ

新しく作成したプログラムをちょうど実行したので、その途中の手順を調査しましょう。

Rustプログラムを実行する前に、以下のように、rustcコマンドを入力し、ソースファイルの名前を渡すことで、 Rustコンパイラを使用してコンパイルしなければなりません。

$ rustc main.rs

あなたにCやC++の背景があるなら、これはgccclangと似ていると気付くでしょう。コンパイルに成功後、 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拡張子のデバッグ情報を含むファイルを表示します。ここから、 mainmain.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は、コードのビルドやコードが依存しているライブラリのダウンロード、 それらのライブラリのビルド(コードが必要とするライブラリを我々は、依存と呼んでいます)などの多くの仕事を扱ってくれるからです。

今までに書いたような最も単純なRustプログラムは、依存がありません。従って、Hello, world!プロジェクトをCargoを使ってビルドしても、 Cargoのコードをビルドする部分しか使用しないでしょう。より複雑なRustプログラムを書くにつれて、 依存を追加し、Cargoでプロジェクトを開始したら、依存の追加は、遥かに簡単になるのです。

Rustプロジェクトの大多数がCargoを使用しているので、これ以降この本では、あなたもCargoを使用していることを想定します。 Cargoは、「インストール」節で議論した公式のインストーラを使用していれば、勝手にインストールされます。 Rustを他の何らかの手段でインストールした場合、以下のコマンドを端末に入れてCargoがインストールされているか確かめてください:

$ cargo --version

バージョンナンバーが見えたら、インストールされています!command not foundなどのエラーが見えたら、 自分のインストール方法を求めてドキュメンテーションを見、Cargoを個別にインストールする方法を決定してください。

Cargoでプロジェクトを作成する

Cargoを使用して新しいプロジェクトを作成し、元のHello, world!プロジェクトとどう違うかを見ましょう。 projectsディレクトリ(あるいはコードを格納すると決めた場所)に戻ってください。それから、 OSに関わらず、以下を実行してください:

$ cargo new hello_cargo --bin
$ cd hello_cargo

最初のコマンドは、hello_cargoという新しいバイナリの実行可能ファイルを作成します。cargo newに渡した--bin引数が、 ライブラリとは対照的に実行可能なアプリケーション(よく単にバイナリと呼ばれる)を作成します。プロジェクトをhello_cargoと名付け、 Cargoは、そのファイルを同名のディレクトリに作成します。

hello_cargoディレクトリに行き、ファイルを列挙してください。Cargoが2つのファイルと1つのディレクトリを生成してくれたことがわかるでしょう: Cargo.tomlファイルと、中にmain.rsファイルがあるsrcディレクトリです。また、 .gitignoreファイルと共に、新しいGitリポジトリも初期化しています。

注釈: Gitは一般的なバージョンコントロールシステムです。cargo newを変更して、異なるバージョンコントロールシステムを使用したり、 --vcsフラグを使用して何もバージョンコントロールシステムを使用しないようにもできます。 cargo new --helpを走らせて、利用可能なオプションを確認してください。

お好きなテキストエディタでCargo.tomlを開いてください。リスト1-2のコードのような見た目のはずです。

ファイル名: Cargo.toml

[package]
name = "hello_cargo"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]

[dependencies]

リスト1-2: cargo newで生成されるCargo.tomlの中身

このファイルはTOML(Tom's Obvious, Minimal Language; 直訳: トムの明確な最小限の言語)フォーマットで、 Cargoの設定フォーマットです。

最初の行の[package]は、後の文がパッケージを設定していることを示すセクションヘッダーです。もっと情報を追加するにつれて、 別のセクションも追加するでしょう。

その後の3行が、Cargoがプログラムをコンパイルするのに必要な設定情報をセットします: 名前、バージョン、誰が書いたかです。 Cargoは名前とEメールの情報を環境から取得するので、その情報が正しくなければ、 今修正してそれから保存してください。

最後の行の[dependencies]は、プロジェクトの依存を列挙するためのセクションの始まりです。 Rustでは、パッケージのコードはクレートとして参照されます。このプロジェクトでは何も他のクレートは必要ありませんが、 第2章の最初のプロジェクトでは必要なので、その時にはこの依存セクションを使用するでしょう。

では、src/main.rsを開いて覗いてみてください:

ファイル名: src/main.rs

fn main() {
    println!("Hello, world!");
}

ちょうどリスト1-1で書いたように、CargoはHello, world!プログラムを生成してくれています。ここまでで、 前のプロジェクトとCargoが生成したプロジェクトの違いは、Cargoがsrcディレクトリにコードを配置し、 最上位のディレクトリにCargo.toml設定ファイルがあることです。

Cargoは、ソースファイルがsrcディレクトリにあることを期待します。プロジェクトの最上位のディレクトリは、 READMEファイル、ライセンス情報、設定ファイル、あるいは、他のコードに関連しないもののためのものです。 Cargoを使用すると、プロジェクトを体系化する手助けをしてくれます。適材適所であり、 全てがその場所にあるのです。

Hello, world!プロジェクトのように、Cargoを使用しないプロジェクトを開始したら、 実際にCargoを使用するプロジェクトに変換することができます。プロジェクトのコードをsrcディレクトリに移動し、 適切なCargo.tomlファイルを作成してください。

Cargoプロジェクトをビルドし、実行する

さて、CargoでHello, world!プログラムをビルドし、実行する時の違いに目を向けましょう!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を使用して、コードをコンパイルし、それから吐かれた実行可能ファイルを全部1コマンドで実行することもできます:

$ 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
   Compiling 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 buildcargo checkでプロジェクトをビルドできる。
  • プロジェクトのビルドと実行を1ステップ、cargo runでできる。
  • ビルドの結果をコードと同じディレクトリに保存するのではなく、Cargoはtarget/debugディレクトリに格納する。

Cargoを使用する追加の利点は、使用しているOSに関わらず、同じコマンドが使用できることです。 故にこの時点で、WindowsとLinux及びmacOSで特定の手順を提供することは最早なくなります。

リリースビルドを行う

プロジェクトを最終的にリリースする準備ができたら、cargo build --releaseを使用して、 最適化を行なってコンパイルすることができます。このコマンドは、target/debugではなく、 target/releaseに実行可能ファイルを作成します。最適化は、Rustコードの実行を速くしてくれますが、 オンにするとプログラムをコンパイルする時間が延びます。このため、2つの異なるプロファイルがあるのです: 頻繁に再ビルドをかけたい開発用と、繰り返し再ビルドすることはなく、できるだけ高速に動いてユーザにあげる最終的なプログラムをビルドする用です。 コードの実行時間をベンチマークするなら、cargo build --releaseを確実に実行し、target/releaseの実行可能ファイルでベンチマークしてください。

習慣としてのCargo

単純なプロジェクトでは、Cargoは単にrustcを使用する以上の価値を生みませんが、プログラムが複雑になるにつれて、 その価値を証明するでしょう。複数のクレートからなる複雑なプロジェクトでは、Cargoにビルドを調整してもらうのが遥かに簡単です。

hello_cargoプロジェクトは単純ではありますが、今では、Rustのキャリアを通じて使用するであろう本物のツールを多く使用するようになりました。 事実、既存のどんなプロジェクトに取り組むにも、以下のコマンドを使用して、Gitでコードをチェックアウトし、 そのプロジェクトのディレクトリに移動し、ビルドできます:

$ git clone someurl.com/someproject
$ cd someproject
$ cargo build

Cargoについてより詳しく知るには、ドキュメンテーションを確認してください。

まとめ

既にRustの旅の素晴らしいスタートを切っています!この章では、以下の方法を学びました:

  • rustupで最新の安定版のRustをインストールする方法
  • 新しいRustのバージョンに更新する方法
  • ローカルにインストールされたドキュメンテーションを開く方法
  • 直接rustcを使用してHello, world!プログラムを書き、実行する方法
  • Cargoの慣習を使用して新しいプロジェクトを作成し、実行する方法

より中身のあるプログラムをビルドし、Rustコードの読み書きに慣れるいいタイミングです。故に、第2章では、 数当てゲームを構築します。むしろ一般的なプログラミングの概念がRustでどう動くのか学ぶことから始めたいのであれば、 第3章を見て、それから第2章に戻ってください。

数当てゲームをプログラムする

実物のプロジェクトに一緒に取り組むことで、Rustの世界へ飛び込みましょう! この章では、実際のプログラム内で使用しながらいくつかの一般的なRustの概念に触れます。 let文、match式、メソッド、関連関数、外部クレートの使用などについて学ぶでしょう! 後ほどの章でこれらの概念について深く知ることになります。この章では、基礎部分だけにしましょう。

古典的な初心者向けのプログラミング問題を実装してみましょう: 数当てゲームです。 これは以下のように動作します: プログラムは1から100までの乱数整数を生成します。 そしてプレーヤーに予想を入力するよう促します。予想を入力したら、プログラムは、 その予想が少なすぎたか多すぎたかを出力します。予想が当たっていれば、ゲームは祝福メッセージを表示し、 終了します。

新規プロジェクトの立ち上げ

新規プロジェクトを立ち上げるには、第1章で作成したprojectsディレクトリに行き、 Cargoを使って以下のように新規プロジェクトを作成します。

$ cargo new guessing_game --bin
$ cd guessing_game

最初のコマンドcargo newは、プロジェクト名を第1引数に取ります(guessing_gameですね)。 --binというフラグは、Cargoにバイナリ生成プロジェクトを作成させます。第1章のものと似ていますね。 2番目のコマンドで新規プロジェクトのディレクトリに移動します。

生成されたCargo.tomlファイルを見てください:

ファイル名: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
authors = ["名前 <you@example.com>"]

[dependencies]

もし、Cargoがあなたの環境から取得した作者情報が間違っていたら、 ファイルを編集して保存し直してください。

第1章でも見かけたように、cargo newコマンドは、"Hello, world!"プログラムを生成してくれます。 src/main.rsファイルをチェックしてみましょう:

ファイル名: src/main.rs

fn main() {
    println!("Hello, world!");
}

さて、この"Hello, world!"プログラムをコンパイルし、cargo runコマンドを使用して、 以前と同じように動かしてみましょう:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
     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: ユーザに予想を入力してもらい、それを出力するコード

注釈: The programming language Rust第1版の翻訳者によると、 ソースコードのコメント中以外に日本語文字があるとコンパイルに失敗することがあるそうなので、文字列の英語は、コメントに和訳を載せます。 また、重複する内容の場合には、最初の1回だけ掲載するようにします。

このコードには、たくさんの情報が詰め込まれてますね。なので、行ごとに見ていきましょう。 ユーザ入力を受け付け、結果を出力するためには、io(入/出力)ライブラリをスコープに導入する必要があります。 ioライブラリは、標準ライブラリ(stdとして知られています)に存在します:

use std::io;

デフォルトでは、preludeに存在するいくつかの型のみ使えます。 もし、使用したい型がpreludeにない場合は、use文で明示的にその型をスコープに導入する必要があります。 std::ioライブラリを使用することで、ユーザ入力を受け付ける能力などの実用的な機能の多くを使用することができます。

第1章で見た通り、main関数がプログラムへのエントリーポイント(スタート地点)になります:

fn main() {

fn構文が関数を新しく宣言し、かっこの()は引数がないことを示し、波括弧の{が関数本体のスタート地点になります。

また、第1章で学んだように、println!は、文字列を画面に表示するマクロになります:

println!("Guess the number!");

println!("Please input your guess.");

このコードは、このゲームが何かを出力し、ユーザに入力を求めています。

値を変数に保持する

次に、ユーザ入力を保持する場所を作りましょう。こんな感じに:

let mut guess = String::new();

さあ、プログラムが面白くなってきましたね。このたった1行でいろんなことが起きています。 これがlet文であることに注目してください。これを使用して変数を生成しています。 こちらは、別の例です:

let foo = bar;

この行では、fooという名前の新しい変数を作成し、barの値に束縛しています。 Rustでは、変数は標準で不変(immutable)です。この概念について詳しくは、 第3章の「変数と可変性」節で議論します。以下の例には、 変数名の前にmut修飾子をつけて変数を可変にする方法が示されています:

let foo = 5; // immutable
let mut bar = 5; // mutable

注釈: //という記法は、行末まで続くコメントを記述します。 コンパイラは、コメントを一切無視し、これについても第3章で詳しく議論します。

さあ、let mut guessguessという名前の可変変数を導入するとわかりましたね。 イコール記号(=)の逆側には、変数guessが束縛される値があります。この値は、 String::new関数の呼び出し結果であり、この関数は、String型のオブジェクトを返します。 String型は、標準ライブラリによって提供される文字列型で、 サイズ可変、UTF-8エンコードされたテキスト破片になります。

::new行にある::という記法は、newString型の関連関数であることを表しています。 関連関数とは、String型の特定のオブジェクトよりも型(この場合はString)に対して 実装された関数のことであり、静的メソッドと呼ばれる言語もあります。

このnew関数は、新しく空の文字列を生成します。new関数は、いろんな型に見られます。 なぜなら、何らかの新規値を生成する関数にとってありふれた名前だからです。

まとめると、let mut guess = String::new();という行は、現在、新たに空のStringオブジェクトに束縛されている 可変変数を作っているわけです。ふう!

プログラムの1行目で、use std::ioとして、標準ライブラリから入/出力機能を取り込んだことを思い出してください。 今度は、io型のstdin関連関数を呼び出しましょう:

io::stdin().read_line(&mut guess)
    .expect("Failed to read line");

仮に、プログラムの冒頭でuse std::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型で失敗の可能性を扱う

まだ、この行は終わりではありませんよ。ここまでに議論したのはテキストでは1行ですが、コードとしての論理行としては、 まだ所詮最初の部分でしかないのです。2番目の部分はこのメソッドです:

.expect("Failed to read line");

.foo()という記法で、メソッドを呼び出す時、改行と空白で長い行を分割するのがしばしば賢明です。 今回の場合、こう書くこともできますよね:

io::stdin().read_line(&mut guess).expect("Failed to read line");

しかし、長い行は読みづらいものです。なので、分割しましょう: 2回のメソッド呼び出しに、2行です。 さて、この行が何をしているのかについて議論しましょうか。

以前にも述べたように、read_lineメソッドは、渡された文字列にユーザが入力したものを入れ込むだけでなく、 値も返します(今回はio::Resultです)。 RustにはResultと名のついた型が 標準ライブラリにたくさんあります: 汎用のResultの他、 io::Resultなどのサブモジュール用に特化したものまで。

このResult型は、列挙型であり、普通、enum(イーナム)と呼ばれます。 列挙型とは、固定された種類の値を持つ型のことであり、それらの値は、enumのバリアント(variant)と呼ばれます。 enumについては、第6章で詳しく解説します。

Result型に関しては、取りうる型の値(バリアント)はOkErrです。Ok列挙子は、処理が成功したことを表し、 中に生成された値を保持します。Err列挙子は、処理が失敗したことを意味し、Errは、処理が失敗した過程や、 理由などの情報を保有します。

これらResult型の目的は、エラー処理の情報をコード化することです。Result型の値も、他の型同様、 メソッドが定義されています。io::Resultオブジェクトには、呼び出し可能なexpectメソッドがあります。 このio::ResultオブジェクトがErr値の場合、expectメソッドはプログラムをクラッシュさせ、 引数として渡されたメッセージを表示します。read_lineメソッドがErrを返したら、 恐らく根底にあるOSによるエラーに起因するのでしょう。 このio::ResultオブジェクトがOk値の場合、expectメソッドは、Okバリアントが保持する 返り値を取り出して、ただその値を返すので、これを使用することができるでしょう。 今回の場合、その返り値とは、ユーザが標準入力に入力したデータのバイト数になります。

もし、expectメソッドを呼び出さなかったら、コンパイルは通るものの、警告が出るでしょう:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `std::result::Result` which must be used
(警告: 使用されなければならない結果が使用されていません)
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: #[warn(unused_must_use)] on by default

コンパイラは、私たちがread_lineメソッドから返ってきたResult値を使用していないと警告してきており、 これは、プログラムがエラーの可能性に対処していないことを示します。

警告を抑制する正しい手段は、実際にエラー対処コードを書くことですが、今は、 問題が起きた時にプロラグムをクラッシュさせたいので、expectを使用できるわけです。 エラーから復旧する方法については、第9章で学ぶでしょう。

println!マクロのプレースホルダーで値を出力する

閉じ波かっこを除けば、ここまでに追加されたコードのうち議論すべきものは、残り1行であり、それは以下の通りです:

println!("You guessed: {}", guess);

この行は、ユーザ入力を保存した文字列の中身を出力します。1組の波括弧の{}は、プレースホルダーの役目を果たします: {}は値を所定の場所に保持する小さなカニのはさみと考えてください。波括弧を使って一つ以上の値を出力できます: 最初の波括弧の組は、フォーマット文字列の後に列挙された最初の値に対応し、 2組目は、2つ目の値、とそんな感じで続いていきます。1回のprintln!の呼び出しで複数値を出力するコードは、 以下のような感じになります:


# #![allow(unused_variables)]
#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 2.53 secs
     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]セクションヘッダーの一番下に追記しましょう:

ファイル名: Cargo.toml

[dependencies]

rand = "0.3.14"

Cargo.tomlファイルにおいて、ヘッダーに続くものは全て、他のセクションが始まるまで続くセクションの一部になります。 [dependecies]セクションは、プロジェクトが依存する外部クレートと必要とするバージョンを記述するところです。 今は、randクレートで、セマンティックバージョンには0.3.14を指定します。Cargoはバージョンナンバー記述の 標準規格であるセマンティックバージョニング (時にSemVerと呼ばれる)を理解します。 0.3.14という数字は、実際には^0.3.14の省略記法で、これは、「バージョン0.3.14と互換性のある公開APIを持つ 任意のバージョン」を意味します。

さて、コードは一切変えずに、リスト2-2のようにプロジェクトをビルドしましょう。

$ cargo build
    Updating registry `https://github.com/rust-lang/crates.io-index` (レジストリを更新しています)
 Downloading rand v0.3.14                                            (rand v0.3.14をダウンロードしています)
 Downloading libc v0.2.14                                            (libc v0.2.14をダウンロードしています)
   Compiling libc v0.2.14                                            (libc v0.2.14をコンパイルしています)
   Compiling rand v0.3.14                                            (rand v0.3.14をコンパイルしています)
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)   (guessing_game v0.1.0をコンパイルしています)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs    

リスト2-2: randクレートを依存として追加した後のcargo buildコマンドの出力

もしかしたら、バージョンナンバーは違うかもしれません(でも、互換性はあります、SemVerのおかげでね!)。 そして、行の出力順序も違うかもしれません。

今や、外部依存を持つようになったので、Cargoはレジストリ(registry、登録所)から最新バージョンを拾ってきます。 レジストリとは、Crates.ioのデータのコピーです. Crates.ioとは、Rustのエコシステムにいる人間が、 他の人も使えるように自分のオープンソースのRustプロジェクトを投稿する場所です。

レジストリの更新後、Cargoは[dependencies]セクションをチェックし、まだ取得していないクレートを全部ダウンロードします。 今回の場合、randしか依存ファイルには列挙していませんが、Cargoはlibcのコピーも拾ってきます。 randクレートがlibcに依存しているからですね。クレートのダウンロード完了後、コンパイラは依存ファイル、 そして、依存が利用可能な状態でプロジェクトをコンパイルします。

何も変更せず即座に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.3.15が登場し、重要なバグ修正がなされているけれども、 自分のコードを破壊してしまう互換性破壊があった場合はどうなるでしょう?

この問題に対する回答は、Cargo.lockファイルであり、このファイルは、初めてcargo buildコマンドを 走らせた時に生成され、guessing_gameディレクトリに存在しています。プロジェクトを初めてビルドする際に、 Cargoは判断基準(criteria)に合致するよう全ての依存のバージョンを計算し、Cargo.lockファイルに記述します。 次にプロジェクトをビルドする際には、CargoはCargo.lockファイルが存在することを確かめ、 再度バージョンの計算の作業を行うのではなく、そこに指定されているバージョンを使用します。 このことにより、自動的に再現可能なビルドを構成できるのです。つまり、明示的にアップグレードしない限り、 プロジェクトが使用するバージョンは0.3.14に保たれるのです。Cargo.lockファイルのおかげでね。

Cargo.lockファイルで再現可能なビルドを保証する

クレートを本当にアップグレードする必要が出てきたら、Cargoの別のコマンド(update)を使用しましょう。 これは、Cargo.lockファイルを無視してCargo.tomlファイル内の全ての指定に合致する最新バージョンを計算します。 それがうまくいったら、CargoはそれらのバージョンをCargo.lockファイルに記述します。

しかし標準でCargoは、0.3.0以上、0.4.0未満のバージョンのみを検索します。randクレートの新バージョンが 2つリリースされていたら(0.3.150.4.0だとします)、cargo updateコマンドを走らせた時に以下のような メッセージを目の当たりにするでしょう:

$ cargo update
    Updating registry `https://github.com/rust-lang/crates.io-index`
    (レジストリ`https://github.com/rust-lang/crates-io-index`を更新しています)
    Updating rand v0.3.14 -> v0.3.15
    (randクレートをv0.3.14 -> v0.3.15に更新しています)

この時点で、Cargo.lockファイルに書かれている現在使用しているrandクレートのバージョンが、 0.3.15になっていることにも気付くでしょう。

randのバージョン0.4.0または、0.4.xシリーズのどれかを使用したかったら、 代わりにCargo.tomlファイルを以下のように更新しなければならないでしょう:

[dependencies]

rand = "0.4.0"

次回、cargo buildコマンドを走らせたら、Cargoは利用可能なクレートのレジストリを更新し、 randクレートの必要条件を指定した新しいバージョンに再評価します。

まだ第14章で議論するCargoそのエコシステム については述べたいことが山ほどありますが、とりあえずは、これで知っておくべきことは全てです。 Cargoのおかげでライブラリはとても簡単に再利用ができるので、Rustacean(Rustユーザのこと)は数多くのパッケージから 構成された小規模のプロジェクトを書くことができるのです。

乱数を生成する

Cargo.tomlrandクレートを追加したので、randクレートを使用開始しましょう。 次のステップは、リスト2-3のようにsrc/main.rsファイルを更新することです。

ファイル名: src/main.rs

extern crate rand;

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: 乱数を生成するコードの追加

まず、コンパイラにrandクレートを外部依存として使用することを知らせる行を追加しています。 これにより、use randを呼ぶのと同じ効果が得られるので、randクレートのものをrand:: という接頭辞をつけて呼び出せるようになりました。

次に、別のuse行を追加しています: use rand::Rngですね。Rngトレイトは乱数生成器が実装するメソッドを定義していて、 このトレイトがスコープにないと、メソッドを使用できないのです。トレイトについて詳しくは、 第10章で解説します。

また、途中に2行追加もしています。rand::thread_rng関数は、これから使う特定の乱数生成器を 返してくれます: この乱数生成器は、実行スレッドに固有で、OSにより、シード値を与えられています。 次に、この乱数生成器のgen_rangeメソッドを呼び出しています。このメソッドは、use rand::Rng文で スコープに導入したRngトレイトで定義されています。gen_rangeメソッドは二つの数字を引数に取り、 それらの間の乱数を生成してくれます。範囲は下限値を含み、上限値を含まないため、1101と指定しないと 1から100の範囲の数字は得られません。

単純に使用すべきトレイトとクレートからどの関数とメソッドを呼び出すか知っているわけではないでしょう。 クレートの使用方法は、各クレートのドキュメントにあります。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.53 secs
     Running `target/debug/guessing_game`
Guess the number!                         (何回も出ているので、ここでは和訳は省略します)
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
     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

extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {

    // ---snip---

    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: 2値比較の可能性のある返り値を処理する

最初の新しい点は、別のuse文です。これで、std::cmp::Orderingという型を標準ライブラリから スコープに導入しています。Resultと同じくOrderingもenumです。ただ、Orderingの列挙子は、 LessGreaterそして、Equalです。これらは、2値比較した時に発生しうる3種類の結果です。

match guess.cmp(&secret_number) {
    Ordering::Less => println!("Too small!"),
    Ordering::Greater => println!("Too big!"),
    Ordering::Equal => println!("You win!"),
}

それから、一番下に新しく5行追加してOrdering型を使用しています。cmpメソッドは、 2値を比較し、比較できるものに対してならなんに対しても呼び出せます。このメソッドは、 比較したいものへの参照を取ります: ここでは、guess変数とsecret_number変数を比較しています。 それからこのメソッドはuse文でスコープに導入したOrdering列挙型の値を返します。 match式を使用して、guess変数とsecret_numbercmpに渡して返ってきたOrderingの列挙子に基づき、 次の動作を決定しています。

match式は、複数のアーム(腕)からできています。一つのアームは、パターンとそのパターンに match式の冒頭で与えた値がマッチした時に走るコードから構成されています。Rustは、matchに与えられた 値を取り、各アームのパターンを順番に照合していきます。match式とパターンは、コードを書く際に ()(くわ)す様々なシチュエーションを表現させてくれ、 すべてのシチュエーションに対処していることを保証するのを手助けしてくれるRustの強力な機能です。 これらの機能は、それぞれ、第6章と第18章で詳しく解説することにします。

ここで使われているmatch式でどんなことが起こるかの例をじっくり観察してみましょう!例えば、 ユーザは50と予想し、ランダム生成された秘密の数字は今回、38だったとしましょう。コードが50と38を比較すると、 cmpメソッドはOrdering::Greaterを返します。50は38よりも大きいからですね。match式に、 Ordering::Greaterが与えられ、各アームのパターンを吟味し始めます。まず、 最初のアームのパターンと照合します(Ordering::Lessですね)。しかし、 値のOrdering::GreaterOrdering::Lessはマッチしないため、このアームのコードは無視され、 次のアームに移ります。次のアームのパターン、Ordering::Greater見事にOrdering::Greaterとマッチします! このアームに紐づけられたコードが実行され、画面にToo big!が表示されます。 これでmatch式の実行は終わりになります。この筋書きでは、最後のアームと照合する必要はもうないからですね。

ところが、リスト2-4のコードは、まだコンパイルが通りません。試してみましょう:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types          (型が合いません)
  --> src/main.rs:23:21
   |
23 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `std::string::String`, found integral variable
   |                                    (構造体`std::string::String`を予期したけど、整数型変数が見つかりました)
   |
   = note: expected type `&std::string::String`
   = note:    found type `&{integer}`

error: aborting due to previous error   (先のエラーのため、処理を中断します)
Could not compile `guessing_game`.      (`guessing_game`をコンパイルできませんでした)

このエラーの核は、型の不一致があると言っています。Rustは、強い静的型システムを持っています。 しかし、型推論にも対応しています。let guess = String::new()と書いた時、コンパイラは、 guessString型であるはずと推論してくれ、その型を明示させられることはありませんでした。 一方で、secret_number変数は、数値型です。1から100を表すことができる数値型はいくつかあります: i32は32ビットの数字; u32は32ビットの非負数字; i64は64ビットの数字;などです。 Rustでの標準は、i32型であり、型情報をどこかに追加して、コンパイラに異なる数値型だと推論させない限り、 secret_numberの型はこれになります。エラーの原因は、Rustでは、文字列と数値型を比較できないことです。

究極的には、プログラムが入力として読み込むString型を現実の数値型に変換し、 予想と数値として比較できるようにしたいわけです。これは、以下の2行をmain関数の本体に追記することでできます:

ファイル名: src/main.rs

// --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!"),
    }
}

その2行とは:

let guess: u32 = guess.trim().parse()
    .expect("Please type a number!");

guessという名前の変数を生成しています。あれ、でも待って。もうプログラムにはguessという名前の変数が ありませんでしたっけ?確かにありますが、Rustでは、新しい値でguessの値を覆い隠す(shadow)ことが 許されているのです。この機能は、今回のような、値を別の型に変換したいシチュエーションでよく使われます。 シャドーイング(shadowing)のおかげで別々の変数を2つ作らされることなく、guessという変数名を再利用することができるのです。 guess_strguessみたいなね(シャドーイングについては、第3章でもっと掘り下げます)。

guessguess.trim().parse()という式に束縛しています。この式中のguessは、 入力が入ったString型の元々のguessを指しています。Stringオブジェクトのtrimメソッドは、 両端の空白をすべて除去します。u32型は、数字しか含むことができませんが、ユーザは、 read_lineの処理を終えるためにエンターを押さなければなりません。 ユーザがエンターを押したら、改行文字が文字列に追加されます。 具体例として、ユーザが5を入力して、 エンターを押せば、guessは次のようになります: 5\n。 この\nが「改行」、つまりエンターキーを押した結果を表しているわけです。 trimメソッドは、\nを削除するので、ただの5になります。

文字列のparseメソッドは、文字列をパースして何らかの数値にします。 このメソッドは、いろんな数値型をパースできるので、let guess: u32としてコンパイラに私たちが求めている型をズバリ示唆する必要があるのです。 guessの後のコロン(:)がコンパイラに変数の型を注釈する合図になります。 Rustには、組み込みの数値型がいくつかあります; ここのu32型は、32ビットの非負整数です。 u32型は小さな非負整数のデフォルトの選択肢として丁度良いです。他の数値型については、第3章で学ぶでしょう。 付け加えると、このサンプルプログラムのu32という注釈とsecret_number変数との比較は、 secret_number変数もu32型であるとコンパイラが推論することを意味します。 従って、今では比較が同じ型の2つの値で行われることになるわけです!

parseメソッドの呼び出しは、エラーになりやすいです。例としては、文字列がA👍%を含んでいたら、 数値に変換できるわけがありません。失敗する可能性があるので、parseメソッドは、 Result型を返すわけです。ちょうど、(「Result型で失敗する可能性に対処する」節で先ほど議論した)read_lineメソッドのようにというわけですね。 今回も、expectメソッドを使用してResult型を同じように扱います。このResultexpectメソッドを再度使用して、 同じように扱います。もし、文字列から数値を生成できなかったために、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.43 secs
     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

// --snip--

    println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess.");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

見てわかる通り、予想入力部分以降をループに入れ込みました。ループ内の行にインデントを追加するのを忘れないようにして、 またプログラムを走らせてみましょう。新たな問題が発生したことに気付いてください。 プログラムが教えた通りに動作しているからですね: 永遠に予想入力を求めるわけです! これでは、ユーザが終了できないようです!

ユーザは、ctrl-cというキーボードショートカットを使って、いつでもプログラムを強制終了させられます。 しかし、「予想を秘密の数字と比較する」節のparseメソッドに関する議論で触れたように、 この貪欲なモンスターを回避する別の方法があります: ユーザが数字以外の答えを入力すれば、プログラムはクラッシュするのです。 ユーザは、その利点を活かして、終了することができます。以下のようにですね:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50 secs
     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/libcore/result.rs:785
(スレッド'main'は'数字を入力してください!: ParseIntError { kind: InvalidDigit }', src/libcore/result.rs:785でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.
(注釈: `RUST_BACKTRACE=1`で走らせるとバックトレースを見れます)
error: Process didn't exit successfully: `target/debug/guess` (exit code: 101)
(エラー: プロセスは予期なく終了しました)

quitと入力すれば、実際にゲームを終了できるわけですが、別に他の数字以外の入力でもそうなります。 しかしながら、これは最低限度と言えるでしょう。正しい数字が予想されたら、自動的にゲームが停止してほしいわけです。

正しい予想をした後に終了する

break文を追加して、ユーザが勝った時にゲームが終了するようにプログラムしましょう:

ファイル名: src/main.rs

// --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

break文の1行をYou win!の後に追記することで、ユーザが秘密の数字を正確に予想した時に、 プログラムはループを抜けるようになりました。ついでに、ループを抜けることは、プログラムを終了することを意味します。 ループがmain関数の最後の部分だからですね。

不正な入力を処理する

さらにゲームの振る舞いを改善するために、ユーザが数値以外を入力した時にプログラムをクラッシュさせるのではなく、 非数値を無視してユーザが数当てを続けられるようにしましょう!これは、 guessString型からu32型に変換される行を改変することで達成できます。リスト2-5のようにですね。

ファイル名: src/main.rs

// --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--

リスト2-5: 非数値の予想を無視し、プログラムをクラッシュさせるのではなく、もう1回予想してもらう

expectメソッドの呼び出しからmatch式に切り替えることは、 エラーでクラッシュする動作からエラー処理を行う処理へ変更する一般的な手段になります。parseメソッドは、 Result型を返し、ResultOkErrの値を取りうるenumであることを思い出してください。 ここでは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)
     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

extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

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: 数当てゲームの完全なコード

まとめ

ここまでで、数当てゲームの作成に成功しました。おめでとうございます!

このプロジェクトは、たくさんの新しいRustの概念に触れる実践的な方法でした: let文、match式、メソッド、関連関数、外部クレートの使用などなど。 以降の数章で、これらの概念についてより深く学ぶことになるでしょう。 第3章では、ほとんどのプログラミング言語が持っている、変数、データ型、関数などの概念について解説し、 それらのRustでの使用方法について示します。 第4章では、所有権について見ます。これにより、Rustは他の言語とかけ離れた存在になっています。 第5章では、構造体とメソッド記法について議論し、第6章ではenumの動作法を説明します。

一般的なプログラミングの概念

この章では、ほとんど全てのプログラミング言語で見られる概念を解説し、それらがRustにおいて、 どう動作するかを見ていきます。多くのプログラミング言語は、その核心において、いろいろなものを共有しています。 この章で提示する概念は、全てRustに固有のものではありませんが、Rustの文脈で議論し、 これらの概念を使用することにまつわる仕様を説明します。

具体的には、変数、基本的な型、関数、コメント、そしてフロー制御について学びます。 これらの基礎は全てのRustプログラムに存在するものであり、それらを早期に学ぶことは 強力な基礎を築くことになるでしょう。

キーワード

Rust言語にも他の言語同様、キーワードが存在し、これらは言語だけが使用できるようになっています。 これらの単語は、変数や関数名には使えないことを弁えておいてください。ほとんどのキーワードは、特別な意味を持っており、 自らのRustプログラムにおいて、様々な作業をこなすために使用することができます; いくつかは、紐付けられた機能がないものの、将来Rustに追加されるかもしれない機能用に予約されています。 キーワードの一覧は、付録Aで確認できます。

変数と可変性

第2章で触れた通り、変数は標準で不変になります。これは、 Rustが提供する安全性や簡潔な並列プログラミングの利点を享受する形でコードを書くための選択の1つです。 ところが、まだ変数を可変にするという選択肢も残されています。 どのように、そしてなぜRustは不変性を推奨するのか、そしてなぜそれとは違う道を選びたくなることがあるのか見ていきましょう。

変数が不変であると、値が一旦名前に束縛されたら、その値を変えることができません。 これを具体的に説明するために、projectsディレクトリにcargo new --bin variablesコマンドを使って、 variablesという名前のプロジェクトを生成しましょう。

それから、新規作成したvariablesディレクトリで、src/main.rsファイルを開き、 その中身を以下のコードに置き換えましょう。このコードはコンパイルできません:

ファイル名: src/main.rs

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);     // xの値は{}です
    x = 6;
    println!("The value of x is: {}", x);
}

これを保存し、cargo runコマンドでプログラムを走らせてください。次の出力に示されているようなエラーメッセージを受け取るはずです:

error[E0384]: cannot assgin twice immutable variable `x`
              (不変変数`x`に2回代入できません)
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
  |         (`x`への最初の代入)
3 |     println!("The value of x is: {}", x);
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

この例では、コンパイラがプログラムに潜むエラーを見つけ出す手助けをしてくれることが示されています。 コンパイルエラーは、イライラすることもあるものですが、まだプログラムにしてほしいことを安全に行えていないだけということなのです; エラーが出るからといって、あなたがいいプログラマではないという意味ではありません! 経験豊富なRustaceanでも、コンパイルエラーを出すことはあります。

このエラーは、エラーの原因が不変変数xに2回代入できないであると示しています。不変なxという変数に第2段階の値を代入しようとしたからです。

以前に不変と指定された値を変えようとした時に、コンパイルエラーが出るのは重要なことです。 なぜなら、この状況はまさしく、バグに繋がるからです。コードのある部分は、 値が変わることはないという前提のもとに処理を行い、別の部分がその値を変更していたら、 最初の部分が目論見通りに動いていない可能性があるのです。このようなバグの発生は、 事実(訳注:実際にプログラムを走らせた結果のことと思われる)の後には追いかけづらいものです。 特に第2のコード片が、値を時々しか変えない場合尚更です。

Rustでは、値が不変であると宣言したら、本当に変わらないことをコンパイラが担保してくれます。 つまり、コードを読み書きする際に、どこでどうやって値が変化しているかを追いかける必要がなくなります。 故にコードを通して正しいことを確認するのが簡単になるのです。

しかし、可変性は時として非常に有益なこともあります。変数は、標準でのみ、不変です。つまり、 第2章のように変数名の前にmutキーワードを付けることで、可変にできるわけです。この値が変化できるようにするとともに、 mutにより、未来の読者に対してコードの別の部分がこの変数の値を変える可能性を示すことで、その意図を汲ませることができるのです。

例として、src/main.rsファイルを以下のように書き換えてください:

ファイル名: src/main.rs

fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

今、このプログラムを走らせると、以下のような出力が得られます:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30 secs
     Running `target/debug/variables`
The value of x is: 5   (xの値は5です)
The value of x is: 6

mutキーワードを使われると、xが束縛している値を5から6に変更できます。 変数を可変にする方が、不変変数だけがあるよりも書きやすくなるので、変数を可変にしたくなることもあるでしょう。

考えるべきトレードオフはバグの予防以外にも、いくつかあります。例えば、大きなデータ構造を使う場合などです。 インスタンスを可変にして変更できるようにする方が、いちいちインスタンスをコピーして新しくメモリ割り当てされたインスタンスを返すよりも速くなります。 小規模なデータ構造なら、新規インスタンスを生成して、もっと関数型っぽいコードを書く方が通して考えやすくなるため、 低パフォーマンスは、その簡潔性を得るのに足りうるペナルティになるかもしれません。

変数と定数(constants)の違い

変数の値を変更できないようにするといえば、他の多くの言語も持っている別のプログラミング概念を思い浮かべるかもしれません: 定数です。不変変数のように、定数は名前に束縛され、変更することが叶わない値のことですが、 定数と変数の間にはいくつかの違いがあります。

まず、定数にはmutキーワードは使えません: 定数は標準で不変であるだけでなく、常に不変なのです。

定数はletキーワードの代わりに、constキーワードで宣言し、値の型は必ず注釈しなければなりません。 型と型注釈については次のセクション、「データ型」で解説する予定なので、その詳細については気にする必要はありません。 ただ単に型は常に注釈しなければならないのだと思っていてください。

定数はどんなスコープでも定義できます。グローバルスコープも含めてです。なので、 いろんなところで使用される可能性のある値を定義するのに役に立ちます。

最後の違いは、定数は定数式にしかセットできないことです。関数呼び出し結果や、実行時に評価される値にはセットできません。

定数の名前がMAX_POINTSで、値が100,000にセットされた定数定義の例をご覧ください。(Rustの定数の命名規則は、 全て大文字でアンダースコアで単語区切りすることです):


# #![allow(unused_variables)]
#fn main() {
const MAX_POINTS: u32 = 100_000;
#}

定数は、プログラムが走る期間、定義されたスコープ内でずっと有効です。従って、 プログラムのいろんなところで使用される可能性のあるアプリケーション空間の値を定義するのに有益な選択肢になります。 例えば、ゲームでプレイヤーが取得可能なポイントの最高値や、光速度などですね。

プログラム中で使用されるハードコードされた値に対して、定数として名前付けすることは、 コードの将来的な管理者にとって値の意味を汲むのに役に立ちます。将来、ハードコードされた値を変える必要が出た時に、 たった1箇所を変更するだけで済むようにもしてくれます。

シャドーイング

第2章の数当てゲームのチュートリアル、「秘密の数字と予想を比較する」節で見たように、前に定義した変数と同じ名前の変数を新しく宣言でき、 新しい変数は、前の変数を覆い隠し(shadow)ます。Rustaceanはこれを最初の変数は、 2番目の変数にシャドーされたと言い、この変数を使用した際に、2番目の変数の値が現れるということです。 以下のようにして、同じ変数名を用いて変数を覆い隠し、letキーワードの使用を繰り返します:

ファイル名: src/main.rs

fn main() {
    let x = 5;

    let x = x + 1;

    let x = x * 2;

    println!("The value of x is: {}", x);
}

このプログラムはまず、x5という値に束縛します。それからlet x =を繰り返すことでxを覆い隠し、 元の値に1を加えることになるので、xの値は、6になります。 3番目のlet文もxを覆い隠し、以前の値に2をかけることになるので、xの最終的な値は12になります。 このプログラムを走らせたら、以下のように出力するでしょう:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/variables`
The value of x is: 12

シャドーイングは、変数をmutにするのとは違います。なぜなら、letキーワードを使わずに、 誤ってこの変数に再代入を試みようものなら、コンパイルエラーが出るからです。letを使うことで、 値にちょっとした加工は加えられますが、その加工が終わったら、変数は不変になるわけです。

mutと上書きのもう一つの違いは、再度letキーワードを使用したら、実効的には新しい変数を生成していることになるので、 値の型を変えつつ、同じ変数名を使いまわせることです。例えば、 プログラムがユーザに何らかのテキストに対して空白文字を入力することで何個分のスペースを表示したいかを尋ねるとします。 ただ、実際にはこの入力を数値として保持したいとしましょう:


# #![allow(unused_variables)]
#fn main() {
let spaces = "   ";
let spaces = spaces.len();
#}

この文法要素は、容認されます。というのも、最初のspaces変数は文字列型であり、2番目のspaces変数は、 たまたま最初の変数と同じ名前になったまっさらな変数のわけですが、数値型になるからです。故に、シャドーイングのおかげで、 異なる名前を思いつく必要がなくなるわけです。spaces_strspaces_numなどですね; 代わりに、 よりシンプルなspacesという名前を再利用できるわけです。一方で、この場合にmutを使おうとすると、 以下に示した通りですが、コンパイルエラーになるわけです:

let mut spaces = "   ";
spaces = spaces.len();

変数の型を可変にすることは許されていないと言われているわけです:

error[E0308]: mismatched types          (型が合いません)
 --> src/main.rs:3:14
  |
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected &str, found usize
  |                           (&str型を予期しましたが、usizeが見つかりました)
  |
  = note: expected type `&str`
             found type `usize`

さあ、変数が動作する方法を見てきたので、今度は変数が取りうるデータ型について見ていきましょう。

データ型

Rustにおける値は全て、何らかのデータ型になり、コンパイラがどんなデータが指定されているか知れるので、 そのデータの取り扱い方も把握できるというわけです。2種のデータ型のサブセットを見ましょう: スカラー型と複合型です。

Rustは静的型付き言語であることを弁えておいてください。つまり、 コンパイル時に全ての変数の型が判明している必要があるということです。コンパイラは通常、値と使用方法に基づいて、 使用したい型を推論してくれます。複数の型が推論される可能性がある場合、例えば、 第2章の「秘密の数字と予想を比較する」節でparseメソッドを使ってString型を数値型に変換した時のような場合には、 型注釈をつけなければいけません。以下のようにですね:


# #![allow(unused_variables)]
#fn main() {
let guess: u32 = "42".parse().expect("Not a number!");    // 数字ではありません!
#}

ここで型注釈を付けなければ、コンパイラは以下のエラーを表示し、これは可能性のある型のうち、 どの型を使用したいのかを知るのに、コンパイラがプログラマからもっと情報を得る必要があることを意味します:

error[E0282]: type annotations needed
              (型注釈が必要です)
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^ cannot infer type for `_`
  |               (`_`の型が推論できません)
  |
  = note: type annotations or generic parameter binding required
    (注釈: 型注釈、またはジェネリクス引数束縛が必要です)

他のデータ型についても、様々な型注釈を目にすることになるでしょう。

スカラー型

スカラー型は、単独の値を表します。Rustには主に4つのスカラー型があります: 整数、浮動小数点数、論理値、最後に文字です。他のプログラミング言語でも、これらの型を見かけたことはあるでしょう。 Rustでの動作方法に飛び込みましょう。

整数型

整数とは、小数部分のない数値のことです。第2章で一つの整数型を使用しました。u32型です。 この型定義は、紐付けられる値が、符号なし整数(符号付き整数はuではなく、iで始まります)になり、 これは、32ビット分のサイズを取ります。表3-1は、Rustの組み込み整数型を表示しています。 符号付きと符号なし欄の各バリアント(例: i16)を使用して、整数値の型を宣言することができます。

表3-1: Rustの整数型

大きさ 符号付き 符号なし
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
arch isize usize

各バリアントは、符号付きか符号なしかを選べ、明示的なサイズを持ちます。符号付き符号なしは、 数値が正負を持つかどうかを示します。つまり、数値が符号を持つ必要があるかどうか(符号付き)、または、 絶対に正数にしかならず符号なしで表現できるかどうか(符号なし)です。これは、数値を紙に書き下すのと似ています: 符号が問題になるなら、数値はプラス記号、またはマイナス記号とともに表示されます; しかしながら、 その数値が正数であると仮定することが安全なら、符号なしで表示できるわけです。符号付き数値は、 2の補数表現で保持されます(これが何なのか確信を持てないのであれば、ネットで検索することができます。 まあ要するに、この解説は、この本の範疇外というわけです)。

各符号付きバリアントは、-(2n - 1)以上2n - 1 - 1以下の数値を保持でき、 ここでnはこのバリアントが使用するビット数です。以上から、i8型は-(27)から27 - 1まで、 つまり、-128から127までを保持できます。符号なしバリアントは、0以上2n - 1以下を保持できるので、 u8型は、0から28 - 1までの値、つまり、0から255までを保持できることになります。

加えて、isizeusize型は、プログラムが動作しているコンピュータの種類に依存します: 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ビットシステム上でも、 この型が普通最速になります。isizeusizeを使う主な状況は、何らかのコレクションにアクセスすることです。

浮動小数点型

Rustにはさらに、浮動小数点数に対しても、2種類の基本型があり、浮動小数点数とは数値に小数点がついたもののことです。 Rustの浮動小数点型は、f32f64で、それぞれ32ビットと64ビットサイズです。基準型はf64です。 なぜなら、現代のCPUでは、f32とほぼ同スピードにもかかわらず、より精度が高くなるからです。

実際に動作している浮動小数点数の例をご覧ください:

ファイル名: src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

浮動小数点数は、IEEE-754規格に従って表現されています。f32が単精度浮動小数点数、 f64が倍精度浮動小数点数です。

数値演算

Rustにも全数値型に期待されうる標準的な数学演算が用意されています: 足し算、引き算、掛け算、割り算、余りです。 以下の例では、let文での各演算の使用方法をご覧になれます:

ファイル名: src/main.rs

fn main() {
    // 足し算
    let sum = 5 + 10;

    // 引き算
    let difference = 95.5 - 4.3;

    // 掛け算
    let product = 4 * 30;

    // 割り算
    let quotient = 56.7 / 32.2;

    // 余り
    let remainder = 43 % 5;
}

これらの文の各式は、数学演算子を使用しており、一つの値に評価され、変数に束縛されます。 付録BにRustで使える演算子の一覧が載っています。

論理値型

他の多くの言語同様、Rustの論理値型も取りうる値は二つしかありません: truefalseです。 Rustの論理値型は、boolと指定されます。 例です:

ファイル名: src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // 明示的型注釈付きで
}

論理値を使う主な手段は、条件式です。例えば、if式などですね。if式のRustでの動作方法については、 「制御フロー」節で解説します。

文字型

ここまで、数値型のみ扱ってきましたが、Rustには文字も用意されています。Rustのchar型は、 言語の最も基本的なアルファベット型であり、以下のコードでその使用方法の一例を見ることができます。 (charは、ダブルクォーテーションマークを使用する文字列に対して、シングルクォートで指定されることに注意してください。)

ファイル名: src/main.rs

fn main() {
   let c = 'z';
   let z = 'ℤ';
   let heart_eyed_cat = '😻';    //ハート目の猫
}

Rustのchar型は、ユニコードのスカラー値を表します。これはつまり、アスキーよりもずっとたくさんのものを表せるということです。 アクセント文字; 中国語、日本語、韓国語文字; 絵文字; ゼロ幅スペースは、全てRustでは、有効なchar型になります。ユニコードスカラー値は、 U+0000からU+D7FFまでとU+E0000から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つの個別の変数(xyzですね)に変換しています。 この過程は、分配と呼ばれます。単独のタプルを破壊して三分割しているからです。最後に、 プログラムは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_variables)]
#fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
#}
配列の要素にアクセスする

配列は、スタック上に確保される一塊のメモリです。添え字によって、 配列の要素にこのようにアクセスすることができます:

ファイル名: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

この例では、firstという名前の変数には1という値が格納されます。配列の[0]番目にある値が、 それだからですね。secondという名前の変数には、配列の[1]番目の値2が格納されます。

配列要素への無効なアクセス

配列の終端を越えて要素にアクセスしようとしたら、どうなるでしょうか? 先ほどの例を以下のように変えたとすると、コンパイルは通りますが、実行するとエラーで終了します:

ファイル名: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
    let index = 10;

    let element = a[index];

    println!("The value of element is: {}", element);   // 要素の値は{}です
}

このコードをcargo runで走らせると、以下のような結果になります:

$ cargo run
   Compiling arrays v0.1.0 (file:///projects/arrays)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/arrays`
thread '<main>' panicked at 'index out of bounds: the len is 5 but the index is
 10', src/main.rs:6
スレッド'<main>'は'範囲外アクセス: 長さは5ですが、添え字は10でした', src/main.rs:6
でパニックしました
note: Run with `RUST_BACKTRACE=1` for a backtrace.

コンパイルでは何もエラーが出なかったものの、プログラムは実行時エラーに陥り、 正常終了しませんでした。要素に添え字アクセスを試みると、言語は、 指定されたその添え字が配列長よりも小さいかを確認してくれます。添え字が配列長よりも大きければ、言語はパニックします。 パニックとは、プログラムがエラーで終了したことを表すRust用語です。

これは、実際に稼働しているRustの安全機構の最初の例になります。低レベル言語の多くでは、 この種のチェックは行われないため、間違った添え字を与えると、無効なメモリにアクセスできてしまいます。 Rustでは、メモリアクセスを許可し、処理を継続する代わりに即座にプログラムを終了することで、 この種のエラーからプログラマを保護しています。Rustのエラー処理については、第9章でもっと議論します。

関数

関数は、Rustのコードにおいてよく見かける存在です。すでに、言語において最も重要な関数のうちの一つを目撃していますね: そう、main関数です。これは、多くのプログラムのエントリーポイント(訳注: プログラム実行時に最初に走る関数のこと)になります。 fnキーワードもすでに見かけましたね。これによって新しい関数を宣言することができます。

Rustの関数と変数の命名規則は、スネークケース(訳注: some_variableのような命名規則)を使うのが慣例です。 スネークケースとは、全文字を小文字にし、単語区切りにアンダースコアを使うことです。 以下のプログラムで、サンプルの関数定義をご覧ください:

ファイル名: src/main.rs

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");  // 別の関数
}

Rustにおいて関数定義は、fnキーワードで始まり、関数名の後に丸かっこの組が続きます。 波かっこが、コンパイラに関数本体の開始と終了の位置を伝えます。

定義した関数は、名前に丸かっこの組を続けることで呼び出すことができます。 another_function関数がプログラム内で定義されているので、main関数内から呼び出すことができるわけです。 ソースコード中でanother_functionmain関数のに定義していることに注目してください; 勿論、main関数の前に定義することもできます。コンパイラは、関数がどこで定義されているかは気にしません。 どこかで定義されていることのみ気にします。

functionsという名前の新しいバイナリ生成プロジェクトを始めて、関数についてさらに深く探求していきましょう。 another_functionの例をsrc/main.rsファイルに配置して、走らせてください。 以下のような出力が得られるはずです:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.28 secs
     Running `target/debug/functions`
Hello, world!
Another function.

行出力は、main関数内に書かれた順序で実行されています。最初に"Hello, world"メッセージが出て、 それからanother_functionが呼ばれて、こちらのメッセージが出力されています。

関数の引数

関数は、引数を持つようにも定義できます。引数とは、関数シグニチャの一部になる特別な変数のことです。 関数に引数があると、引数の位置に実際の値を与えることができます。技術的にはこの実際の値は 実引数と呼ばれますが、普段の会話では、仮引数("parameter")と実引数("argument")を関数定義の変数と関数呼び出し時に渡す実際の値、 両方の意味に区別なく使います(訳注: 日本語では、どちらも単に引数と呼ぶことが多いでしょう)。

以下の書き直したanother_functionでは、Rustの仮引数がどのようなものかを示しています:

ファイル名: src/main.rs

fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {}", x);   // xの値は{}です
}

このプログラムを走らせてみてください; 以下のような出力が得られるはずです:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21 secs
     Running `target/debug/functions`
The value of x is: 5

another_functionの宣言には、xという名前の仮引数があります。xの型は、 i32と指定されています。値5another_functionに渡されると、println!マクロにより、 フォーマット文字列中の1組の波かっこがある位置に値5が出力されます。

関数シグニチャにおいて、各仮引数の型を宣言しなければなりません。これは、Rustの設計において、 意図的な判断です: 関数定義で型注釈が必要不可欠ということは、コンパイラがその意図するところを推し量るのに、 コードの他の箇所で使用する必要がないということを意味します。

関数に複数の仮引数をもたせたいときは、仮引数定義をカンマで区切ってください。 こんな感じです:

ファイル名: src/main.rs

fn main() {
    another_function(5, 6);
}

fn another_function(x: i32, y: i32) {
    println!("The value of x is: {}", x);
    println!("The value of y is: {}", y);
}

この例では、2引数の関数を生成しています。そして、引数はどちらもi32型です。それからこの関数は、 仮引数の値を両方出力します。関数引数は、全てが同じ型である必要はありません。今回は、 偶然同じになっただけです。

このコードを走らせてみましょう。今、functionプロジェクトのsrc/main.rsファイルに記載されているプログラムを先ほどの例と置き換えて、 cargo runで走らせてください:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/functions`
The value of x is: 5
The value of y is: 6

xに対して値5yに対して値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`を使う変数宣言は、文です)

このlet y = 6という文は値を返さないので、xに束縛するものがないわけです。これは、 CやRubyなどの言語とは異なる動作です。CやRubyでは、代入は代入値を返します。これらの言語では、 x = y = 6と書いて、xyも値6になるようにできるのですが、Rustにおいては、 そうは問屋が卸さないわけです。

式は何かに評価され、これからあなたが書くRustコードの多くを構成します。 簡単な数学演算(5 + 6など)を思い浮かべましょう。この例は、値11に評価される式です。式は文の一部になりえます: リスト3-1において、let y = 6という文の6は値6に評価される式です。関数呼び出しも式です。マクロ呼び出しも式です。 新しいスコープを作る際に使用するブロック({})も式です:

ファイル名: src/main.rs

fn main() {
    let x = 5;

    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {}", y);
}

以下の式:

{
    let x = 3;
    x + 1
}

は今回の場合、4に評価されるブロックです。その値が、let文の一部としてyに束縛されます。 今まで見かけてきた行と異なり、文末にセミコロンがついていないx + 1の行に気をつけてください。 式は終端にセミコロンを含みません。式の終端にセミコロンを付けたら、文に変えてしまいます。そして、文は値を返しません。 次に関数の戻り値や式を見ていく際にこのことを肝に命じておいてください。

戻り値のある関数

関数は、それを呼び出したコードに値を返すことができます。戻り値に名前を付けはしませんが、 矢印(->)の後に型を書いて宣言します。Rustでは、関数の戻り値は、関数本体ブロックの最後の式の値と同義です。 returnキーワードで関数から早期リターンし、値を指定することもできますが、多くの関数は最後の式を暗黙的に返します。 こちらが、値を返す関数の例です:

ファイル名: src/main.rs

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {}", x);
}

five関数内には、関数呼び出しもマクロ呼び出しも、let文でさえ存在しません。数字の5が単独であるだけです。 これは、Rustにおいて、完璧に問題ない関数です。関数の戻り値型が-> i32と指定されていることにも注目してください。 このコードを実行してみましょう; 出力はこんな感じになるはずです:

$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30 secs
     Running `target/debug/functions`
The value of x is: 5

five内の5が関数の戻り値です。だから、戻り値型がi32なのです。これについてもっと深く考察しましょう。 重要な箇所は2つあります: まず、let x = five()という行は、関数の戻り値を使って変数を初期化していることを示しています。 関数five5を返すので、この行は以下のように書くのと同義です:


# #![allow(unused_variables)]
#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;
}

このコードを実行すると、以下のようにエラーが出ます:

error[E0308]: mismatched types
              (型が合いません)
 --> src/main.rs:7:28
  |
7 |   fn plus_one(x: i32) -> i32 {
  |  ____________________________^
8 | |     x + 1;
  | |          - help: consider removing this semicolon
9 | | }
  | |_^ expected i32, found ()
  |     (i32を予期したのに、()型が見つかりました)
  |
  = note: expected type `i32`
             found type `()`

メインのエラーメッセージである「型が合いません」でこのコードの根本的な問題が明らかになるでしょう。 関数plus_oneの定義では、i32型を返すと言っているのに、文は値に評価されないからです。このことは、 ()、つまり空のタプルとして表現されています。それゆえに、何も戻り値がなく、これが関数定義と矛盾するので、 結果としてエラーになるわけです。この出力内で、コンパイラは問題を修正する手助けになりそうなメッセージも出していますね: セミコロンを削除するよう提言しています。そして、そうすれば、エラーは直るわけです。

コメント

全プログラマは、自分のコードがわかりやすくなるよう努めますが、時として追加の説明が許されることもあります。 このような場合、プログラマは注釈またはコメントをソースコードに残し、コメントをコンパイラは無視しますが、 ソースコードを読む人間には有益なものと思えるでしょう。

こちらが単純なコメントです:


# #![allow(unused_variables)]
#fn main() {
// hello, world
#}

Rustでは、コメントは2連スラッシュで始め、行の終わりまで続きます。コメントが複数行にまたがる場合、 各行に//を含める必要があります。こんな感じに:


# #![allow(unused_variables)]
#fn main() {
// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.
// ここで何か複雑なことをしていて、長すぎるから複数行のコメントが必要なんだ。
// ふう!願わくば、このコメントで何が起きているか説明されていると嬉しい。
#}

コメントは、コードが書かれた行の末尾にも配置することができます:

Filename: src/main.rs

fn main() {
    let lucky_number = 7; // I’m feeling lucky today(今日はラッキーな気がするよ)
}

しかし、こちらの形式のコメントの方が見かける機会は多いでしょう。注釈しようとしているコードの1行上に書く形式です:

ファイル名: src/main.rs

fn main() {
    // I’m feeling lucky today
    // 今日はラッキーな気がするよ
    let lucky_number = 7;
}

Rustには他の種類のコメント、ドキュメントコメントもあり、それについては第14章で議論します。

フロー制御

条件が真かどうかによってコードを走らせるかどうかを決定したり、 条件が真の間繰り返しコードを走らせるか決定したりすることは、多くのプログラミング言語において、基本的な構成ブロックです。 Rustコードの実行フローを制御する最も一般的な文法要素は、if式とループです。

if

if式によって、条件に依存して枝分かれをさせることができます。条件を与え、以下のように宣言します。 「もし条件が合ったら、この一連のコードを実行しろ。条件に合わなければ、この一連のコードは実行するな」と。

projectsディレクトリにbranchesという名のプロジェクトを作ってif式について掘り下げていきましょう。 src/main.rsファイルに、以下のように入力してください:

ファイル名: src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");       // 条件は真です
    } else {
        println!("condition was false");      // 条件は偽です
    }
}

if式は全て、キーワードのifから始め、条件式を続けます。今回の場合、 条件式は変数numberが5未満の値になっているかどうかをチェックします。 条件が真の時に実行したい一連のコードを条件式の直後に波かっこで包んで配置します。if式の条件式と紐付けられる一連のコードは、 時としてアームと呼ばれることがあります。 第2章の「予想と秘密の数字を比較する」の節で議論したmatch式のアームのようですね。

オプションとして、else式を含むこともでき(ここではそうしています)、これによりプログラムは、 条件式が偽になった時に実行するコードを与えられることになります。仮に、else式を与えずに条件式が偽になったら、 プログラムは単にifブロックを無視して次のコードを実行しにいきます。

このコードを走らせてみましょう; 以下のような出力を目の当たりにするはずです:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/branches`
condition was true

numberの値を条件がfalseになるような値に変更してどうなるか確かめてみましょう:

let number = 7;

再度プログラムを実行して、出力に注目してください:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/branches`
condition was false

このコード内の条件式は、bool型でなければならないことにも触れる価値があります。 条件式が、bool型でない時は、エラーになります。例えば、試しに以下のコードを実行してみてください:

ファイル名: src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("number was three");     // 数値は3です
    }
}

今回、ifの条件式は3という値に評価され、コンパイラがエラーを投げます:

error[E0308]: mismatched types
              (型が合いません)
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected bool, found integral variable
  |               (bool型を予期したのに、整数変数が見つかりました)
  |
  = note: expected type `bool`
             found type `{integer}`

このエラーは、コンパイラはbool型を予期していたのに、整数だったことを示唆しています。 RubyやJavaScriptなどの言語とは異なり、Rustでは、論理値以外の値が、自動的に論理値に変換されることはありません。 明示的に必ずifには条件式として、論理値を与えなければなりません。 例えば、数値が0以外の時だけifのコードを走らせたいなら、以下のようにif式を変更することができます:

ファイル名: src/main.rs

fn main() {
    let number = 3;

    if number != 0 {
        println!("number was something other than zero");   // 数値は0以外の何かです
    }
}

このコードを実行したら、number was something other than zeroと表示されるでしょう。

else ifで複数の条件を扱う

ifelseを組み合わせてelse if式にすることで複数の条件を持たせることもできます。例です:

ファイル名: src/main.rs

fn main() {
    let number = 6;

    if number % 4 == 0 {
        // 数値は4で割り切れます
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        // 数値は3で割り切れます
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        // 数値は2で割り切れます
        println!("number is divisible by 2");
    } else {
        // 数値は4、3、2で割り切れません
        println!("number is not divisible by 4, 3, or 2");
    }
}

このプログラムには、通り道が4つあります。実行後、以下のような出力を目の当たりにするはずです:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/branches`
number is divisible by 3

このプログラムを実行すると、if式が順番に吟味され、最初に条件が真になった本体が実行されます。 6は2で割り切れるものの、number is devisible by 2や、 elseブロックのnumber is not divisible by 4, 3, or 2という出力はされないことに注目してください。 それは、言語が最初の真条件のブロックのみを実行し、 条件に合ったものが見つかったら、残りはチェックすらしないからです。

else if式を使いすぎると、コードがめちゃくちゃになってしまうので、1つ以上あるなら、 コードをリファクタリングしたくなるかもしれません。これらのケースに有用なmatchと呼ばれる、 強力なRustの枝分かれ文法要素については第6章で解説します。

let文内でif式を使う

ifは式なので、let文の右辺に持ってくることができます。リスト3-2のようにですね。

ファイル名: src/main.rs

fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };

    // numberの値は、{}です
    println!("The value of number is: {}", number);
}

リスト3-2: if式の結果を変数に代入する

このnumber変数は、if式の結果に基づいた値に束縛されます。このコードを走らせてどうなるか確かめてください:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30 secs
     Running `target/debug/branches`
The value of number is: 5

一連のコードは、そのうちの最後の式に評価され、数値はそれ単独でも式になることを思い出してください。 今回の場合、このif式全体の値は、どのブロックのコードが実行されるかに基づきます。これはつまり、 ifの各アームの結果になる可能性がある値は、同じ型でなければならないということになります; リスト3-4で、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);
}

このコードをコンパイルしようとすると、エラーになります。ifelseアームは互換性のない値の型になり、 コンパイラがプログラム内で問題の見つかった箇所をスバリ指摘してくれます:

error[E0308]: if and else have incompatible types
              (ifとelseの型に互換性がありません)
 --> src/main.rs:4:18
  |
4 |       let number = if condition {
  |  __________________^
5 | |         5
6 | |     } else {
7 | |         "six"
8 | |     };
  | |_____^ expected integral variable, found &str
  |         (整数変数を予期しましたが、&strが見つかりました)
  |
  = note: expected type `{integer}`
             found type `&str`

ifブロックの式は整数に評価され、elseブロックの式は文字列に評価されます。これでは動作しません。 変数は単独の型でなければならないからです。コンパイラは、コンパイル時にnumber変数の型を確実に把握する必要があるため、 コンパイル時にnumberが使われている箇所全部で型が有効かどうか検査することができるのです。 numberの型が実行時にしか決まらないのであれば、コンパイラはそれを実行することができなくなってしまいます; どの変数に対しても、架空の複数の型があることを追いかけなければならないのであれば、コンパイラはより複雑になり、 コードに対して行える保証が少なくなってしまうでしょう。

ループでの繰り返し

一連のコードを1回以上実行できると、しばしば役に立ちます。この作業用に、 Rustにはいくつかのループが用意されています。ループは、本体内のコードを最後まで実行し、 直後にまた最初から処理を開始します。 ループを試してみるのに、loopsという名の新プロジェクトを作りましょう。

Rustには3種類のループが存在します: loopwhileforです。 それぞれ試してみましょう。

loopでコードを繰り返す

loopキーワードを使用すると、同じコードを何回も何回も永遠に明示的にやめさせるまで実行します。

例として、loopsディレクトリのsrc/main.rsファイルを以下のような感じに書き換えましょう:

ファイル名: src/main.rs

fn main() {
    loop {
        println!("again!");   // また
    }
}

このプログラムを実行すると、プログラムを手動で止めるまで、何度も何度も続けてagain!と出力するでしょう。 ほとんどのターミナルでctrl-cというショートカットが使え、 永久ループに囚われてしまったプログラムを終了させられます。試しにやってみましょう:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

^Cという記号が出た場所が、ctrl-cを押した場所です。^Cの後にはagain!と表示されたり、 されなかったりします。ストップシグナルをコードが受け取った時にループのどこにいたかによります。

幸いなことに、Rustにはループを抜け出す別のより信頼できる手段があります。 ループ内にbreakキーワードを配置することでプログラムに実行を終了すべきタイミングを教えることができます。 第2章の「正しい予想をした後に終了する」節の数当てゲーム内でこれをして、ユーザが予想を的中させ、 ゲームに勝った時にプログラムを終了させたことを思い出してください。

whileで条件付きループ

プログラムにとってループ内で条件式を評価できると、有益なことがしばしばあります。条件が真の間、 ループが走るわけです。条件が真でなくなった時にプログラムはbreakを呼び出し、ループを終了します。 このタイプのループは、loopifelsebreakを組み合わせることでも実装できます; お望みなら、プログラムで試してみるのもいいでしょう。

しかし、このパターンは頻出するので、Rustにはそれ用の文法要素が用意されていて、whileループと呼ばれます。 リスト3-3は、whileを使用しています: プログラムは3回ループし、それぞれカウントダウンします。 それから、ループ後に別のメッセージを表示して終了します:

ファイル名: src/main.rs

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number = number - 1;
    }

    // 発射!
    println!("LIFTOFF!!!");
}

リスト3-3: 条件が真の間、コードを走らせるwhileループを使用する

この文法要素により、loopifelsebreakを使った時に必要になるネストがなくなり、 より明確になります。条件が真の間、コードは実行されます; そうでなければ、ループを抜けます.

forでコレクションを覗き見る

while要素を使って配列などのコレクションの要素を覗き見ることができます。例えば、リスト3-4を見ましょう。

ファイル名: src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        // 値は{}です
        println!("the value is: {}", a[index]);

        index = index + 1;
    }
}

リスト3-4: whileループでコレクションの各要素を覗き見る

ここで、コードは配列の要素を順番にカウントアップして覗いています。番号0から始まり、 配列の最終番号に到達するまでループします(つまり、index < 5が真でなくなる時です)。 このコードを走らせると、配列内の全要素が出力されます:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
     Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

予想通り、配列の5つの要素が全てターミナルに出力されています。index変数の値はどこかで5という値になるものの、 配列から6番目の値を拾おうとする前にループは実行を終了します。

しかし、このアプローチは間違いが発生しやすいです; 添え字の長さが間違っていれば、 プログラムはパニックしてしまいます。また遅いです。 コンパイラが実行時にループの各回ごとに境界値チェックを行うようなコードを追加するからです。

より効率的な対立案として、forループを使ってコレクションの各アイテムに対してコードを実行することができます。 forループはリスト3-5のこんな見た目です。

ファイル名: src/main.rs

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a.iter() {
        // 値は{}です
        println!("the value is: {}", element);
    }
}

リスト3-4: forループを使ってコレクションの各要素を覗き見る

このコードを走らせたら、リスト3-4と同じ出力が得られるでしょう。より重要なのは、 コードの安全性を向上させ、配列の終端を超えてアクセスしたり、 終端に届く前にループを終えてアイテムを見逃してしまったりするバグの可能性を完全に排除したことです。

例えば、リスト3-4のコードで、a配列からアイテムを1つ削除したのに、条件式をwhile index < 4にするのを忘れていたら、 コードはパニックします。forループを使っていれば、配列の要素数を変えても、 他のコードをいじることを覚えておく必要はなくなるわけです。

forループのこの安全性と簡潔性により、Rustで使用頻度の最も高いループになっています。 リスト3-5で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はヒープ上に十分な大きさの空の領域を見つけ、使用中にし、ポインタを返してきます。ポインタとは、その場所へのアドレスです。 この過程は、ヒープに領域を確保すると呼ばれ、時としてそのフレーズを単にallocateするなどと省略したりします。 (訳注: こちらもこなれた日本語訳はないでしょう。allocateはメモリを確保すると訳したいところですが) スタックに値を載せることは、メモリ確保とは考えられません。ポインタは、既知の固定サイズなので、 スタックに保管することができますが、実データが必要になったら、ポインタを追いかける必要があります。

レストランで席を確保することを考えましょう。入店したら、グループの人数を告げ、 店員が全員座れる空いている席を探し、そこまで誘導します。もしグループの誰かが遅れて来るのなら、 着いた席の場所を尋ねてあなたを発見することができます。

ヒープへのデータアクセスは、スタックのデータへのアクセスよりも低速です。 ポインタを追って目的の場所に到達しなければならないからです。現代のプロセッサは、メモリをあちこち行き来しなければ、 より速くなります。似た例えを続けましょう。レストランで多くのテーブルから注文を受ける給仕人を考えましょう。最も効率的なのは、 次のテーブルに移らずに、一つのテーブルで全部の注文を受け付けてしまうことです。テーブルAで注文を受け、 それからテーブルBの注文、さらにまたA、それからまたBと渡り歩くのは、かなり低速な過程になってしまうでしょう。 同じ意味で、プロセッサは、 データが隔離されている(ヒープではそうなっている可能性がある)よりも近くにある(スタックではこうなる)ほうが、 仕事をうまくこなせるのです。ヒープに大きな領域を確保する行為も時間がかかることがあります。

コードが関数を呼び出すと、関数に渡された値(ヒープのデータへのポインタも含まれる可能性あり)と、 関数のローカル変数がスタックに載ります。関数の実行が終了すると、それらの値はスタックから取り除かれます。

どの部分のコードがどのヒープ上のデータを使用しているか把握すること、ヒープ上の重複するデータを最小化すること、 メモリ不足にならないようにヒープ上の未使用のデータを掃除することは全て、所有権が解決する問題です。 一度所有権を理解したら、あまり頻繁にスタックとヒープに関して考える必要はなくなるでしょうが、 ヒープデータを管理することが所有権の存在する理由だと知っていると、所有権がありのままで動作する理由を 説明するのに役立つこともあります。

所有権規則

まず、所有権のルールについて見ていきましょう。この規則を具体化する例を 扱っていく間もこれらのルールを肝に命じておいてください:

  • Rustの各値は、所有者と呼ばれる変数と対応している。
  • いかなる時も所有者は一つである。
  • 所有者がスコープから外れたら、値は破棄される。

変数スコープ

第2章で、Rustプログラムの例はすでに見ています。もう基本的な記法は通り過ぎたので、 fn main() {というコードはもう例に含みません。従って、例をなぞっているなら、 これからの例はmain関数に手動で入れ込まなければいけなくなるでしょう。結果的に、例は少々簡潔になり、 定型コードよりも具体的な詳細に集中しやすくなります。

所有権の最初の例として、何らかの変数のスコープについて見ていきましょう。スコープとは、 要素が有効になるプログラム内の範囲のことです。以下のような変数があるとしましょう:


# #![allow(unused_variables)]
#fn main() {
let s = "hello";
#}

変数sは、文字列リテラルを参照し、ここでは、文字列の値はプログラムのテキストとしてハードコードされています。 この変数は、宣言された地点から、現在のスコープの終わりまで有効になります。リスト4-1には、 変数sが有効な場所に関する注釈がコメントで付記されています。


# #![allow(unused_variables)]
#fn main() {
{                      // sは、ここでは有効ではない。まだ宣言されていない
    let s = "hello";   // sは、ここから有効になる

    // sで作業をする
}                      // このスコープは終わり。もうsは有効ではない
#}

リスト4-1: 変数と有効なスコープ

言い換えると、ここまでに重要な点は二つあります:

  1. sスコープに入ると、有効になる
  2. スコープを抜けるまで、有効なまま

ここで、スコープと変数が有効になる期間の関係は、他の言語に類似しています。さて、この理解のもとに、 String型を導入して構築していきましょう。

String

所有権の規則を具体化するには、第3章の「データ型」節で講義したものよりも、より複雑なデータ型が必要になります。 以前講義した型は全てスタックに保管され、スコープが終わるとスタックから取り除かれますが、 ヒープに確保されるデータ型を観察して、 コンパイラがどうそのデータを掃除すべきタイミングを把握しているかを掘り下げていきたいです。

ここでは、例としてString型を使用し、String型の所有権にまつわる部分に着目しましょう。 また、この観点は、標準ライブラリや自分で生成する他の複雑なデータ型にも適用されます。 String型については、第8章でより深く議論します。

すでに文字列リテラルは見かけましたね。文字列リテラルでは、文字列の値はプログラムにハードコードされます。 文字列リテラルは便利ですが、テキストを使いたいかもしれない場面全てに最適なわけではありません。一因は、 文字列リテラルが不変であることに起因します。別の原因は、コードを書く際に、全ての文字列値が判明するわけではないからです: 例えば、ユーザ入力を受け付け、それを保持したいとしたらどうでしょうか?このような場面用に、Rustには、 2種類目の文字列型、String型があります。この型はヒープにメモリを確保するので、 コンパイル時にはサイズが不明なテキストも保持することができるのです。from関数を使用して、 文字列リテラルからString型を生成できます。以下のように:


# #![allow(unused_variables)]
#fn main() {
let s = String::from("hello");
#}

この二重コロンは、string_fromなどの名前を使うのではなく、 String型直下のfrom関数を特定する働きをする演算子です。この記法について詳しくは、 第5章の「メソッド記法」節と、第7章の「モジュール定義」でモジュールを使った名前空間分けについて話をするときに議論します。

この種の文字列は、可変化することができます:


# #![allow(unused_variables)]
#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回解放してしまっても、バグになるわけです。 allocatefreeは完璧に1対1対応にしなければならないのです。

Rustは、異なる道を歩んでいます: ひとたび、メモリを所有している変数がスコープを抜けたら、 メモリは自動的に返還されます。こちらの例は、 リスト4-1のスコープ例を文字列リテラルからString型を使うものに変更したバージョンになります:


# #![allow(unused_variables)]
#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_variables)]
#fn main() {
let x = 5;
let y = x;
#}

リスト4-2: 変数xの整数値をyに代入する

もしかしたら、何をしているのか予想することができるでしょう: 「値5xに束縛する; それからxの値をコピーしてyに束縛する。」これで、 二つの変数(xy)が存在し、両方、値は5になりました。これは確かに起こっている現象を説明しています。 なぜなら、整数は既知の固定サイズの単純な値で、これら二つの5という値は、スタックに積まれるからです。

では、Stringバージョンを見ていきましょう:


# #![allow(unused_variables)]
#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から受け取った全メモリ量をバイトで表したものです。長さと許容量の違いは問題になることですが、 この文脈では違うので、とりあえずは、許容量を無視しても構わないでしょう。

s1s2に代入すると、String型のデータがコピーされます。つまり、スタックにあるポインタ、長さ、 許容量をコピーするということです。ポインタが指すヒープ上のデータはコピーしません。言い換えると、 メモリ上のデータ表現は図4-2のようになるということです。

同じ値を指すs1とs2

図4-2: s1のポインタ、長さ、許容量のコピーを保持する変数s2のメモリ上での表現

メモリ上の表現は、図4-3のようにはなりません。これは、 Rustが代わりにヒープデータもコピーするという選択をしていた場合のメモリ表現ですね。Rustがこれをしていたら、 ヒープ上のデータが大きい時にs2 = s1という処理の実行時性能がとても悪くなっていた可能性があるでしょう。

2箇所へのs1とs2

図4-3: Rustがヒープデータもコピーしていた場合にs2 = s1という処理が行なった可能性のあること

先ほど、変数がスコープを抜けたら、Rustは自動的にdrop関数を呼び出し、 その変数が使っていたヒープメモリを片付けると述べました。しかし、図4-4は、 両方のデータポインタが同じ場所を指していることを示しています。これは問題です: s2s1がスコープを抜けたら、 両方とも同じメモリを解放しようとします。これは二重解放エラーとして知られ、以前触れたメモリ安全性上のバグの一つになります。 メモリを2回解放することは、メモリの退廃につながり、さらにセキュリティ上の脆弱性を生む可能性があります。

メモリ安全性を保証するために、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と呼ばれる代わりに、 ムーブとして知られているわけです。この例では、s1s2ムーブされたと表現するでしょう。 以上より、実際に起きることを図4-4に示してみました。

s2にムーブされたs1

図4-4: s1が無効化された後のメモリ表現

これにて一件落着です。s2だけが有効なので、スコープを抜けたら、それだけがメモリを解放して、 終わりになります。

付け加えると、これにより暗示される設計上の選択があります: Rustでは、 自動的にデータの"deep copy"が行われることは絶対にないわけです。それ故に、あらゆる自動コピーは、実行時性能の観点で言うと、 悪くないと考えてよいことになります。

変数とデータの相互作用法: クローン

仮に、スタック上のデータだけでなく、本当にString型のヒープデータのdeep copyが必要ならば、 cloneと呼ばれるよくあるメソッドを使うことができます。メソッド記法については第5章で議論しますが、 メソッドは多くのプログラミング言語に見られる機能なので、以前に見かけたこともあるんじゃないでしょうか。

こちらで、cloneメソッドの稼働例をご覧ください:


# #![allow(unused_variables)]
#fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);
#}

これは単純にうまく動き、図4-3で示した動作を明示的に生み出します。ここでは、 ヒープデータが実際にコピーされています。

cloneメソッドの呼び出しを見かけたら、何らかの任意のコードが実行され、その実行コストは高いと把握できます。 何か違うことが起こっているなと見た目でわかるわけです。

スタックのみのデータ: コピー

まだ話題にしていない別のしわ(訳注: 「気になるもの」程度の意味と思われる)の話があります。 この整数を使用したコードは、一部をリスト4-2で示しましたが、うまく動作し、有効です:


# #![allow(unused_variables)]
#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など。
  • 論理値型、booltruefalseという値がある。
  • あらゆる浮動小数点型、f64など。
  • 文字型、char
  • タプル。ただ、Copyの型だけを含む場合。例えば、(i32, i32)Copyだが、 (i32, String)は違う。

所有権と関数

意味論的に、関数に値を渡すことと、値を変数に代入することは似ています。関数に変数を渡すと、 代入のようにムーブやコピーされます。リスト4-7は変数がスコープに入ったり、 抜けたりする地点について注釈してある例です。

ファイル名: 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を呼び出そうとすると、コンパイラは、コンパイルエラーを投げるでしょう。 これらの静的チェックにより、ミスを犯さないでいられます。sxを使用するコードを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番目に、&s1calcuate_lengthに渡し、その定義では、String型ではなく、&Stringを受け取っていることに注目してください。

これらのアンド記号が参照であり、これのおかげで所有権をもらうことなく値を参照することができるのです。 図4-5はその図解です。

文字列s1を指す&String型のs

図4-5: String s1を指す&Stringの図表

注釈: &による参照の逆は、参照外しであり、参照外し演算子の*で達成できます。 第8章で参照外し演算子の使用例を眺め、第15章で参照外しについて詳しく議論します。

ここの関数呼び出しについて、もっと詳しく見てみましょう:


# #![allow(unused_variables)]
#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_variables)]
#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");
}

始めに、smutに変えなければなりませんでした。そして、&mut sで可変な参照を生成し、 some_string: &mut Stringで可変な参照を受け入れなければなりませんでした。

ところが、可変な参照には大きな制約が一つあります: 特定のスコープである特定のデータに対しては、 一つしか可変な参照を持てないことです。こちらのコードは失敗します:

ファイル名: src/main.rs

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

これがエラーです:

error[E0499]: cannot borrow `s` as mutable more than once at a time
(エラー: 一度に`s`を可変として2回以上借用することはできません)
 --> borrow_twice.rs:5:19
  |
4 |     let r1 = &mut s;
  |                   - first mutable borrow occurs here
  |                    (最初の可変な参照はここ)
5 |     let r2 = &mut s;
  |                   ^ second mutable borrow occurs here
  |                    (二つ目の可変な参照はここ)
6 | }
  | - first borrow ends here
  |   (最初の借用はここで終わり)

この制約は、可変化を許可するものの、それを非常に統制の取れた形で行えます。これは、新たなRustaceanにとっては、 壁です。なぜなら、多くの言語では、いつでも好きな時に可変化できるからです。

この制約がある利点は、コンパイラがコンパイル時にデータ競合を防ぐことができる点です。 データ競合とは、競合条件と類似していて、これら3つの振る舞いが起きる時に発生します:

  • 2つ以上のポインタが同じデータに同時にアクセスする。
  • 少なくとも一つのポインタがデータに書き込みを行っている。
  • データへのアクセスを同期する機構が使用されていない。

データ競合は未定義の振る舞いを引き起こし、実行時に追いかけようとした時に特定し解決するのが難しい問題です。 しかし、Rustは、データ競合が起こるコードをコンパイルさえしないので、この問題が発生しないようにしてくれるわけです。

いつものように、波かっこを使って新しいスコープを生成し、同時並行なものでなく、複数の可変な参照を作ることができます。


# #![allow(unused_variables)]
#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_variables)]
#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


# #![allow(unused_variables)]
#fn main() {
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()
}
#}

リスト4-7: String引数へのバイト数で表された番号を返すfirst_word関数

Stringの値を要素ごとに見て、空白かどうかを確かめる必要があるので、 as_bytesメソッドを使って、Stringオブジェクトをバイト配列に変換しています。

let bytes = s.as_bytes();

次に、そのバイト配列に対して、iterメソッドを使用してイテレータを生成しています:

for (i, &item) in bytes.iter().enumerate() {

イテレータについて詳しくは、第13章で議論します。今は、iterは、コレクション内の各要素を返すメソッドであること、 enumerateiterの結果を包んで、代わりにタプルの一部として各要素を返すことを知っておいてください。 enumerateから返ってくるタプルの第1要素は、番号であり、2番目の要素は、(コレクションの)要素への参照になります。 これは、手動で番号を計算するよりも少しだけ便利です。

enumerateメソッドがタプルを返すので、Rustのあらゆる場所同様、パターンを使って、そのタプルを分解できます。 従って、forループ内で、タプルの番号に対するiとタプルの1バイトに対応する&itemを含むパターンを指定しています。 .iter().enumerate()から要素への参照を取得するので、パターンに&を使っています。

forループ内で、バイトリテラル表記を使用して空白を表すバイトを検索しています。空白が見つかったら、その位置を返します。 それ以外の場合、s.len()を使って文字列の長さを返します。

    if item == b' ' {
        return i;
    }
}

s.len()

さて、文字列内の最初の単語の終端の番号を見つけ出せるようになりましたが、問題があります。 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の中身は、値5になる

    s.clear(); // Stringを空にする。つまり、""と等しくする

    // wordはまだ値5を保持しているが、もうこの値を有効に使用できる文字列は存在しない。
    // wordは完全に無効なのだ!
}

リスト4-8: first_word関数の呼び出し結果を保持し、Stringの中身を変更する

このプログラムは何のエラーもなくコンパイルが通り、words.clear()の呼び出し後に使用しても、 コンパイルが通ります。wordsの状態に全く関連づけられていないので、その中身はまだ値5のままです。 その値5を変数sに使用し、最初の単語を取り出そうとすることはできますが、これはバグでしょう。 というのも、sの中身は、5wordに保存してから変わってしまったからです。

word内の番号がsに格納されたデータと同期されなくなるのを心配することは、面倒ですし間違いになりやすいです! これらの番号を管理するのは、second_word関数を書いたら、さらに脆くなります。 そのシグニチャは以下のようにならなければおかしいです:

fn second_word(s: &String) -> (usize, usize) {

今、私たちは開始終端の番号を追うようになりました。特定の状態のデータから計算されたけど、 その状態に全く紐付かない値が増えました。同期を取る必要のある宙に浮いた関連性のない変数が3つになってしまいました。

運のいいことに、Rustにはこの問題への解決策が用意されています: 文字列スライスです。

文字列スライス

文字列スライスとは、Stringの一部への参照で、こんな見た目をしています:


# #![allow(unused_variables)]
#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];の場合には、worldsの7バイト目へのポインタと5という長さを保持するスライスになるでしょう。

図4-6は、これを図解しています。

文字列sの6バイト目へのポインタと長さ5を保持するworld

図4-6: Stringオブジェクトの一部を参照する文字列スライス

Rustの..という範囲記法で、最初の番号(ゼロ)から始めたければ、2連ピリオドの前に値を書かなければいいのです。 換言すれば、これらは等価です:


# #![allow(unused_variables)]
#fn main() {
let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];
#}

同様の意味で、Stringの最後のバイトをスライスが含むのならば、末尾の数値を書かなければいいのです。 つまり、これらは等価になります:


# #![allow(unused_variables)]
#fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];
#}

さらに、両方の値を省略すると、文字列全体のスライスを得られます。故に、これらは等価です:


# #![allow(unused_variables)]
#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


# #![allow(unused_variables)]
#fn main() {
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[..]
}
#}

リスト4-7で取った手段と同じ方法で単語の終端番号を取得しています。つまり、最初の空白を探すことです。 空白を発見したら、文字列の最初と、空白の番号を開始、終了地点として使用して文字列スライスを返しています。

これで、first_wordを呼び出すと、元のデータに紐付けられた単独の値を得られるようになりました。 この値は、スライスの開始地点への参照とスライス中の要素数から構成されています。

second_word関数についても、スライスを返すことでうまくいくでしょう:

fn second_word(s: &String) -> &str {

これで、ずっと混乱しにくい素直なAPIになりました。なぜなら、Stringへの参照が有効なままであることをコンパイラが、 保証してくれるからです。最初の単語の終端番号を得た時に、 文字列を空っぽにして先ほどの番号が無効になってしまったリスト4-8のプログラムのバグを覚えていますか? そのコードは、論理的に正しくないのですが、即座にエラーにはなりませんでした。問題は後になってから発生し、 それは空の文字列に対して、最初の単語の番号を使用し続けようとした時でした。スライスならこんなバグはあり得ず、 コードに問題があるなら、もっと迅速に判明します。スライスバージョンのfirst_wordを使用すると、 コンパイルエラーが発生します:

ファイル名: src/main.rs

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // error!    (エラー!)
}

こちらがコンパイルエラーです:

error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
(エラー: 不変として借用されているので、`s`を可変で借用できません)
 --> src/main.rs:6:5
  |
4 |     let word = first_word(&s);
  |                            - immutable borrow occurs here (不変借用はここで起きています)
5 |
6 |     s.clear(); // error!        (エラー!)
  |     ^ mutable borrow occurs here (可変借用はここで起きています)
7 | }
  | - immutable borrow ends here (不変借用はここで終わっています)

借用規則から、何かへの不変な参照がある時、さらに可変な参照を得ることはできないことを思い出してください。 clearStringを切り詰める必要があるので、可変な参照を得ようとして失敗しているわけです。 RustのおかげでAPIが使いやすくなるだけでなく、ある種のエラー全てを完全にコンパイル時に排除してくれるのです!

文字列リテラルはスライスである

文字列は、バイナリに埋め込まれると話したことを思い出してください。今やスライスのことを知ったので、 文字列リテラルを正しく理解することができます。


# #![allow(unused_variables)]
#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 {

リスト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は`String`のスライスに対して機能する
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_wordは文字列リテラルのスライスに対して機能する
    let word = first_word(&my_string_literal[..]);

    // 文字列リテラルは、すでに文字列スライス*な*ので、
    // スライス記法なしでも機能するのだ!
    let word = first_word(my_string_literal);
}

他のスライス

文字列リテラルは、想像通り、文字列に特化したものです。ですが、もっと一般的なスライス型も存在します。 この配列を考えてください:


# #![allow(unused_variables)]
#fn main() {
let a = [1, 2, 3, 4, 5];
#}

文字列の一部を参照したくなる可能性があるのと同様、配列の一部を参照したくなる可能性もあります。 以下のようにすれば、参照することができます:


# #![allow(unused_variables)]
#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_variables)]
#fn main() {
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}
#}

リスト5-1: User構造体定義

構造体を定義した後に使用するには、各フィールドに対して具体的な値を指定して構造体のインスタンスを生成します。 インスタンスは、構造体名を記述し、key: valueペアを含む波かっこを付け加えることで生成します。 ここで、キーはフィールド名、値はそのフィールドに格納したいデータになります。フィールドは、 構造体で宣言した通りの順番に指定する必要はありません。換言すると、構造体定義とは、 型に対する一般的な雛形のようなものであり、インスタンスは、その雛形を特定のデータで埋め、その型の値を生成するわけです。 例えば、リスト5-2で示されたように特定のユーザを宣言することができます。


# #![allow(unused_variables)]
#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_variables)]
#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_variables)]
#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関数

構造体のフィールドと同じ名前を関数の引数にもつけることは筋が通っていますが、 emailusernameというフィールド名と変数を繰り返さなきゃいけないのは、ちょっと面倒です。 構造体にもっとフィールドがあれば、名前を繰り返すことはさらに煩わしくなるでしょう。 幸運なことに、便利な省略記法があります!

フィールドと変数が同名の時にフィールド初期化省略記法を使う

仮引数名と構造体のフィールド名がリスト5-4では、全く一緒なので、フィールド初期化省略記法を使ってbuild_userを書き換えても、 振る舞いは全く同じにしつつ、リスト5-5に示したようにemailusernameを繰り返さなくてもよくなります。


# #![allow(unused_variables)]
#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: emailusername引数が構造体のフィールドと同名なので、 フィールド初期化省略法を使用するbuild_user関数

ここで、emailというフィールドを持つUser構造体の新規インスタンスを生成しています。 emailフィールドをbuild_user関数のemail引数の値にセットしたいわけです。 emailフィールドとemail引数は同じ名前なので、email: emailと書くよりも、 emailと書くだけで済むのです。

構造体更新記法で他のインスタンスからインスタンスを生成する

多くは前のインスタンスの値を使用しつつ、変更する箇所もある形で新しいインスタンスを生成できるとしばしば有用です。 構造体更新記法でそうすることができます。

まず、リスト5-6では、更新記法なしでuser2に新しいUserインスタンスを生成する方法を示しています。 emailusernameには新しい値をセットしていますが、それ以外にはリスト5-2で生成したuser1の値を使用しています。


# #![allow(unused_variables)]
#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_variables)]
#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インスタンス用の値に新しいemailusernameをセットしつつ、 残りの値は、user1変数のフィールド値を使う

リスト5-7のコードも、emailusernameについては異なる値、activesign_in_countフィールドについては、 user1と同じ値になるインスタンスをuser2に生成します。

異なる型を生成する名前付きフィールドのないタプル構造体を使用する

構造体名により追加の意味を含むものの、フィールドに紐づけられた名前がなく、むしろフィールドの型だけのタプル構造体と呼ばれる、 タプルに似た構造体を定義することもできます。タプル構造体は、構造体名が提供する追加の意味は含むものの、 フィールドに紐付けられた名前はありません; むしろ、フィールドの型だけが存在します。タプル構造体は、タプル全体に名前をつけ、 そのタプルを他のタプルとは異なる型にしたい場合に有用ですが、普通の構造体のように各フィールド名を与えるのは、 冗長、または余計になるでしょう。

タプル構造体を定義するには、structキーワードの後に構造体名、さらにタプルに含まれる型を続けます。 例えば、こちらは、ColorPointという2種類のタプル構造体の定義と使用法です:


# #![allow(unused_variables)]
#fn main() {
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
#}

blackoriginの値は、違う型であることに注目してください。これらは、異なるタプル構造体のインスタンスだからですね。 定義された各構造体は、構造体内のフィールドが同じ型であっても、それ自身が独自の型になります。 例えば、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という名前にしています。波括弧の中でwidthheightというフィールドを定義し、 u32という型にしました。それからmain内でRectangleの特定のインスタンスを生成し、 幅を30、高さを50にしました。

これでarea関数は引数が一つになり、この引数は名前がrectangle、型はRectangle構造体インスタンスへの不変借用になりました。 第4章で触れたように、構造体の所有権を奪うよりも借用する必要があります。こうすることでmainは所有権を保って、 rect1を使用し続けることができ、そのために関数シグニチャと関数呼び出し時に&を使っているわけです。

area関数は、Rectangleインスタンスのwidthheightフィールドにアクセスしています。 これで、areaの関数シグニチャは、我々の意図をズバリ示すようになりました: widthheightフィールドを使って、 Rectangleの面積を計算します。これにより、幅と高さが相互に関係していることが伝わり、 タプルの01という添え字を使うのではなく、値に説明的な名前を与えられるのです。簡潔性を勝ち取ったわけですね。

トレイトの継承で有用な機能を追加する

プログラムのデバッグをし、フィールドの値を調べている間に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_variables)]
#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番目のRectangleselfに完全にはめ込まれたら、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よりも小さいものの、 rect3rect1より幅が広いからです:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

メソッドを定義したいことはわかっているので、impl Rectangleブロック内での話になります。 メソッド名は、can_holdになり、引数として別のRectangleを不変借用で取るでしょう。 メソッドを呼び出すコードを見れば、引数の型が何になるかわかります: rect1.can_hold(&rect2)は、 &rect2Rectangleのインスタンスであるrect2への不変借用を渡しています。 これは道理が通っています。なぜなら、rect2を読み込む(書き込みではなく。この場合、可変借用が必要になります)だけでよく、 can_holdメソッドを呼び出した後にもrect2が使えるよう、所有権をmainに残したままにしたいからです。 can_holdの返り値は、論理型になり、メソッドの中身は、selfの幅と高さがもう一つのRectangleの幅と高さよりも、 それぞれ大きいことを確認します。リスト5-13のimplブロックに新しいcan_holdメソッドを追記しましょう。 リスト5-15に示した通りです。

ファイル名: src/main.rs


# #![allow(unused_variables)]
#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_variables)]
#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_variables)]
#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アドレスがなりうる種類、V4V6を列挙することで、 表現できます。これらは、enumの列挙子として知られています:


# #![allow(unused_variables)]
#fn main() {
enum IpAddrKind {
    V4,
    V6,
}
#}

これで、IpAddrKindはコードの他の場所で使用できる独自のデータ型になります。

Enumの値

以下のようにして、IpAddrKindの各バリアントのインスタンスは生成できます:


# #![allow(unused_variables)]
#fn main() {
# enum IpAddrKind {
#     V4,
#     V6,
# }
#
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
#}

enumの列挙子は、その識別子の元に名前空間分けされていることと、 2連コロンを使ってその二つを区別していることに注意してください。 これが有効な理由は、こうすることで、値IpAddrKind::V4IpAddrKind::V6という値は両方とも、 同じ型IpAddrKindになったからです。そうしたら、例えば、どんなIpAddrKindを取る関数も定義できるようになります。


# #![allow(unused_variables)]
#fn main() {
# enum IpAddrKind {
#     V4,
#     V6,
# }
#
fn route(ip_type: IpAddrKind) { }
#}

そして、この関数をどちらのバリアントに対しても呼び出せます:


# #![allow(unused_variables)]
#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_variables)]
#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が紐付いています。構造体を使ってkindaddress値を一緒に包んだので、 もう列挙子は値と紐付けられています。

各enumの列挙子に直接データを格納して、enumを構造体内に使うというよりもenumだけを使って、 同じ概念をもっと簡潔な方法で表現することができます。この新しいIpAddrの定義は、 V4V6列挙子両方にString値が紐付けられていることを述べています。


# #![allow(unused_variables)]
#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_variables)]
#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_variables)]
#fn main() {
struct Ipv4Addr {
    // 省略
}

struct Ipv6Addr {
    // 省略
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}
#}

このコードは、enum列挙子内にいかなる種類のデータでも格納できることを描き出しています: 例を挙げれば、文字列、数値型、構造体などです。他のenumを含むことさえできます!また、 標準ライブラリの型は、あなたが思い付いたよりも複雑ではないことがしばしばあります。

標準ライブラリにIpAddrに対する定義は含まれるものの、標準ライブラリの定義をスコープに導入していないので、 まだ、干渉することなく自分自身の定義を生成して使用できることに注意してください。型をスコープに導入することについては、 第7章でもっと詳しく言及します。

リスト6-2でenumの別の例を見てみましょう: 今回のコードは、幅広い種類の型が列挙子に埋め込まれています。


# #![allow(unused_variables)]
#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_variables)]
#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_variables)]
#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"では、こんなことが語られています。

私はそれを10億ドルの失敗と呼んでいます。その頃、私は、オブジェクト指向言語の参照に対する、 最初のわかりやすい型システムを設計していました。私の目標は、 どんな参照の使用も全て完全に安全であるべきことを、コンパイラにそのチェックを自動で行ってもらって保証することだったのです。 しかし、null参照を入れるという誘惑に打ち勝つことができませんでした。それは、単純に実装が非常に容易だったからです。 これが無数のエラーや脆弱性、システムクラッシュにつながり、過去40年で10億ドルの苦痛や損害を引き起こしたであろうということなのです。

null値の問題は、nullの値をnullでない値のように使用しようとしたら、何らかの種類のエラーが出ることです。 このnullかそうでないかという特性は広く存在するので、この種の間違いを大変犯しやすいのです。

しかしながら、nullが表現しようとしている概念は、それでも役に立つものです: nullは、 何らかの理由で現在無効、または存在しない値のことなのです。

問題は、全く概念にあるのではなく、特定の実装にあるのです。そんな感じなので、Rustにはnullがありませんが、 値が存在するか不在かという概念をコード化するenumならあります。このenumがOption<T>で、 以下のように標準ライブラリに定義されています。


# #![allow(unused_variables)]
#fn main() {
enum Option<T> {
    Some(T),
    None,
}
#}

Option<T>は有益すぎて、初期化処理(prelude)にさえ含まれています。つまり、明示的にスコープに導入する必要がないのです。 さらに、列挙子もそうなっています: SomeNoneOption::の接頭辞なしに直接使えるわけです。 ただ、Option<T>はそうは言っても、普通のenumであり、Some(T)NoneOption<T>型のただの列挙子です。

<T>という記法は、まだ語っていないRustの機能です。これは、ジェネリック型引数であり、ジェネリクスについて詳しくは、 第10章で解説します。とりあえず、知っておく必要があることは、<T>は、Option enumのSome列挙子が、 あらゆる型のデータを1つだけ持つことができることを意味していることだけです。こちらは、 Option値を使って、数値型や文字列型を保持する例です。


# #![allow(unused_variables)]
#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>値を確実に有効な値かのようには使用させてくれません。 例えば、このコードはi8Option<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>`
  |

なんて強烈な!実際に、このエラーメッセージは、i8Option<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_variables)]
#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_variables)]
#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_variables)]
#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_variables)]
#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))と呼び出すつもりだったなら、coinCoin::Quarter(UsState::Alaska)になります。その値をmatchの各アームと比較すると、 Coin::Quater(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_variables)]
#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の本体の変数xSome(5)になります。そして、これをマッチの各アームと比較します。

None => None,

Some(5)という値は、Noneというパターンにはマッチしませんので、次のアームに処理が移ります。

Some(i) => Some(i + 1),

Some(5)Some(i)にマッチしますか?えっと、します!列挙子が同じです。iSomeに含まれる値に束縛されるので、 iは値5になります。それから、このマッチのアームのコードが実行されるので、iの値に1を足し、 合計の6を中身にした新しいSome値を生成します。

さて、xNoneになるリスト6-5の2回目のplus_oneの呼び出しを考えましょう。matchに入り、 最初のアームと比較します。

None => None,

マッチします!足し算する値がないので、プログラムは停止し、=>の右辺にあるNone値が返ります。 最初のアームがマッチしたため、他のアームは比較されません。

matchとenumの組み合わせは、多くの場面で有効です。Rustコードにおいて、このパターンはよく見かけるでしょう: enumに対しmatchし、内部のデータに変数を束縛させ、それに基づいたコードを実行します。最初はちょっと巧妙ですが、 一旦慣れてしまえば、全ての言語にあってほしいと願うことになるでしょう。一貫してユーザのお気に入りなのです。

マッチは包括的

もう一つ議論する必要のあるmatchの観点があります。1点バグがありコンパイルできないこんなバージョンの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_variables)]
#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記法でifletをより冗長性の少ない方法で組み合わせ、残りを無視しつつ、一つのパターンにマッチする値を扱うことができます。 Option<u8>にマッチするけれど、値が3の時にだけコードを実行したい、リスト6-6のプログラムを考えてください。


# #![allow(unused_variables)]
#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_variables)]
#fn main() {
# let some_u8_value = Some(0u8);
if let Some(3) = some_u8_value {
    println!("three");
}
#}

if letという記法は等号記号で区切られたパターンと式を取り、式がmatchに与えられ、パターンが最初のアームになったmatchと、 同じ動作をします。

if letを使うと、タイプ数が減り、インデントも少なくなり、定型コードも減ります。しかしながら、 matchでは強制された包括性チェックを失ってしまいます。matchif letかの選択は、 特定の場面でどんなことをしたいかと簡潔性を得ることが包括性チェックを失うのに適切な代償となるかによります。

言い換えると、if letは値が一つのパターンにマッチした時にコードを走らせ、 他は無視するmatchへの糖衣構文と考えることができます。

if letでは、elseを含むこともできます。elseに入るコードブロックは、 if letelseに等価なmatch式の_の場合に入るコードブロックと同じになります。 リスト6-4のCoin enum定義を思い出してください。ここでは、Quarter列挙子は、 UsStateの値も保持していましたね。クォーターコインの状態を告げつつ、 見かけたクォーター以外のコインの枚数を数えたいなら、以下のようにmatch式で実現することができるでしょう:


# #![allow(unused_variables)]
#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 letelseを使うこともできるでしょう:


# #![allow(unused_variables)]
#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の値がデータを内部に含む場合、処理すべきケースの数に応じて、matchif letを使用して値を取り出し、 使用できます。

もうRustプログラムで構造体とenumを使用して、自分の領域の概念を表現できます。API内で使用するために独自の型を生成することで、 型安全性を保証することができます: コンパイラが、各関数の予期する型の値のみを関数が得ることを確かめてくれるのです。

使用するのに率直な整理整頓されたAPIをユーザに提供し、ユーザが必要とするものだけを公開するために、 今度は、Rustのモジュールに目を向けてみましょう。

モジュールを使用してコードを体系化し、再利用する

Rustでのプログラミングをし始めた頃は、コードは全てmain関数内に収まったかもしれません。コードが肥大化するにつれ、 最終的に機能を別の関数に移して再利用性とまとまりを高めるでしょう。コードを細切りにすることで、 個々のコード片をそれだけで理解しやすくします。しかし、あまりにも多くの関数があったらどうなるでしょうか? Rustには、コードの再利用を体系化された形で行うことのできるモジュールシステムが組み込まれています。

コードを関数に抽出するのと同様に、関数(や他のコード、構造体やenumなど)を異なるモジュールに抽出することができます。 モジュールとは、関数や型定義を含む名前空間のことで、それらの定義がモジュール外からも見えるようにするか(public)否か(private)は、 選択することができます。以下が、モジュールの動作法の概要です:

  • modキーワードで新規モジュールを宣言します。モジュール内のコードは、この宣言の直後の波かっこ内か、 別のファイルに存在します。
  • 標準では、関数、型、定数、モジュールは非公開です。pubキーワードで要素は公開され、 名前空間の外からも見えるようになります。
  • useキーワードでモジュールやモジュール内の定義をスコープに入れることができるので、 参照するのが楽になります。

この各部品を見て、それらが全体にどうはまり込むかを理解します。

modとファイルシステム

モジュールの例をCargoで新規プロジェクトを生成することから始めるが、バイナリクレートの代わりに、 ライブラリクレートを作成します: 他人が依存として自分のプロジェクトに引き込めるプロジェクトです。 例を挙げると、第2章で議論したrandクレートは、数当てゲームプロジェクトで依存に使用したライブラリクレートです。

何らかの一般的なネットワーク機能を提供するライブラリの骨格を作成します; モジュールと関数の体系化に集中し、 関数の本体にどんなコードが入るかについては気にかけません。このライブラリをcommunicatorと呼びましょう。 ライブラリを生成するために、--binの代わりに--libオプションを渡してください:

$ cargo new communicator --lib
$ cd communicator

Cargoがsrc/main.rsの代わりにsrc/lib.rsを生成したことに注目してください。src/lib.rsには、 以下のような記述があります:

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}
#}

Cargoは、--binオプションを使った時に得られる"Hello, world!"バイナリではなく、空のテストを生成して、 ライブラリの事始めをしてくれました。#[]mod testsという記法については、この章の後ほど、 「superを使用して親モジュールにアクセスする」節で見ますが、今のところは、 このコードをsrc/lib.rsの最後に残しておきましょう。

src/main.rsファイルがないので、cargo runコマンドでCargoが実行できるものは何もないわけです。 従って、cargo buildコマンドを使用してライブラリクレートのコードをコンパイルします。

コードの意図によって、いろんなシチュエーションで最適になるライブラリコードを体系化する別のオプションをお目にかけます。

モジュール定義

communicatorネットワークライブラリについて、まずはconnectという関数定義を含むnetworkという名前のモジュールを定義します。 Rustにおいて、モジュール定義は全て、modキーワードから開始します。このコードをsrc/lib.rsファイルの頭、 テストコードの上に追記してください。

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
mod network {
    fn connect() {
    }
}
#}

modキーワードに続いて、モジュール名のnetwork、さらに一連のコードを波かっこ内に記述します。 このブロック内に存在するものは全て、networkという名前空間に属します。今回の場合、 connectという単独の関数があります。この関数をnetworkモジュール外のスクリプトから呼び出したい場合、 モジュールを指定し、以下のように名前空間記法の::を使用する必要があるでしょう: network::connect()

同じsrc/lib.rsファイル内に複数のモジュールを並べることもできます。例として、 connectという関数を含むclientモジュールも用意するには、リスト7-1に示したように追記すればいいわけです。

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
mod network {
    fn connect() {
    }
}

mod client {
    fn connect() {
    }
}
#}

リスト7-1: src/lib.rsに並べて定義されたnetworkモジュールとclientモジュール

これで、network::connect関数とclient::connect関数が用意できました。これらは全く異なる機能を有する可能性があり、 異なるモジュールに存在するので、関数名がお互いに衝突することはありません。

今回の場合、ライブラリを構成しているので、ライブラリビルド時にエントリーポイントとなるファイルは、 src/lib.rsになります。しかし、モジュールを作成するという点に関しては、src/lib.rsには何も特別なことはありません。 ライブラリクレートに対してsrc/lib.rsにモジュールを生成するのと全く同様に、 バイナリクレートに対してsrc/main.rsにモジュールを生成することもできます。実は、モジュール内にモジュールを書くこともでき、 モジュールが肥大化するにつれて、関連のある機能を一緒くたにし、機能を切り離すのに有用なのです。 コードを体系化すると選択する方法は、コードの部分部分の関連性に対する考え方によって選択することになります。 例ですが、clientコードとそのconnect関数は、リスト7-2のように、代わりにnetwork名前空間内に存在したら、 ライブラリの使用者にとって意味のあるものになるかもしれません。

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
mod network {
    fn connect() {
    }

    mod client {
        fn connect() {
        }
    }
}
#}

リスト7-2: clientモジュールをnetworkモジュール内に移動させる

src/lib.rsファイル内で、すでにあるmod networkmod clientの定義をリスト7-2のものと置き換えると、 clientモジュールはnetworkの内部モジュールになるわけです。関数、 network::connectnetwork::client::connectはどちらもconnectという名前ですが、 異なる名前空間にあるので、互いに干渉することはありません。

このように、モジュールは階層構造を形成します。src/lib.rsの中身が頂点に立ち、サブモジュールが子供になるわけです。 リスト7-1の例を階層構造という観点で見たときの構造は、以下のような感じになります:

communicator
 ├── network
 └── client

さらに、リスト7-2の例に対応する階層構造は、以下の通りです:

communicator
 └── network
     └── client

この階層構造は、リスト7-2において、clientモジュールはnetworkモジュールの兄弟というよりも、子供になっていることを示しています。 より複雑なプロジェクトなら、たくさんのモジュールが存在し、把握するのに論理的に体系化しておく必要があるでしょう。 プロジェクト内で「論理的」とは、あなた次第であり、ライブラリ作成者と使用者がプロジェクトの領域についてどう考えるか次第でもあるわけです。 こちらで示したテクニックを使用して、並列したモジュールや、ネストしたモジュールなど、どんな構造のモジュールでも、 作成してください。

モジュールを別ファイルに移す

モジュールは階層構造をなす……コンピュータにおいて、もっと見慣れた構造に似ていませんか: そう、ファイルシステムです! Rustのモジュールシステムを複数のファイルで使用して、プロジェクトを分割するので、 全部がsrc/lib.rssrc/main.rsに存在することにはならなくなります。これの例として、 リスト7-3のようなコードから始めましょう。

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
mod client {
    fn connect() {
    }
}

mod network {
    fn connect() {
    }

    mod server {
        fn connect() {
        }
    }
}
#}

リスト7-3: 全てsrc/lib.rsに定義された三つのモジュール、 clientnetworknetwork::server

src/lib.rsファイルのモジュール階層は、こうなっています:

communicator
 ├── client
 └── network
     └── server

これらのモジュールが多数の関数を含み、その関数が長ったらしくなってきたら、このファイルをスクロールして、 弄りたいコードを探すのが困難になるでしょう。関数が一つ以上のmodブロックにネストされているので、 関数の中身となるコードも長ったらしくなってしまうのです。これだけで、clientnetworkserverモジュールをsrc/lib.rsから分け、 単独のファイルに配置するには十分でしょう。

最初に、clientモジュールのコードをclientモジュールの宣言だけに置き換えましょう。 すると、src/lib.rsはリスト7-4のコードのようになります。

ファイル名: src/lib.rs

mod client;

mod network {
    fn connect() {
    }

    mod server {
        fn connect() {
        }
    }
}

リスト7-4: clientモジュールの中身を抽出するが、宣言はsrc/lib.rsに残したまま

一応、clientモジュールをここで宣言していますが、ブロックをセミコロンで置換したことで、 clientモジュールのスコープのコードは別の場所を探すようにコンパイラに指示しているわけです。 言い換えると、mod client;の行は、以下のような意味になります:

mod client {
    // contents of client.rs
}

さて、このモジュール名の外部ファイルを作成する必要が出てきました。src/ディレクトリ内にclient.rsファイルを作成し、 開いてください。それから以下のように入力してください。前段階で削除したclientモジュールのconnect関数です:

ファイル名: src/client.rs


# #![allow(unused_variables)]
#fn main() {
fn connect() {
}
#}

このファイルには、mod宣言が必要ないことに着目してください。なぜなら、src/lib.rsmodを使って、 もうclientモジュールを宣言しているからです。このファイルは、clientモジュールの中身を提供するだけなのです。 ここにもmod clientを記述したら、clientclientという名前のサブモジュールを与えることになってしまいます!

コンパイラは、標準でsrc/lib.rsだけを検索します。プロジェクトにもっとファイルを追加したかったら、 src/lib.rsで他のファイルも検索するよう、コンパイラに指示する必要があるのです; このため、mod clientsrc/lib.rsに定義し、 src/client.rsには定義できなかったのです。

これでプロジェクトは問題なくコンパイルできるはずです。まあ、警告がいくつか出るんですが。 cargo runではなく、cargo buildを使うことを忘れないでください。バイナリクレートではなく、 ライブラリクレートだからですね:

$ cargo build
   Compiling communicator v0.1.0 (file:///projects/communicator)

warning: function is never used: `connect`
(警告: 関数は使用されていません: `connect`)
 --> src/client.rs:1:1
  |
1 | / fn connect() {
2 | | }
  | |_^
  |
  = note: #[warn(dead_code)] on by default

warning: function is never used: `connect`
 --> src/lib.rs:4:5
  |
4 | /     fn connect() {
5 | |     }
  | |_____^

warning: function is never used: `connect`
 --> src/lib.rs:8:9
  |
8 | /         fn connect() {
9 | |         }
  | |_________^

これらの警告は、全く使用されていない関数があると忠告してくれています。今は、警告を危惧する必要はありません; この章の後ほど、「pubで公開するか制御する」節で特定します。嬉しいことにただの警告です; プロジェクトはビルドに成功しました!

次に、同様のパターンを使用してnetworkモジュールも単独のファイルに抽出しましょう。 src/lib.rsで、networkモジュールの本体を削除し、宣言にセミコロンを付加してください。こんな感じです:

ファイル名: src/lib.rs

mod client;

mod network;

それから新しいsrc/network.rsファイルを作成して、以下のように入力してください:

ファイル名: src/network.rs


# #![allow(unused_variables)]
#fn main() {
fn connect() {
}

mod server {
    fn connect() {
    }
}
#}

このモジュールファイル内にもまだmod宣言があることに気付いてください; serverはまだnetworkのサブモジュールにしたいからです。

再度cargo buildしてください。成功!抽出すべきモジュールがもう1個あります: serverです。 これはサブモジュール(つまり、モジュール内のモジュール)なので、 モジュール名に倣ったファイルにモジュールを抽出するという今の手法は、通用しません。いずれにしても、 エラーが確認できるように、試してみましょう。まず、src/network.rsファイルをserverモジュールの中身を含む代わりに、 mod server;となるように変更してください。

ファイル名: src/network.rs

fn connect() {
}

mod server;

そして、src/server.rsファイルを作成し、抽出したserverモジュールの中身を入力してください:

ファイル名: src/server.rs


# #![allow(unused_variables)]
#fn main() {
fn connect() {
}
#}

cargo buildを実行しようとすると、リスト7-4に示したようなエラーが出ます:

$ cargo build
   Compiling communicator v0.1.0 (file:///projects/communicator)
error: cannot declare a new module at this location
(エラー: この箇所では新規モジュールを宣言できません)
 --> src/network.rs:4:5
  |
4 | mod server;
  |     ^^^^^^
  |
note: maybe move this module `src/network.rs` to its own directory via `src/network/mod.rs`
(注釈: もしかして、`src/network.rs`というこのモジュールを`src/network/mod.rs`経由で独自のディレクトリに移すの)
 --> src/network.rs:4:5
  |
4 | mod server;
  |     ^^^^^^
note: ... or maybe `use` the module `server` instead of possibly redeclaring it
(注釈: それとも、再度宣言する可能性はなく、`server`というモジュールを`use`したの)
 --> src/network.rs:4:5
  |
4 | mod server;
  |     ^^^^^^

リスト7-5: serverサブモジュールをsrc/server.rsに抽出しようとしたときのエラー

エラーは、この箇所では新規モジュールを宣言できませんと忠告し、src/network.rsmod server;行を指し示しています。 故に、src/network.rsは、src/lib.rsと何かしら違うのです: 理由を知るために読み進めましょう。

リスト7-5の真ん中の注釈は、非常に有用です。というのも、まだ話題にしていないことを指摘しているからです。

note: maybe move this module `network` to its own directory via
`network/mod.rs`

以前行ったファイル命名パターンに従い続けるのではなく、注釈が提言していることをすることができます:

  1. 親モジュール名であるnetworkという名前の新規ディレクトリを作成する。
  2. src/network.rsファイルをnetworkディレクトリに移し、 src/network/mod.rsと名前を変える。
  3. サブモジュールファイルのsrc/server.rsnetworkディレクトリに移す。

以下が、これを実行するコマンドです:

$ mkdir src/network
$ mv src/network.rs src/network/mod.rs
$ mv src/server.rs src/network

cargo buildを走らせたら、ようやくコンパイルは通ります(まだ警告はありますけどね)。 それでも、モジュールの配置は、リスト7-3でsrc/lib.rsに全てのコードを収めていたときと全く同じになります:

communicator
 ├── client
 └── network
     └── server

対応するファイルの配置は、以下のようになっています:

└── src
    ├── client.rs
    ├── lib.rs
    └── network
        ├── mod.rs
        └── server.rs

では、network::serverモジュールを抽出したかったときに、 なぜ、src/network.rsファイルをsrc/network/mod.rsファイルに変更し、 network::serverのコードをnetworkディレクトリ内のsrc/network/server.rsに置かなければならなかったのでしょうか? なぜ、単にnetwork::serverモジュールをsrc/server.rsに抽出できなかったのでしょうか? 理由は、server.rsファイルがsrcディレクトリにあると、コンパイラが、 servernetworkのサブモジュールと考えられることを検知できないからです。 ここでのコンパイラの動作をはっきりさせるために、以下のようなモジュール階層をもつ別の例を考えましょう。 こちらでは、定義は全てsrc/lib.rsに存在します。

communicator
 ├── client
 └── network
     └── client

この例でも、モジュールは3つあります: clientnetworknetwork::clientです。 以前と同じ段階を経てモジュールをファイルに抽出すると、clientモジュール用にsrc/client.rsを作成することになるでしょう。 networkモジュールに関しては、src/network.rsを作成します。しかし、 network::clientモジュールをsrc/client.rsファイルに抽出することはできません。 もうトップ階層のclientモジュールとして存在するからです! clientnetwork::client双方のコードをsrc/client.rsファイルに書くことができたら、 コンパイラは、コードがclient用なのか、network::client用なのか知る術を失ってしまいます。

従って、networkモジュールのnetwork::clientサブモジュールをファイルに抽出するには、 src/network.rsファイルではなく、networkモジュールのディレクトリを作成する必要があったわけです。 そうすれば、networkモジュールのコードは、src/network/mod.rsファイルに移ることになり、 network::clientというサブモジュールは専用のsrc/network/client.rsファイルを持てるわけです。 これで、頂点にあるsrc/client.rsは間違いなく、clientモジュールに属するコードになるわけです。

モジュールファイルシステムの規則

ファイルに関するモジュール規則をまとめましょう:

  • fooという名前のモジュールにサブモジュールがなければ、fooの定義は、 foo.rsというファイルに書くべきです。
  • fooというモジュールに本当にサブモジュールがあったら、fooの定義は、 foo/mod.rsというファイルに書くべきです。

これらのルールは再帰的に適用されるので、fooというモジュールにbarというサブモジュールがあり、 barにはサブモジュールがなければ、srcディレクトリには以下のようなファイルが存在するはずです:

├── foo
│   ├── bar.rs (contains the declarations in `foo::bar`)
│   │          (`foo::bar`内の定義を含む)
│   └── mod.rs (contains the declarations in `foo`, including `mod bar`)
               (`mod bar`を含む、`foo`内の定義を含む)

モジュールは、親モジュールのファイル内でmodキーワードを使って宣言されるべきなのです。

次は、pubキーワードについて話し、警告を取り除きます!

pubで公開するか制御する

リスト7-5に示したエラーメッセージをnetworknetwork::serverのコードを、 src/network/mod.rssrc/network/server.rsファイルにそれぞれ移動することで解決しました。 その時点でcargo buildはプロジェクトをビルドできましたが、 client::connectnetwork::connectnetwork::server::connect関数が、 使用されていないという警告メッセージが出ていました:

warning: function is never used: `connect`
 --> src/client.rs:1:1
  |
1 | / fn connect() {
2 | | }
  | |_^
  |
  = note: #[warn(dead_code)] on by default

warning: function is never used: `connect`
 --> src/network/mod.rs:1:1
  |
1 | / fn connect() {
2 | | }
  | |_^

warning: function is never used: `connect`
 --> src/network/server.rs:1:1
  |
1 | / fn connect() {
2 | | }
  | |_^

では、何故このような警告を受けているのでしょうか?結局のところ、必ずしも自分のプロジェクト内ではなく、 利用者に利用されることを想定した関数を含むライブラリを構成しているので、 これらのconnect関数が使用されていかないということは、問題になるはずはありません。 これらの関数を生成することの要点は、自分ではなく、他のプロジェクトで使用することにあるのです。

このプログラムがこのような警告を引き起こす理由を理解するために、外部からcommunicatorライブラリを呼び出して、 他のプロジェクトからこれを使用してみましょう。そうするには、以下のようなコードを含むsrc/main.rsを作成して、 ライブラリクレートと同じディレクトリにバイナリクレートを作成します。

ファイル名: src/main.rs

extern crate communicator;

fn main() {
    communicator::client::connect();
}

extern crateコマンドを使用して、communicatorライブラリクレートをスコープに導入しています。 パッケージには2つのクレートが含まれるようになりました。Cargoは、src/main.rsをバイナリクレートのルートファイルとして扱い、 これはルートファイルがsrc/lib.rsになる既存のライブラリクレートとは区別されます。このパターンは、 実行形式プロジェクトで非常に一般的です: ほとんどの機能はライブラリクレートにあり、バイナリクレートはそれを使用するわけです。 結果として、他のプログラムもまたこのライブラリクレートを使用でき、良い責任の分離になるわけです。

communicatorライブラリの外部のクレートが検索するという観点から言えば、これまでに作ってきたモジュールは全て、 communicatorというクレートと同じ名前を持つモジュール内にあります。クレートのトップ階層のモジュールを、 ルートモジュールと呼びます。

プロジェクトのサブモジュール内で外部クレートを使用しているとしても、extern crateはルートモジュール(つまり、src/main.rs、 またはsrc/lib.rs)に書くべきということにも、注目してください。それから、 サブモジュールで外部クレートの要素をトップ階層のモジュールかのように参照できるわけです。

現状、バイナリクレートは、clientモジュールからライブラリのconnect関数を呼び出しているだけです。 ところが、cargo buildを呼び出すと、警告の後にエラーが発生します:

error[E0603]: module `client` is private
(エラー: `client`モジュールは非公開です)
 --> src/main.rs:4:5
  |
4 |     communicator::client::connect();
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

ああ!このエラーは、clientモジュールが非公開であることを教えてくれ、それが警告の肝だったわけです。 Rustの文脈において、公開とか非公開という概念にぶち当たったのは、これが初めてでもあります。 全コードの初期状態は、非公開です: 誰も他の人はコードを使用できないわけです。プログラム内で非公開の関数を使用していなければ、 自分のプログラムだけがその関数を使用することを許可された唯一のコードなので、コンパイラは関数が未使用と警告してくるのです。

client::connectのような関数を公開にすると指定した後は、バイナリクレートからその関数への呼び出しが許可されるだけでなく、 関数が未使用であるという警告も消え去るわけです。関数を公開にすれば、コンパイラは、 関数が自分のプログラム外のコードからも使用されることがあると知ります。コンパイラは、 関数が「使用されている」という架空の外部使用の可能性を考慮してくれます。それ故に、関数が公開とマークされれば、 コンパイラはそれが自分のプログラムで使用されるべきという要求をなくし、その関数が未使用という警告も止めるのです。

関数を公開にする

コンパイラに何かを公開すると指示するには、定義の先頭にpubキーワードを追記します。 今は、client::connectが未使用であるとする警告とバイナリークレートのモジュール`client`が非公開であるエラーの解消に努めます。 src/lib.rsを弄って、clientモジュールを公開にしてください。そう、こんな感じに:

ファイル名: src/lib.rs

pub mod client;

mod network;

pubキーワードは、modの直前に配置されています。再度ビルドしてみましょう:

error[E0603]: function `connect` is private
 --> src/main.rs:4:5
  |
4 |     communicator::client::connect();
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

やった!違うエラーになりました!そうです、別のエラーメッセージは、祝杯を上げる理由になるのです。 新エラーは、関数`connect`は非公開ですと示しているので、src/client.rsを編集して、 client::connectも公開にしましょう:

ファイル名: src/client.rs


# #![allow(unused_variables)]
#fn main() {
pub fn connect() {
}
#}

さて、再び、cargo buildを走らせてください:

warning: function is never used: `connect`
 --> src/network/mod.rs:1:1
  |
1 | / fn connect() {
2 | | }
  | |_^
  |
  = note: #[warn(dead_code)] on by default

warning: function is never used: `connect`
 --> src/network/server.rs:1:1
  |
1 | / fn connect() {
2 | | }
  | |_^

コードのコンパイルが通り、client:connectが使用されていないという警告はなくなりました!

コード未使用警告が必ずしも、コード内の要素を公開にしなければならないことを示唆しているわけではありません: これらの関数を公開APIの一部にしたくなかったら、未使用コード警告がもう必要なく、安全に削除できるコードに注意を向けてくれている可能性もあります。 また未使用コード警告は、ライブラリ内でこの関数を呼び出している箇所全てを誤って削除した場合に、 バグに目を向けさせてくれている可能性もあります。

しかし今回は、本当に他の2つの関数もクレートの公開APIにしたいので、これもpubとマークして残りの警告を除去しましょう。 src/network/mod.rsを変更して以下のようにしてください:

ファイル名: src/network/mod.rs

pub fn connect() {
}

mod server;

そして、コードをコンパイルします:

warning: function is never used: `connect`
 --> src/network/mod.rs:1:1
  |
1 | / pub fn connect() {
2 | | }
  | |_^
  |
  = note: #[warn(dead_code)] on by default

warning: function is never used: `connect`
 --> src/network/server.rs:1:1
  |
1 | / fn connect() {
2 | | }
  | |_^

んんー、nework::connectpubに設定されたにもかかわらず、まだ未使用関数警告が出ます。 その理由は、関数はモジュール内で公開になったものの、関数が存在するnetworkモジュールは公開ではないからです。 今回は、ライブラリの内部から外に向けて作業をした一方、client::connectでは、外から内へ作業をしていました。 src/lib.rsを変えてnetworkも公開にする必要があります。以下のように:

ファイル名: src/lib.rs

pub mod client;

pub mod network;

これでコンパイルすれば、あの警告はなくなります:

warning: function is never used: `connect`
 --> src/network/server.rs:1:1
  |
1 | / fn connect() {
2 | | }
  | |_^
  |
  = note: #[warn(dead_code)] on by default

残る警告は1つなので、自分で解消してみてください!

プライバシー規則

まとめると、要素の公開性は以下のようなルールになります:

  • 要素が公開なら、どの親モジュールを通してもアクセス可能です。
  • 要素が非公開なら、直接の親モジュールとその親の子モジュールのみアクセスできます。

プライバシー例

もうちょっと鍛錬を得るために、もういくつかプライバシー例を見てみましょう。新しいライブラリプロジェクトを作成し、 リスト7-5のコードを新規プロジェクトのsrc/lib.rsに入力してください。

ファイル名: src/lib.rs

mod outermost {
    pub fn middle_function() {}

    fn middle_secret_function() {}

    mod inside {
        pub fn inner_function() {}

        fn secret_function() {}
    }
}

fn try_me() {
    outermost::middle_function();
    outermost::middle_secret_function();
    outermost::inside::inner_function();
    outermost::inside::secret_function();
}

リスト7-6: 公開と非公開関数の例。不正なものもあります

このコードをコンパイルする前に、try_me関数のどの行がエラーになるか当ててみてください。 それからコンパイルを試して、合ってたかどうか確かめ、エラーの議論を求めて読み進めてください!

エラーを確かめる

try_me関数は、プロジェクトのルートモジュールに存在しています。outermostという名前のモジュールは非公開ですが、 プライバシー規則の2番目にある通り、try_meそのままに、outermostは現在(ルート)のモジュールなので、 try_me関数は、outermostモジュールにアクセスすることを許可されるのです。

middle_functionは公開なので、outermost::middle_functionという呼び出しも動作し、 try_memiddle_functionにその親モジュールのoutermostを通してアクセスしています。 このモジュールは、アクセス可能と既に決定しました。

outermost::middle_secret_functionの呼び出しは、コンパイルエラーになるでしょう。 middle_secret_functionは非公開なので、2番目の規則が適用されます。ルートモジュールは、 middle_secret_functionの現在のモジュール(outermostがそうです)でも、 middle_secret_functionの現在のモジュールの子供でもないのです。

insideという名前のモジュールは非公開で子モジュールを持たないので、現在のモジュールであるoutermostからのみアクセスできます。 つまり、try_me関数は、outermost::inside::inner_functionoutermost::inside::secret_functionも呼び出すことを許されないのです。

エラーを修正する

エラーを修正しようとする過程でできるコード変更案は、以下の通りです。各々試してみる前に、 エラーを解消できるか当ててみてください。それからコンパイルして正しかったか間違っていたか確かめ、 プライバシー規則を使用して理由を理解してください。もっと実験を企てて試してみるのもご自由に!

  • insideモジュールが公開だったらどうだろうか?
  • outermostが公開で、insideが非公開ならどうだろうか?
  • inner_functionの本体で::outermost::middle_secret_function()を呼び出したらどうだろうか? (頭の二つのコロンは、ルートモジュールから初めてモジュールを参照したいということを意味します)

今度は、useキーワードで要素をスコープに導入する話をしましょう。

異なるモジュールの名前を参照する

モジュール名を呼び出しの一部に使用して、モジュール内に定義された関数の呼び出し方法を解説しました。 リスト7-7に示したnested_modules関数の呼び出しのような感じですね。

ファイル名: src/main.rs

pub mod a {
    pub mod series {
        pub mod of {
            pub fn nested_modules() {}
        }
    }
}

fn main() {
    a::series::of::nested_modules();
}

リスト7-7: 囲まれたモジュールをフルパス指定して関数を呼び出す

見てお分かりの通り、フルパス指定した名前を参照すると非常に長ったらしくなります。 幸い、Rustには、これらの呼び出しをもっと簡潔にするキーワードが用意されています。

useキーワードで名前をスコープに導入する

Rustのuseキーワードは、呼び出したい関数のモジュールをスコープに導入することで、 長ったらしい関数呼び出しを短縮します。以下は、 a::series::ofモジュールをバイナリクレートのルートスコープに持ってくる例です:

Filename: src/main.rs

pub mod a {
    pub mod series {
        pub mod of {
            pub fn nested_modules() {}
        }
    }
}

use a::series::of;

fn main() {
    of::nested_modules();
}

use a::series::of;の行は、ofモジュールを参照したい箇所全部でフルパスのa::series::ofを使用するのではなく、 ofを利用できることを意味しています。

このuseキーワードは、指定したものだけをスコープに入れます: モジュールの子供はスコープに導入しないのです。 そのため、nested_modules関数を呼び出したい際に、それでもまだof::nested_modulesを使わなければならないのです。

以下のように、代わりにuseで関数を指定して、関数をスコープに入れることもできました:

pub mod a {
    pub mod series {
        pub mod of {
            pub fn nested_modules() {}
        }
    }
}

use a::series::of::nested_modules;

fn main() {
    nested_modules();
}

そうすれば、モジュールをすべて取り除き、関数を直接参照することができます。

enumもモジュールのようにある種の名前空間をなすので、enumのバリアントをuseでスコープに導入することもできます。 どんなuse文に関しても、一つの名前空間から複数の要素をスコープに導入する場合、波かっことお尻にカンマを使用することで列挙できます。 こんな感じで:

enum TrafficLight {
    Red,
    Yellow,
    Green,
}

use TrafficLight::{Red, Yellow};

fn main() {
    let red = Red;
    let yellow = Yellow;
    let green = TrafficLight::Green;
}

Greenuse文に含んでいないので、まだGreenバリアント用にTrafficLight名前空間を指定しています。

Globで全ての名前をスコープに導入する

ある名前空間の要素を全て一度にスコープに導入するには、*表記が使用でき、これはglob(塊)演算子と呼ばれます。 この例は、あるenumの列挙子を各々を列挙せずに全てスコープに導入しています:

enum TrafficLight {
    Red,
    Yellow,
    Green,
}

use TrafficLight::*;

fn main() {
    let red = Red;
    let yellow = Yellow;
    let green = Green;
}

*演算子はTrafficLight名前空間に存在する全て公開要素をスコープに導入します。 あまりglobは使用するべきではありません: 便利ではありますが、globは予想以上の要素を引き込んで、 名前衝突を引き起こす可能性があるのです。

superを使用して親モジュールにアクセスする

この章の頭で見かけたように、ライブラリクレートを作成する際、Cargoはtestsモジュールを用意してくれました。 今からそれについて詳しく掘り下げていくことにしましょう。communicatorプロジェクトでsrc/lib.rsを開いてください:

ファイル名: src/lib.rs

pub mod client;

pub mod network;

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

第11章でテストについて詳しく説明しますが、これでこの例の一部が持つ意味がわかったのではないでしょうか: 他のモジュールに隣接するtestsという名前のモジュールがあり、このモジュールはit_worksという名前の関数を含んでいます。 特別な注釈があるものの、testsモジュールもただのモジュールです!よって、モジュール階層は以下のような見た目になります:

communicator
 ├── client
 ├── network
 |   └── client
 └── tests

テストは、ライブラリ内でコードの準備運動を行うためのものなので、このit_works関数からclient::connect関数を呼び出してみましょう。 まあ、尤も今のところ、機能の検査は何もしないんですけどね。これはまだ動きません:

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        client::connect();
    }
}
#}

cargo testコマンドを呼び出してテストを実行してください:

$ cargo test
   Compiling communicator v0.1.0 (file:///projects/communicator)
error[E0433]: failed to resolve. Use of undeclared type or module `client`
(エラー: 解決に失敗しました。未定義の型、またはモジュール`client`を使用しています)
 --> src/lib.rs:9:9
  |
9 |         client::connect();
  |         ^^^^^^ Use of undeclared type or module `client`

コンパイルが失敗しましたが、なぜでしょうか?src/main.rsのように、関数の直前にcommunicator::を配置する必要はありません。 なぜなら、間違いなくここでは、communicatorライブラリクレート内にいるからです。 原因は、パスが常に現在のモジュールに対して相対的になり、ここではtestsになっているからです。 唯一の例外は、use文内であり、パスは標準でクレートのルートに相対的になります。 testsモジュールは、clientモジュールがスコープに存在する必要があるのです!

では、どうやってモジュール階層を一つ上がり、testsモジュールのclient::connect関数を呼び出すのでしょうか? testsモジュールにおいて、先頭にコロンを使用して、コンパイラにルートから始めて、フルパスを列挙したいと知らせることもできます。 こんな感じで:

::client::connect();

あるいは、superを使用して現在のモジュールからモジュール階層を一つ上がることもできます。 以下のように:

super::client::connect();

この例では、これら二つの選択はそれほど異なるようには見えませんが、モジュール階層がもっと深ければ、 常にルートから書き始めるのは、コードを冗長にする原因になります。そのような場合、 superを使用して現在のモジュールから兄弟のモジュールに辿り着くのは、いいショートカットになります。 さらに、コードのいろんなところでルートからパスを指定し、モジュール構造を変化させた場合、 複数箇所でパスを更新する必要が陥り、面倒なことになるでしょう。

各テストでsuper::と入力しなければならないのも不快なことですが、それを解決してくれる道具をもう見かけています: useです!super::の機能は、useに与えるパスを変更するので、ルートモジュールではなく、 親モジュールに対して相対的になります。

このような理由から、ことにtestsモジュールにおいてuse super::somthingは通常、 最善策になるわけです。故に、今ではテストはこんな見た目になりました:

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
#[cfg(test)]
mod tests {
    use super::client;

    #[test]
    fn it_works() {
        client::connect();
    }
}
#}

再度cargo testを実行すると、テストは通り、テスト結果出力の最初の部分は以下のようになるでしょう:

$ cargo test
   Compiling communicator v0.1.0 (file:///projects/communicator)
     Running target/debug/communicator-92007ddb5330fa5a

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

まとめ

これでコードを体系化する新しいテクニックを知りましたね!これらのテクニックを使用して、 関連のある機能をまとめ上げ、ファイルが長くなりすぎるのを防ぎ、ライブラリの使用者に整理整頓された公開APIを提供してください。

次は、自分の素晴らしく綺麗なコードで使用できる標準ライブラリのコレクションデータ構造について見ていきましょう。

一般的なコレクション

Rustの標準ライブラリは、コレクションと呼ばれる多くの非常に有益なデータ構造を含んでいます。他の多くのデータ型は、 ある一つの値を表しますが、コレクションは複数の値を含むことができます。組み込みの配列とタプル型とは異なり、 これらのコレクションが指すデータはヒープに確保され、データ量はコンパイル時にわかる必要はなく、 プログラムの実行にあわせて、伸縮可能であることになります。各種のコレクションには異なる能力とコストが存在し、 自分の現在の状況に最適なものを選び取るスキルは、時間とともに育っていきます。この章では、 Rustのプログラムにおいて、非常に頻繁に使用される3つのコレクションについて議論しましょう。

  • ベクタ型は、可変長の値を並べて保持できる。
  • 文字列は、文字のコレクションである。以前、String型について触れたが、 この章ではより掘り下げていく。
  • ハッシュマップは、値を特定のキーと紐付けさせてくれる。より一般的なデータ構造である、 マップの特定の実装である。

標準ライブラリで提供されている他の種のコレクションについて学ぶには、 ドキュメントを参照されたし。

ベクタ型、文字列、ハッシュマップの生成と更新方法や、各々が特別な点について議論していきましょう。

ベクタで一連の値を保持する

最初に見るコレクションは、Vec<T>であり、ベクタとしても知られています。ベクタは、 メモリ上に値を隣り合わせに並べる単独のデータ構造に2つ以上の値を保持させてくれます。 ベクタには、同じ型の値しか保持できません。要素のリストがある場合に有用です。 例えば、テキストファイルの各行とか、ショッピングカートのアイテムの価格などです。

新しいベクタを生成する

新しい空のベクタを作るには、リスト8-1に示されたように、Vec::new関数を呼ぶことができます。


# #![allow(unused_variables)]
#fn main() {
let v: Vec<i32> = Vec::new();
#}

リスト8-1: 新しい空のベクタを生成してi32型の値を保持する

ここでは、型注釈を付け足したことに注目してください。このベクタに対して、何も値を挿入していないので、 コンパイラには、どんなデータを保持させるつもりなのかわからないのです。これは重要な点です。ベクタは、 ジェネリクスを使用して実装されているのです; 独自の型でジェネリクスを使用する方法については、 第10章で解説します。今は、標準ライブラリにより提供されているVec<T>型は、どんな型でも保持でき、 特定のベクタが特定の型を保持するとき、その型は山かっこ内に指定されることを知っておいてください。 リスト8-1では、コンパイラにvVec<T>は、i32型の要素を保持すると指示しました。

より現実的なコードでは、一旦値を挿入したら、コンパイラは保持させたい値の型をしばしば推論できるので、 この型注釈をすることは滅多にありません。初期値のあるVec<T>を生成する方が一般的ですし、 Rustには、利便性のためにvec!というマクロも用意されています。このマクロは、 与えた値を保持する新しいベクタ型を生成します。リスト8-2では、123という値を持つ新しいVec<i32>を生成しています。


# #![allow(unused_variables)]
#fn main() {
let v = vec![1, 2, 3];
#}

リスト8-2: 値を含む新しいベクタを生成する

初期値のi32値を与えたので、コンパイラは、vの型がVec<i32>であると推論でき、型注釈は必要なくなりました。 次は、ベクタを変更する方法を見ましょう。

ベクタを更新する

ベクタを生成し、それから要素を追加するには、リスト8-3に示したように、pushメソッドを使用できます。


# #![allow(unused_variables)]
#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型であり、コンパイラはこのことをデータから推論するので、 Vec<i32>という注釈は必要なくなります。

ベクタをドロップすれば、要素もドロップする

他のあらゆる構造体同様、ベクタもスコープを抜ければ、解放されます。リスト8-4に注釈したようにですね。


# #![allow(unused_variables)]
#fn main() {
{
    let v = vec![1, 2, 3, 4];

    // vで作業をする

} // <- vはここでスコープを抜け、解放される
#}

リスト8-4: ベクタとその要素がドロップされる箇所を示す

ベクタがドロップされると、その中身もドロップされます。つまり、保持されていた整数値が、 片付けられるということです。これは一見単純な点に見えるかもしれませんが、ベクタの要素への参照を導入した途端、 もうちょっと複雑になる可能性を秘めています。次は、それに挑んでいきましょう!

ベクタの要素を読む

もうベクタを生成し、更新し、破壊する方法を知ったので、コンテンツを読む方法を知るのはいいステップアップです。 ベクタに保持された値を参照する方法は2つあります。例では、さらなる明瞭性を求めて、 これらの関数から返る値の型を注釈しました。

リスト8-5に示したのは、両メソッドがベクタの値に対して、添字記法とgetメソッドによりアクセスするところです。


# #![allow(unused_variables)]
#fn main() {
let v = vec![1, 2, 3, 4, 5];

let third: &i32 = &v[2];
let third: Option<&i32> = v.get(2);
#}

リスト8-5: 添字記法かgetメソッドを使用してベクタの要素にアクセスする

ここでは、2つのことに気付いてください。まず、3番目の要素を得るのに2という添え字の値を使用していることです: ベクタは、数値により順序付けされ、添え字は0から始まります。2番目に、3番目の要素を得る2つの方法は、 &[]を使用して参照を得るものと、番号を引数としてgetメソッドに渡して、Option<&T>を得るものということです。

Rustには要素を参照する方法が2通りあるので、ベクタに要素が含まれない番号の値を使用しようとした時に、 プログラムの振る舞いを選択できます。例として、ベクタに5つ要素があり、番号100の要素にアクセスを試みた場合、 プログラムがすることを確認しましょう。リスト8-6に示したようにですね。


# #![allow(unused_variables)]
#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を返します。 普通の状態でも、ベクタの範囲外にアクセスする可能性がある場合に、このメソッドを使用することになるでしょう。 そうしたら、コードにはSome(&element)Noneを扱うロジックが存在することになります。そう、 第6章で議論したように。例えば、番号は人間に数値を入力してもらうことで得ることもできます。 もし大きすぎる値を誤って入力し、プログラムがNone値を得てしまったら、現在ベクタに幾つ要素があるかをユーザに教え、 再度正しい値を入力してもらうことができるでしょう。その方が、タイプミスでプログラムをクラッシュさせるより、 ユーザに優しくなるでしょう。

プログラムに有効な参照がある場合、borrow checker(借用精査機)は(第4章で解説しましたが)、 所有権と借用規則を強制し、ベクタ型の中身へのこの参照や他のいかなる参照も有効であり続けることを保証してくれます。 同一スコープ上では、可変と不変な参照を同時には存在させられないというルールを思い出してください。 このルールはリスト8-7にも適用され、リスト8-7ではベクタの最初の要素への不変参照を保持し、 終端に要素を追加しようとしていますが、動きません。

let mut v = vec![1, 2, 3, 4, 5];

let first = &v[0];

v.push(6);

リスト8-7: 要素への参照を保持しつつ、ベクタに要素を追加しようとする

このコードをコンパイルすると、こんなエラーになります:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
(エラー: 不変としても借用されているので、`v`を可変で借用できません)
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
  |                  (不変借用はここで発生しています)
5 |
6 |     v.push(6);
  |     ^ mutable borrow occurs here
  |      (可変借用は、ここで発生しています)
7 | }
  | - immutable borrow ends here
  |   (不変借用はここで終了しています)

リスト8-7のコードは、一見動くはずのように見えるかもしれません: なぜ、最初の要素への参照が、 ベクタの終端への変更を気にかける必要があるのでしょうか?このエラーは、ベクタの動作法のせいです: 新規要素をベクタの終端に追加すると、ベクタが現在存在する位置に隣り合って要素を入れるだけの領域がなかった場合に、 メモリの新規確保をして古い要素を新しいスペースにコピーする必要があるかもしれないからです。 その場合、最初の要素を指す参照は、解放されたメモリを指すことになるでしょう。借用規則により、 そのような場面に落ち着かないよう回避されるのです。

注釈: Vec<T>の実装に関する詳細については、https://doc.rust-lang.org/stable/nomicon/vec.htmlの、 "The Rustonomicon"を参照されたし。

ベクタの値を走査する

ベクタの要素に順番にアクセスしたいなら、添え字で1回に1要素にアクセスするのではなく、全要素を走査することができます。 リスト8-8でforループを使い、i32のベクタの各要素に対する不変な参照を得て、それらを出力する方法を示しています。


# #![allow(unused_variables)]
#fn main() {
let v = vec![100, 32, 57];
for i in &v {
    println!("{}", i);
}
#}

リスト8-8: forループで要素を走査し、ベクタの各要素を出力する

全要素に変更を加える目的で、可変なベクタの各要素への可変な参照を走査することもできます。 リスト8-9のforループでは、各要素に50を足しています。


# #![allow(unused_variables)]
#fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50;
}
#}

リスト8-9: ベクタの要素への可変な参照を走査する

可変参照が参照している値を変更するには、+=演算子を使用する前に、 参照外し演算子(*)を使用してiの値に辿り着かないといけません。

Enumを使って複数の型を保持する

この章の冒頭で、ベクタは同じ型の値しか保持できないと述べました。これは不便に考えられることもあります; 異なる型の要素を保持する必要性が出てくるユースケースも確かにあるわけです。幸運なことに、 enumの列挙子は、同じenumの型の元に定義されるので、ベクタに異なる型の要素を保持する必要が出たら、 enumを定義して使用することができます!

例えば、スプレッドシートの行から値を得たくなったとしましょう。ここで行の列には、整数を含むものや、 浮動小数点数を含むもの、文字列を含むものがあります。列挙子が異なる値の型を保持するenumを定義できます。 そして、このenumの列挙子は全て同じ型: enumの型と考えられるわけです。それからそのenumを保持するベクタを生成でき、 結果的に異なる型を保持できるようになるわけです。リスト8-10でこれを模擬しています。


# #![allow(unused_variables)]
#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でベクタがどんな型でも保持できたら、 ベクタの要素に対して行われる処理に対して一つ以上の型がエラーを引き起こす可能性があったでしょう。 enumに加えてmatch式を使うことは、第6章で議論した通り、コンパイル時にありうる場合全てに対処していることをコンパイラが、 確認できることを意味します。

プログラム記述時にプログラムがベクタに実行時に保持するありとあらゆる一連の型をプログラマが知らない場合、 この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の標準ライブラリには、他の文字列型も含まれています。OsStringOsStrCStringCStrなどです。 ライブラリクレートにより、文字列データを保持する選択肢はさらに増えます。 それらの名前が全てStringStrで終わっているのがわかりますか?所有権ありと借用されたバージョンを指しているのです。 ちょうど以前見かけたString&strのようですね。例えば、これらの文字列型は、異なるエンコード方法でテキストを保持していたり、 メモリ上の表現が異なったりします。この章では、これらの他の種類の文字列については議論しません; 使用方法やどれが最適かについては、APIドキュメントを参照してください。

新規文字列を生成する

Vec<T>で使用可能な処理の多くがStringでも使用できます。文字列を生成するnew関数から始めましょうか。 リスト8-11に示したようにですね。


# #![allow(unused_variables)]
#fn main() {
let mut s = String::new();
#}

リスト8-11: 新しい空のStringを生成する

この行は、新しい空のsという文字列を生成しています。それからここにデータを読み込むことができるわけです。 だいたい、文字列の初期値を決めるデータがあるでしょう。そのために、to_stringメソッドを使用します。 このメソッドは、文字列リテラルがしているように、Displayトレイトを実装する型ならなんでも使用できます。 リスト8-12に2例、示しています。


# #![allow(unused_variables)]
#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_variables)]
#fn main() {
let s = String::from("initial contents");
#}

リスト8-13: String::from関数を使って文字列リテラルからStringを作る

文字列は、非常に多くのものに使用されるので、多くの異なる一般的なAPIを使用でき、たくさんの選択肢があるわけです。 冗長に思われるものもありますが、適材適所です!今回の場合、String::fromto_stringは全く同じことをします。 従って、どちらを選ぶかは、スタイル次第です。

文字列はUTF-8エンコードされていることを覚えていますか?要するに文字列には、適切にエンコードされていればどんなものでも含めます。 リスト8-14に示したように。


# #![allow(unused_variables)]
#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_strpushで文字列に追加する

push_strメソッドで文字列スライスを追記することで、Stringを伸ばすことができます。 リスト8-15の通りです。


# #![allow(unused_variables)]
#fn main() {
let mut s = String::from("foo");
s.push_str("bar");
#}

リスト8-15: push_strメソッドでStringに文字列スライスを追記する

この2行の後、sfoobarを含むことになります。push_strメソッドは、必ずしも引数の所有権を得なくていいので、 文字列スライスを取ります。例えば、リスト8-16のコードは、中身をs1に追加した後、 s2を使えなかったら不幸だということを示しています。


# #![allow(unused_variables)]
#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メソッドでlStringに追加するコードを呈示しています。


# #![allow(unused_variables)]
#fn main() {
let mut s = String::from("lo");
s.push('l');
#}

リスト8-17: pushString値に1文字を追加する

このコードの結果、slolを含むことになるでしょう。

編者注: lollaughing out loud(大笑いする)の頭文字からできたスラングです。 日本語のwwwみたいなものですね。

+演算子、またはformat!マクロで連結

2つのすでにある文字列を組み合わせたくなることがよくあります。リスト8-18に示したように、 一つ目の方法は、+演算子を使用することです。


# #![allow(unused_variables)]
#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章で議論します。 adds引数の所有権を奪わないので、この処理後もs2が有効なStringになるわけです。

2番目に、シグニチャからaddselfの所有権をもらうことがわかります。selfには&がついていないからです。 これはつまり、リスト8-18においてs1add呼び出しにムーブされ、その後は有効ではなくなるということです。 故に、s3 = s1 + &s2;は両文字列をコピーして新しいものを作るように見えますが、 この文は実際にはs1の所有権を奪い、s2の中身のコピーを追記し、結果の所有権を返すのです。言い換えると、 たくさんのコピーをしているように見えますが、違います; 実装は、コピーよりも効率的です。

複数の文字列を連結する必要が出ると、+演算子の振る舞いは扱いにくくなります:


# #![allow(unused_variables)]
#fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "-" + &s2 + "-" + &s3;
#}

ここで、stic-tac-toeになるでしょう。+"文字のせいで何が起きているのかわかりにくいです。 もっと複雑な文字列の連結には、format!マクロを使用することができます:


# #![allow(unused_variables)]
#fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{}-{}-{}", s1, s2, s3);
#}

このコードでも、stic-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がメモリにどのように文字列を保持しているかについて議論する必要があります。

内部表現

StringVec<u8>のラッパです。リスト8-14から適切にUTF-8でエンコードされた文字列の例をご覧ください。 まずは、これ:


# #![allow(unused_variables)]
#fn main() {
let len = String::from("Hola").len();
#}

この場合、lenは4になり、これは、文字列"Hola"を保持するベクタの長さが4バイトであることを意味します。 これらの各文字は、UTF-8でエンコードすると、1バイトになるのです。しかし、以下の行ではどうでしょうか? (この文字列は大文字のキリル文字Zeで始まり、アラビア数字の3では始まっていないことに注意してください)


# #![allow(unused_variables)]
#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_variables)]
#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_variables)]
#fn main() {
for c in "नमस्ते".chars() {
    println!("{}", c);
}
#}

このコードは、以下のように出力します:

न
म
स
्
त
े

bytesメソッドは、各バイトをそのまま返すので、最適になることもあるかもしれません:


# #![allow(unused_variables)]
#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_variables)]
#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: ハッシュマップを生成してキーと値を挿入する

最初に標準ライブラリのコレクション部分からHashMapuseする必要があることに注意してください。 今までの3つの一般的なコレクションの内、これが最も使用頻度が低いので、初期化処理で自動的にスコープに導入される機能には含まれていません。 また、標準ライブラリからのサポートもハッシュマップは少ないです; 例えば、生成するための組み込みマクロがありません。

ベクタと全く同様に、ハッシュマップはデータをヒープに保持します。このHashMapはキーがString型、 値はi32型です。ベクタのように、ハッシュマップは均質です: キーは全て同じ型でなければならず、 値も全て同じ型でなければなりません。

ハッシュマップを生成する別の方法は、タプルのベクタに対してcollectメソッドを使用するものです。 ここで、各タプルは、キーと値から構成されています。collectメソッドはいろんなコレクション型にデータをまとめ上げ、 そこにはHashMapも含まれています。例として、チーム名と初期スコアが別々のベクタに含まれていたら、 zipメソッドを使ってタプルのベクタを作り上げることができ、そこでは「ブルー」は10とペアになるなどします。 リスト8-21に示したように、それからcollectメソッドを使って、そのタプルのベクタをハッシュマップに変換することができるわけです。


# #![allow(unused_variables)]
#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_variables)]
#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_namefield_valueがハッシュマップにムーブされた後は、 これらの変数を使用することは叶いません。

値への参照をハッシュマップに挿入したら、値はハッシュマップにムーブされません。参照が指している値は、 最低でもハッシュマップが有効な間は、有効でなければなりません。これらの問題について詳細には、 第10章の「ライフタイムで参照を有効化する」節で語ります。

ハッシュマップの値にアクセスする

リスト8-23に示したように、キーをgetメソッドに提供することで、ハッシュマップから値を取り出すことができます。


# #![allow(unused_variables)]
#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に包まれます。というのも、getOption<&V>を返すからです; キーに対応する値がハッシュマップになかったら、 getNoneを返すでしょう。プログラムは、このOptionを第6章で解説した方法のどれかで扱う必要があるでしょう。

ベクタのように、forループでハッシュマップのキーと値のペアを走査することができます:


# #![allow(unused_variables)]
#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_variables)]
#fn main() {
use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);

println!("{:?}", scores);
#}

リスト8-24: 特定のキーで保持された値を置き換える

このコードは、{"Blue": 25}と出力するでしょう。10という元の値は上書きされたのです。

キーに値がなかった時のみ値を挿入する

特定のキーに値があるか確認することは一般的であり、存在しない時に値を挿入することも一般的です。 ハッシュマップには、これを行うentryと呼ばれる特別なAPIがあり、これは、引数としてチェックしたいキーを取ります。 このentryメソッドの戻り値は、Entryと呼ばれるenumであり、これは存在したりしなかったりするかもしれない値を表します。 イエローチームに対するキーに値が紐づけられているか否か確認したくなったとしましょう。存在しなかったら、 50という値を挿入したく、ブルーチームに対しても同様です。entryAPIを使用すれば、コードはリスト8-25のようになります。


# #![allow(unused_variables)]
#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_variables)]
#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とは、 BuildeHasherトレイトを実装する型のことです。トレイトについてとその実装方法については、第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 buildcargo runを使用していれば、標準で有効になり、 ここではそうなっています。

リスト9-2の出力で、バックトレースの11行目が問題発生箇所を指し示しています: src/main.rsの4行目です。 プログラムにパニックしてほしくなければ、自分のファイルについて言及している最初の行で示されている箇所が、 どのようにパニックを引き起こす値でこの箇所にたどり着いたか割り出すために調査を開始すべき箇所になります。 バックトレースの使用法を模擬するためにわざとパニックするコードを書いたリスト9-1において、 パニックを解消する方法は、3つしか要素のないベクタの添字99の要素を要求しないことです。 将来コードがパニックしたら、パニックを引き起こすどんな値でコードがどんな動作をしているのかと、 代わりにコードは何をすべきなのかを算出する必要があるでしょう。

また、この章の後ほど、「panic!するかpanic!するまいか」節でpanic!とエラー状態を扱うのにpanic!を使うべき時と使わぬべき時に戻ってきます。 次は、Resultを使用してエラーから回復する方法を見ましょう。

Resultで回復可能なエラー

多くのエラーは、プログラムを完全にストップさせるほど深刻ではありません。時々、関数が失敗すると、 容易に解釈し、対応できる理由によることがあります。例えば、ファイルを開こうとして、 ファイルが存在しないために処理が失敗したら、プロセスを殺すのではなく、ファイルを作成したいことがあります。

第2章のResult型で失敗する可能性に対処する」Result enumが以下のように、 OkErrの2値からなるよう定義されていることを思い出してください:


# #![allow(unused_variables)]
#fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
#}

TEは、ジェネリックな型引数です: ジェネリクスについて詳しくは、第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::openResultを返すとどう知るのでしょうか?標準ライブラリの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アーム内でOkErrバリアントの前に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::openErr列挙子に含めて返す値の型は、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の最後のアームは同じままなので、ファイルが行方不明のエラー以外ならプログラムはパニックします。

エラー時にパニックするショートカット: unwrapexpect

matchの使用は、十分に仕事をしてくれますが、いささか冗長になり得る上、必ずしも意図をよく伝えるとは限りません。 Result<T, E>型には、色々な作業をするヘルパーメソッドが多く定義されています。それらの関数の一つは、 unwrapと呼ばれますが、リスト9-4で書いたmatch式と同じように実装された短絡メソッドです。 Result値がOk列挙子なら、unwrapOkの中身を返します。ResultErr列挙子なら、 unwrappanic!マクロを呼んでくれます。こちらが実際に動作している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");
}

expectunwrapと同じように使用してます: ファイルハンドルを返したり、panic!マクロを呼び出しています。 expectpanic!呼び出しで使用するエラーメッセージは、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_variables)]
#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_variables)]
#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_variables)]
#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::openread_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を返す別の関数を呼び出した時、 ?演算子を使用してエラーを呼び出し元に委譲する可能性を生み出す代わりに、matchResultのメソッドのどれかを使う必要があるでしょう。

さて、panic!呼び出しやResultを返す詳細について議論し終えたので、 どんな場合にどちらを使うのが適切か決める方法についての話に戻りましょう。

panic!すべきかするまいか

では、panic!すべき時とResultを返すべき時はどう決定すればいいのでしょうか?コードがパニックしたら、 回復する手段はありません。回復する可能性のある手段の有る無しに関わらず、どんなエラー場面でもpanic!を呼ぶことはできますが、 そうすると、呼び出す側のコードの立場に立ってこの場面は回復不能だという決定を下すことになります。 Result値を返す決定をすると、決断を下すのではなく、呼び出し側に選択肢を与えることになります。 呼び出し側は、場面に合わせて回復を試みることを決定したり、この場合のErr値は回復不能と断定して、 panic!を呼び出し、回復可能だったエラーを回復不能に変換することもできます。故に、Resultを返却することは、 失敗する可能性のある関数を定義する際には、いい第1選択肢になります。

稀な場面では、Resultを返すよりもパニックするコードを書く方がより適切になることもあります。 例やプロトタイプコード、テストでパニックするのが適切な理由を探求しましょう。 それからコンパイラは失敗することはないとわからないけれど、人間はできる場面を議論しましょう。 そして、ライブラリコードでパニックするか決定する方法についての一般的なガイドラインで結論づけましょう。

例、プロトタイプコード、テスト

例を記述して何らかの概念を具体化している時、頑健なエラー処理コードも例に含むことは、例の明瞭さを欠くことになりかねません。 例において、unwrapなどのパニックする可能性のあるメソッド呼び出しは、 アプリケーションにエラーを処理してほしい方法へのプレースホルダーを意味していると理解され、 これは残りのコードがしていることによって異なる可能性があります。

同様に、unwrapexpectメソッドは、エラーの処理法を決定する準備ができる前、プロトタイプの段階では、 非常に便利です。それらにより、コードにプログラムをより頑健にする時の明らかなマーカーが残されるわけです。

メソッド呼び出しがテスト内で失敗したら、そのメソッドがテスト下に置かれた機能ではなかったとしても、 テスト全体が失敗してほしいでしょう。panic!が、テストが失敗と印づけられる手段なので、 unwrapexpectの呼び出しはスバリ起こるべきことです。

コンパイラよりもプログラマがより情報を持っている場合

ResultOk値であると確認する何らかの別のロジックがある場合、unwrapを呼び出すことは適切でしょうが、 コンパイラは、そのロジックを理解はしません。それでも、処理する必要のあるResultは存在するでしょう: 呼び出している処理が何であれ、自分の特定の場面では論理的に起こり得なくても、一般的にまだ失敗する可能性はあるわけです。 手動でコードを調査してErr列挙子は存在しないと確認できたら、unwrapを呼び出すことは完全に受容できることです。 こちらが例です:


# #![allow(unused_variables)]
#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以外の型がある場合、プログラムは、 何もないではなく何かあると想定します。そうしたらコードは、 SomeNone列挙子の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_variables)]
#fn main() {
pub struct Guess {
    value: u32,
}

impl Guess {
    pub fn new(value: u32) -> Guess {
        if value < 1 || value > 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という名前の構造体を定義しています。 ここに数値が保管されます。

それからGuessGuess値のインスタンスを生成するnewという名前の関連関数を実装しています。 new関数は、u32型のvalueという引数を取り、Guessを返すように定義されています。 new関数の本体のコードは、valueをふるいにかけ、1から100の範囲であることを確かめます。 valueがふるいに引っかかったら、panic!呼び出しを行います。これにより、呼び出しコードを書いているプログラマに、 修正すべきバグがあると警告します。というのも、この範囲外のvalueGuessを生成することは、 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関数の条件式でチェックされていないvalueGuessに存在する手段はないことが保証されるわけです。

そうしたら、引数を一つ持つか、1から100の範囲の数値のみを返す関数は、シグニチャでu32ではなく、 Guessを取るか返し、本体内で追加の確認を行う必要はなくなると宣言できるでしょう。

まとめ

Rustのエラー処理機能は、プログラマがより頑健なコードを書く手助けをするように設計されています。 panic!マクロは、プログラムが処理できない状態にあり、無効だったり不正な値で処理を継続するのではなく、 プロセスに処理を中止するよう指示することを通知します。Result enumは、Rustの型システムを使用して、 コードが回復可能な方法で処理が失敗するかもしれないことを示唆します。Resultを使用して、 呼び出し側のコードに成功や失敗する可能性を処理する必要があることも教えます。 適切な場面でpanic!Resultを使用することで、必然的な問題の眼前でコードの信頼性を上げてくれます。

今や、標準ライブラリがOptionResult enumなどでジェネリクスを有効活用するところを目の当たりにしたので、 ジェネリクスの動作法と自分のコードでの使用方法について語りましょう。

ジェネリック型、トレイト、ライフタイム

全てのプログラミング言語には、概念の重複を効率的に扱う道具があります。Rustにおいて、そのような道具の一つがジェネリクスです。 ジェネリクスは、具体型や他のプロパティの抽象的な代役です。コード記述の際、コンパイルやコード実行時に、 ジェネリクスの位置に何が入るかを知ることなく、ジェネリクスの振る舞いや他のジェネリクスとの関係を表現できるのです。

関数が未知の値の引数を取り、同じコードを複数の具体的な値に対して走らせるように、 i32Stringなどの具体的な型の代わりに何かジェネリックな型の引数を取ることができます。 実際、第6章でOption<T>、第8章でVec<T>HashMap<K, V>、第9章でResult<T, E>を既に使用しました。 この章では、独自の型、関数、メソッドをジェネリクスとともに定義する方法を探求します!

まず、関数を抽出して、コードの重複を減らす方法を確認しましょう。次に同じテクニックを活用して、 引数の型のみが異なる2つの関数からジェネリックな関数を生成します。また、 ジェネリックな型を構造体やenum定義で使用する方法も説明します。

それから、トレイトを使用して、ジェネリックな方法で振る舞いを定義する方法を学びます。 ジェネリックな型のあるトレイトを組み合わせてただ単にどんな型に対してもとは対照的に、 ジェネリックな型を特定の振る舞いのある型のみに制限することができます。

最後に、ライフタイムを議論します。ライフタイムとは、コンパイラに参照がお互いにどう関係しているかの情報を与える1種のジェネリクスです。 ライフタイムのおかげでコンパイラに参照が有効であることを確認してもらうことを可能にしつつ、多くの場面で値を借用できます。

関数を抽出することで重複を取り除く

ジェネリクスの記法に飛び込む前にまずは、関数を抽出することでジェネリックな型が関わらない重複を取り除く方法を見ましょう。 そして、このテクニックを適用してジェネリックな関数を抽出するのです!重複したコードを認識して関数に抽出できるのと同じように、 ジェネリクスを使用できる重複コードも認識し始めるでしょう。

リスト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に変更するのに要したステップです:

  1. 重複したコードを認識する。
  2. 重複コードを関数本体に抽出し、コードの入力と戻り値を関数シグニチャで指定する。
  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>構造体を定義してあらゆる型のxy座標を保持する方法を示しています。

ファイル名: 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: 型Txy値を保持するPoint<T>構造体

構造体定義でジェネリクスを使用する記法は、関数定義のものと似ています。まず、山カッコ内に型引数の名前を構造体名の直後に宣言します。 そして、そうしていなければ具体的なデータ型を記述する構造体定義の箇所にジェネリックな型を使用できます。

1つのジェネリックな型だけを使用してPoint<T>を定義したので、この定義は、Point<T>構造体がなんらかの型Tに関して、 ジェネリックであると述べていてその型がなんであれ、xyのフィールドは両方その同じ型になっていることに注意してください。 リスト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なので、xyというフィールドは同じ型でなければならない

この例で、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}`

xyが両方ジェネリックだけれども、異なる型になり得るPoint構造体を定義するには、 複数のジェネリックな型引数を使用できます。例えば、リスト10-8では、Pointの定義を変更して、 型TUに関してジェネリックにし、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つの型に関してジェネリックなので、xyは異なる型の値になり得る

もう、示されたPointインスタンスは全部許可されます!所望の数だけ定義でジェネリックな型引数を使用できますが、 数個以上使用すると、コードが読みづらくなります。コードで多くのジェネリックな型が必要な時は、 コードの小分けが必要なサインかもしれません。

enum定義では

構造体のように、列挙子にジェネリックなデータ型を保持するenumを定義することができます。 標準ライブラリが提供しているOption<T> enumに別の見方をしましょう。このenumは第6章で使用しました:


# #![allow(unused_variables)]
#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_variables)]
#fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
#}

Result enumは2つの型TEに関してジェネリックで、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: 型Txフィールドへの参照を返すxというメソッドをPoint<T>構造体に実装する

ここで、フィールドxのデータへの参照を返すxというメソッドをPoint<T>に定義しました。

Point<T>にメソッドを実装していると指定するためにTを使用できるようimplの直後に宣言しなければならないことに注意してください。 implの後にTをジェネリックな型として宣言することで、コンパイラは、Pointの山カッコ内の型が、 具体的な型ではなくジェネリックな型であることを認識できるのです。

例えば、あらゆるジェネリックな型とともにPoint<T>インスタンスではなく、Point<f32>だけにメソッドを実装することもできるでしょう。 リスト10-10では、具体的な型f32を使用しています。つまり、implの後に型を宣言しません。


# #![allow(unused_variables)]
#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というメソッドが存在するが、 Tf32ではないPoint<T>の他のインスタンスにはこのメソッドが定義されないことを意味します。 このメソッドは、この点が座標(0.0, 0.0)の点からどれだけ離れているかを測定し、 浮動小数点数にのみ利用可能な数学的処理を使用します。

構造体定義のジェネリックな型引数は、必ずしもその構造体のメソッドシグニチャで使用するものと同じにはなりません。 例を挙げれば、リスト10-11は、リスト10-8のPoint<T, U>にメソッドmixupを定義しています。 このメソッドは、他のPointを引数として取り、この引数はmixupを呼び出しているselfPointとは異なる型の可能性があります。 このメソッドは、(型Tの)selfPointx値と渡した(型Wの)Pointy値から新しい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)にi32y(値は10.4)にf64を持つPointを定義しました。p2変数は、 x(値は"Hello")に文字列スライス、y(値はc)にcharを持つPoint構造体です。 引数p2p1mixupを呼び出すと、p3が得られ、xi32になります。xp1由来だからです。 p3変数は、ycharになります。yp2由来だからです。println!マクロの呼び出しは、 p3.x = 5, p3.y = cと出力するでしょう。

この例の目的は、一部のジェネリックな引数はimplで宣言され、他の一部はメソッド定義で宣言される場面をデモすることです。 ここで、ジェネリックな引数TUimplの後に宣言されています。構造体定義にはまるからです。 ジェネリックな引数VWfn mixupの後に宣言されています。何故なら、このメソッドにしか関係ないからです。

ジェネリクスを使用したコードのパフォーマンス

ジェネリックな型引数を使用すると、実行時にコストが発生するかあなたは不思議に思っている可能性があります。 コンパイラが、ジェネリクスを具体的な型がある時よりもジェネリックな型を使用したコードを実行するのが遅くならないように実装しているのは、 嬉しいお知らせです。

コンパイラはこれをジェネリクスを使用しているコードの単相化をコンパイル時に行うことで達成しています。 単相化(monomorphization)は、コンパイル時に使用されている具体的な型を入れることで、 ジェネリックなコードを特定のコードに変換する過程のことです。

この過程において、コンパイラは、リスト10-5でジェネリックな関数を生成するために使用した手順と真逆のことをしています: コンパイラは、ジェネリックなコードが呼び出されている箇所全部を見て、 ジェネリックなコードが呼び出されている具体的な型のコードを生成するのです。

標準ライブラリのOption<T> enumを使用する例でこれが動作する方法を見ましょう:


# #![allow(unused_variables)]
#fn main() {
let integer = Some(5);
let float = Some(5.0);
#}

コンパイラがこのコードをコンパイルすると、単相化を行います。その過程で、コンパイラはOption<T>のインスタンスに使用された値を読み取り、 2種類のOption<T>を識別します: 一方はi32で、もう片方はf64です。そのように、 コンパイラは、Option<T>のジェネリックな定義をOption_i32Option_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です。

NewsArticleTweetインスタンスに格納される可能性のあるデータの総括を表示するメディア総括ライブラリを作成したいです。 このために、各型からまとめが必要で、インスタンスに対してsummarizeメソッドを呼び出すことでそのまとめを要求する必要があります。 リスト10-12は、この振る舞いを表現するSummaryトレイトの定義を表示しています。

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
pub trait Summary {
    fn summarize(&self) -> String;
}
#}

リスト10-12: summarizeメソッドで提供される振る舞いからなるSummaryトレイト

ここでは、traitキーワード、それからトレイト名を使用してトレイトを定義していて、その名前は今回の場合、 Summaryです。波括弧の中にこのトレイトを実装する型の振る舞いを記述するメソッドシグニチャを定義し、 今回の場合は、fn summarize(&self) -> Stringです。

メソッドシグニチャの後に、波括弧内に実装を提供する代わりに、セミコロンを使用しています。 このトレイトを実装する型はそれぞれ、メソッドの本体に独自の振る舞いを提供しなければなりません。 コンパイラにより、Summaryトレイトを保持するあらゆる型に、このシグニチャと全く同じメソッドsummarizeが定義されていることが、 強制されます。

トレイトには、本体に複数のメソッドを含むことができます: メソッドシグニチャは行ごとに列挙され、 各行はセミコロンで終止します。

トレイトを型に実装する

今やSummaryトレイトで欲しい振る舞いを定義したので、メディア総括機で型に実装することができます。 リスト10-13は見出し、著者、場所を使用してsummarizeの戻り値を生成するNewsArticle構造体のSummaryトレイト実装を示しています。 Tweet構造体に関しては、ツイートの内容が既に280文字に限定されていることを想定して、 summarizeをユーザ名にツイート全体のテキストが続く形で定義します。

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#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トレイトをNewsArticleTweet型に実装する

型にトレイトを実装することは、普通のメソッドを実装することに似ています。違いは、implの後に、 実装したいトレイトの名前を置き、それからforキーワード、さらにトレイトの実装対象の型の名前を指定することです。 implブロック内に、トレイト定義で定義したメソッドシグニチャを置きます。各シグニチャの後にセミコロンを追記するのではなく、 波括弧を使用し、メソッド本体に特定の型のトレイトのメソッドに欲しい特定の振る舞いを入れます。

トレイトを実装後、普通の関数同様にNewsArticleTweetのインスタンスに対してメソッドを呼び出せます。 こんな感じで:

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トレイトとNewArticleTweet型を同じlib.rsに定義したので、 全部同じスコープにあることに注目してください。このlib.rsaggregatorと呼ばれるクレート専用にして、 誰か他の人が私たちのクレートの機能を活用して自分のライブラリのスコープに定義された構造体にSummaryトレイトを実装したいとしましょう。 まず、トレイトをスコープにインポートする必要があるでしょう。use aggregator::Summary;と指定してそれを行い、 これにより、自分の型にSummaryを実装することが可能になるでしょう。Summaryトレイトは、 他のクレートが実装するためには、公開トレイトである必要があり、ここでは、リスト10-12のtraitの前に、 pubキーワードを置いたのでそうなっています。

トレイト実装で注意すべき制限の1つは、トレイトか対象の型が自分のクレートにローカルである時のみ、 型に対してトレイトを実装できるということです。例えば、Displayのような標準ライブラリのトレイトをaggregatorクレートの機能の一部として、 Tweetのような独自の型に実装できます。型Tweetaggregatorクレートにローカルだからです。 また、SummaryaggregatorクレートでVec<T>に対して実装することもできます。 トレイトSummaryは、aggregatorクレートにローカルだからです。

しかし、外部のトレイトを外部の型に対して実装することはできません。例として、 aggregatorクレート内でVec<T>に対してDisplayトレイトを実装することはできません。 DisplayVec<T>は標準ライブラリで定義され、aggregatorクレートにローカルではないからです。 この制限は、コヒーレンス(coherence)あるいは、具体的にオーファンルール(orphan rule)と呼ばれるプログラムの特性の一部で、 親の型が存在しないためにそう命名されました。この規則により、他の人のコードが自分のコードを壊したり、 その逆が起きないことを保証してくれます。この規則がなければ、2つのクレートが同じ型に対して同じトレイトを実装できてしまい、 コンパイラはどちらの実装を使うべきかわからなくなってしまうでしょう。

デフォルト実装

時として、全ての型の全メソッドに対して実装を必要とするのではなく、トレイトの全てあるいは一部のメソッドに対してデフォルトの振る舞いがあると有用です。 そうすれば、特定の型にトレイトを実装する際、各メソッドのデフォルト実装を保持するかオーバーライドできるわけです。

リスト10-14は、リスト10-12のように、メソッドシグニチャだけを定義するのではなく、 Summaryトレイトのsummarizeメソッドにデフォルトの文字列を指定する方法を示しています:

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
pub trait Summary {
    fn summarize(&self) -> String {
        // (もっと読む)
        String::from("(Read more...)")
    }
}
#}

リスト10-14: summarizeメソッドのデフォルト実装があるSummaryトレイトの定義

デフォルト実装を使用して独自の実装を定義するのではなく、NewsArticleのインスタンスをまとめるには、 impl Summary for NewsArticle {}と空のimplブロックを指定します。

たとえ、最早NewsArticleに直接summarizeメソッドを定義することはなくても、デフォルト実装を提供し、 NewsArticleSummaryトレイトを実装すると指定しました。結果的に、それでも、 NewsArticleのインスタンスに対してsummarizeメソッドを呼び出すことができます。 このように:

let article = NewsArticle {
    // ペンギンチームがスタンレーカップチャンピオンシップを勝ち取る!
    headline: String::from("Penguins win the Stanley Cup Championship!"),
    location: String::from("Pittsburgh, PA, USA"),
    author: String::from("Iceburgh"),
    // ピッツバーグ・ペンギンが再度NHLで最強のホッケーチームになった
    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のTweetSummary実装を変える必要はありません。 理由は、デフォルト実装をオーバーライドする記法がデフォルト実装のないトレイトメソッドを実装する記法と同じだからです。

デフォルト実装は、他のデフォルト実装がないメソッドでも呼び出すことができます。 このように、トレイトは多くの有用な機能を提供しつつ、実装者に僅かな部分だけ指定してもらう必要しかないのです。 例えば、Summaryトレイトを実装が必須のsummarize_authorメソッドを持つように定義し、 それからsummarize_authorメソッドを呼び出すデフォルト実装のあるsummarizeメソッドを定義することもできます:


# #![allow(unused_variables)]
#fn main() {
pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        // {}からもっと読む
        format!("(Read more from {}...)", self.summarize_author())
    }
}
#}

このバージョンのSummaryを使用するには、型にトレイトを実装する際にsummarize_authorを定義する必要だけあります:

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

summarize_author定義後、Tweet構造体のインスタンスに対してsummarizeを呼び出せ、 summarizeのデフォルト実装は、提供済みのsummarize_authorの定義を呼び出すでしょう。 summarize_authorを実装したので、追加のコードを書く必要なく、Summaryトレイトは、 summarizeメソッドの振る舞いを与えてくれました。

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...)と出力します。

同じメソッドのオーバーライドした実装からは、デフォルト実装を呼び出すことができないことに注意してください。

トレイト境界

これでトレイトの定義とトレイトを型に実装する方法を知ったので、ジェネリックな型引数でトレイトを使用する方法を探求できます。 トレイト境界を使用してジェネリックな型を制限し、型が特定のトレイトや振る舞いを実装するものに制限されることを保証できます。

例として、リスト10-13で、Summaryトレイトを型NewsArticleTweetに実装しました。 引数itemに対してsummarizeメソッドを呼び出す関数notifyを定義でき、この引数はジェネリックな型Tです。 ジェネリックな型Tがメソッドsummarizeを実装しないというエラーを出さずにitemsummarizeを呼び出せるために、 Tに対してトレイト境界を使用してitemは、Summaryトレイトを実装する型でなければならないと指定できます:

pub fn notify<T: Summary>(item: T) {
    // 衝撃的なニュース! {}
    println!("Breaking news! {}", item.summarize());
}

トレイト境界をジェネリックな型引数宣言とともにコロンの後、山カッコ内に配置しています。Tに対するトレイト境界のため、 notifyを呼び出してNewsArticleTweetのどんなインスタンスも渡すことができます。 あらゆる他の型、Stringi32などでこの関数を呼び出すコードは、型がSummaryを実装しないので、 コンパイルできません。

+記法でジェネリックな型に対して複数のトレイト境界を指定できます。例えば、関数でTに対してフォーマット表示と、 summarizeメソッドを使用するには、T: Summary + Displayを使用して、TSummaryDisplayを実装するどんな型にもなると宣言できます。

しかしながら、トレイト境界が多すぎると欠点もあります。各ジェネリックには、特有のトレイト境界があるので、 複数のジェネリックな型引数がある関数には、関数名と引数リストの間に多くのトレイト境界の情報が付くこともあり、 関数シグニチャが読みづらくなる原因になります。このため、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
{

この関数シグニチャは、多くのトレイト境界のない関数のように、関数名、引数リスト、戻り値の型が一緒になって近いという点でごちゃごちゃしていません。

トレイト境界でlargest関数を修正する

ジェネリックな型引数の境界で使用したい振る舞いを指定する方法を知ったので、リスト10-5に戻って、 ジェネリックな型引数を使用するlargest関数の定義を修正しましょう!最後にそのコードを実行しようとしたら、 こんなエラーが出ました:

error[E0369]: binary operation `>` cannot be applied to type `T`
 --> src/main.rs:5:12
  |
5 |         if item > largest {
  |            ^^^^^^^^^^^^^^
  |
  = note: an implementation of `std::cmp::PartialOrd` might be missing for `T`

largestの本体で、大なり演算子(>)を使用して型Tの2つの値を比較したかったのです。その演算子は、 標準ライブラリトレイトのstd::cmp::PartialOrdでデフォルトメソッドとして定義されているので、 largest関数が、比較できるあらゆる型のスライスに対して動くようにTのトレイト境界にPartialOrdを指定する必要があります。 初期化処理に含まれているので、PartialOrdをスコープに導入する必要はありません。 largestのシグニチャを以下のような見た目に変えてください:

fn largest<T: PartialOrd>(list: &[T]) -> T {

今度コードをコンパイルすると、異なる一連のエラーが出ます:

error[E0508]: cannot move out of type `[T]`, a non-copy slice
(エラー: `[T]`、コピーでないスライスからムーブできません。)
 --> src/main.rs:2:23
  |
2 |     let mut largest = list[0];
  |                       ^^^^^^^
  |                       |
  |                       cannot move out of here
  |                       help: consider using a reference instead: `&list[0]`
                         (助言: 代わりに参照の使用を考慮してください: `&list[0]`)

error[E0507]: cannot move out of borrowed content
(エラー: 借用された内容からムーブできません)
 --> src/main.rs:4:9
  |
4 |     for &item in list.iter() {
  |         ^----
  |         ||
  |         |hint: to prevent move, use `ref item` or `ref mut item`
  |         cannot move out of borrowed content
            (ヒント: ムーブを避けるには、`ref item`か`ref mut item`を使用してください)

このエラーの鍵となる行は、cannot move out of type [T], a non-copy sliceです。 ジェネリックでないバージョンのlargest関数では、最大のi32charを探そうとするだけでした。 第4章の「スタックだけのデータ: コピー」節で議論したように、i32charのような既知のサイズの型は、 スタックに格納できるので、Copyトレイトを実装しています。しかし、largest関数をジェネリックにすると、 list引数がCopyトレイトを実装しない型を含む可能性も出てきたのです。結果として、 list[0]から値をムーブできず、largestにムーブできず、このエラーに落ち着いたのです。

このコードをCopyトレイトを実装する型とだけで呼び出すには、Tのトレイト境界にCopyを追加できます! リスト10-15は、関数に渡したスライスの値の型がi32charなどのように、PartialOrdCopyを実装する限り、 コンパイルできるジェネリックなlargest関数の完全なコードを示しています。

ファイル名: src/main.rs

fn largest<T: PartialOrd + Copy>(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-15: PartialOrdCopyトレイトを実装するあらゆるジェネリックな型に対して動く、 largest関数の動く定義

もしlargest関数をCopyを実装する型だけに制限したくなかったら、Copyではなく、 TがCloneというトレイト境界を含むと指定することもできます。そうしたら、 largest関数に所有権が欲しい時にスライスの各値をクローンできます。clone関数を使用するということは、 Stringのようなヒープデータを所有する型の場合にもっとヒープ確保が発生する可能性があることを意味し、 大きなデータを取り扱っていたら、ヒープ確保は遅いこともあります。

largestの別の実装方法は、関数がスライスのT値への参照を返すようにすることです。 戻り値の型をTではなく&Tに変え、それにより関数の本体を参照を返すように変更したら、 CloneCopyトレイト境界は必要なくなり、ヒープ確保も避けられるでしょう。 試しにこれらの対立的な解決策もご自身で実装してみてください!

トレイト境界を使用して、メソッド実装を条件分けする

implブロックでジェネリックな型引数を使用するトレイト境界を活用することで、 特定のトレイトを実装する型に対するメソッド実装を条件分けできます。例えば、 リスト10-16の型Pair<T>は、常にnew関数を実装します。しかし、Pair<T>は、 内部の型Tが比較を可能にするPartialOrdトレイト出力を可能にするDisplayトレイトを実装している時のみ、 cmp_displayメソッドを実装します。


# #![allow(unused_variables)]
#fn main() {
use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self {
            x,
            y,
        }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}
#}

リスト10-16: トレイト境界によってジェネリックな型に対するメソッド実装を条件分けする

また、別のトレイトを実装するあらゆる型に対するトレイト実装を条件分けすることもできます。 トレイト境界を満たすあらゆる型にトレイトを実装することは、ブランケット実装(blanket implementation)と呼ばれ、 Rustの標準ライブラリで広く使用されています。例を挙げれば、標準ライブラリは、 Displayトレイトを実装するあらゆる型にToStringトレイトを実装しています。 標準ライブラリのimplブロックは以下のような見た目です:

impl<T: Display> ToString for T {
    // --snip--
}

標準ライブラリにはこのブランケット実装があるので、Displayトレイトを実装する任意の型に対して、 ToStringトレイトで定義されたto_stringメソッドを呼び出せるのです。 例えば、整数はDisplayを実装するので、このように整数値を対応するString値に変換できます:


# #![allow(unused_variables)]
#fn main() {
let s = 3.to_string();
#}

ブランケット実装は、「実装したもの」節のトレイトのドキュメンテーションに出現します。

トレイトとトレイト境界により、ジェネリックな型引数を使用して重複を減らしつつ、コンパイラに対して、 そのジェネリックな型に特定の振る舞いが欲しいことを指定するコードを書くことができます。 それからコンパイラは、トレイト境界の情報を活用してコードに使用された具体的な型が正しい振る舞いを提供しているか確認できます。 動的型付け言語では、型が実装しない型のメソッドを呼び出せば、実行時にエラーが出るでしょう。 しかし、Rustはこの種のエラーをコンパイル時に移したので、コードが動かせるようにさえなる以前に問題を修正することを強制されるのです。 加えて、コンパイル時に既に確認したので、実行時に振る舞いがあるかどう確認するコードを書かなくても済みます。 そうすることでジェネリクスの柔軟性を諦める必要なく、パフォーマンスを向上させます。

もう使用したことのある別の種のジェネリクスは、ライフタイムと呼ばれます。 型が欲しい振る舞いを保持していることを保証するのではなく、必要な間だけ参照が有効であることをライフタイムは保証します。 ライフタイムがどうやってそれを行うかを見ましょう。

ライフタイムで参照を有効化する

第4章の「参照と借用」節で議論しなかった詳細の1つは、Rustにおいて参照は全てライフタイムを保持することであり、 ライフタイムとは、その参照が有効になるスコープのことです。多くの場合、型が推論されるように、 多くの場合、ライフタイムも暗黙的に推論されます。複数の型の可能性があるときには、型を注釈しなければなりません。 同様に、参照のライフタイムがいくつか異なる方法で関係することがある場合には注釈しなければなりません。 コンパイラは、ジェネリックライフタイム引数を使用して関係を注釈し、実行時に実際の参照が確かに有効であることを保証することを要求するのです。

ライフタイムの概念は、ほかのプログラミング言語の道具とはどこか異なり、議論はあるでしょうが、 ライフタイムがRustで一番際立った機能になっています。この章では、ライフタイムの全てを講義しないものの、 ライフタイム記法と遭遇する可能性のある一般的な手段を議論するので、概念に馴染めます。 もっと詳しく知るには、第19章の「高度なライフタイム」節を参照されたし。

ライフタイムでダングリング参照を回避する

ライフタイムの主な目的は、ダングリング参照を回避することであり、ダングリング参照によりプログラムは、 参照することを意図したデータ以外のデータを参照してしまいます。リスト10-17のプログラムを考えてください。 これには、外側のスコープと内側のスコープが含まれています。

{
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}

リスト10-17: 値がスコープを抜けてしまった参照を使用しようとする

注釈: リスト10-17や10-18、10-24では、変数に初期値を与えずに宣言しているので、変数名は外側のスコープに存在します。 初見では、これはRustにはnull値が存在しないということと衝突しているように見える可能性があります。 しかしながら、値を与える前に変数を使用しようとすれば、コンパイルエラーになり、 確かにRustではnull値は許可されないことを示します。

外側のスコープで初期値なしのrという変数を宣言し、内側のスコープで初期値5のxという変数を宣言しています。 内側のスコープ内で、rの値をxへの参照にセットしようとしています。それから内側のスコープが終わり、 rの値を出力しようとしています。rが参照している値が使おうとする前にスコープを抜けるので、 このコードはコンパイルできません。こちらがエラーメッセージです:

error[E0597]: `x` does not live long enough
(エラー: `x`の生存期間が短すぎます)
  --> src/main.rs:7:5
   |
6  |         r = &x;
   |              - borrow occurs here
   |              (借用はここで起きています)
7  |     }
   |     ^ `x` dropped here while still borrowed
   |     (`x`は借用されている間にここでドロップされました)
...
10 | }
   | - borrowed value needs to live until here
   | (借用された値はここまで生きる必要があります)

変数xの「生存期間が短すぎます。」原因は内側のスコープが7行目で終わった時点でxがスコープを抜けるからです。 ですが、rはまだ、外側のスコープに対して有効です; スコープが大きいので、「長生きする」と言います。 Rustで、このコードが動くことを許可していたら、rxがスコープを抜けた時に解放されるメモリを参照していることになり、 rで行おうとするいかなることもちゃんと動かないでしょう。では、どうやってコンパイラはこのコードが無効であると決定しているのでしょうか? 借用精査機(borrow checker)を使用しています。

借用精査機

Rustコンパイラには、スコープを比較して全ての借用が有効であるかを決定する借用精査機があります。 リスト10-18は、リスト10-17と同じコードを示していますが、変数のライフタイムを表示する注釈が付いています:

{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

リスト10-18: それぞれ'a'bと名付けられたrxのライフタイムの注釈

ここで、rのライフタイムは'axのライフタイムは'bで注釈しました。ご覧の通り、 内側の'bブロックの方が、外側の'aライフタイムブロックよりはるかに小さいです。 コンパイル時に、コンパイラは2つのライフタイムのサイズを比較し、r'aのライフタイムだけれども、 'bのライフタイムのメモリを参照していると確認します。'b'aよりも短いので、プログラムは拒否されます: 参照の被写体が参照ほど長生きしないのです。

リスト10-19でコードを修正したので、ダングリング参照はなくなり、エラーなくコンパイルできます。


# #![allow(unused_variables)]
#fn main() {
{
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+
#}

リスト10-19: データのライフタイムが参照より長いので、有効な参照

ここでxのライフタイムは'bになり、今回の場合'aよりも大きいです。つまり、 コンパイラはxが有効な間、rの参照も常に有効になることを把握しているので、rxを参照できます。

今や、参照のライフタイムがどこにあり、コンパイラがライフタイムを解析して参照が常に有効であることを保証する仕組みがわかったので、 関数の文脈でジェネリックな引数と戻り値のライフタイムを探求しましょう。

関数のジェネリックなライフタイム

2つの文字列スライスのうち、長い方を返す関数を書きましょう。この関数は、 2つの文字列スライスを取り、1つの文字列スライスを返します。longest関数の実装完了後、 リスト10-20のコードは、The logest string is abcdと出力するはずです。

ファイル名: src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    // 最長の文字列は、{}です
    println!("The longest string is {}", result);
}

リスト10-20: longest関数を呼び出して2つの文字列スライスのうち長い方を探すmain関数

関数に取ってほしい引数が文字列スライス、つまり参照であることに注意してください。 何故なら、longest関数に引数の所有権を奪ってほしくないからです。 この関数にStringのスライス(変数string1に格納されている型)と文字列リテラル(変数string2が含むもの)を受け取らせたいのです。

リスト10-20で使用している引数が求めているものである理由についてもっと詳しい議論は、 第4章の「引数としての文字列スライス」節をご参照ください。

リスト10-21に示したようにlongetst関数を実装しようとしたら、コンパイルできないでしょう。

ファイル名: src/main.rs

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

リスト10-21: 2つの文字列スライスのうち長い方を返すけれども、コンパイルできないlongest関数の実装

代わりに、以下のようなライフタイムに言及するエラーが出ます:

error[E0106]: missing lifetime specifier
(エラー: ライフタイム指定子が不足しています)
 --> src/main.rs:1:33
  |
1 | fn longest(x: &str, y: &str) -> &str {
  |                                 ^ expected lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `x` or `y`
  (助言: この関数の戻り値型は借用された値を含んでいますが、
シグニチャは、それが`x`か`y`由来のものなのか宣言していません)

助言テキストが戻り値の型はジェネリックなライフタイム引数である必要があると明かしています。 というのも、返している参照がxyを参照しているかコンパイラにはわからないからです。 この関数の本体のifブロックはxへの参照を返し、elseブロックはyへの参照を返すので、 実際、どちらか私たちにもわかりません!

この関数を定義する際、この関数に渡される具体的な値がわからないので、ifケースか、elseケースが実行されるか、わからないのです。 また、渡される参照の具体的なライフタイムもわからないので、リスト10-18と10-19で、 返す参照が常に有効であるかを決定したようにスコープを見ることもできないのです。 借用精査機もこれを決定することはできません。xyのライフタイムがどう戻り値のライフタイムと関係するかわからないからです。 このエラーを修正するには、借用精査機が解析を実行できるように、参照間の関係を定義するジェネリックなライフタイム引数を追加します。

ライフタイム注釈記法

ライフタイム注釈は、いかなる参照の生存期間も変えることはありません。シグニチャがジェネリックな型引数を指定している時に、 関数があらゆる型を受け入れるのと全く同様に、ジェネリックなライフタイム引数を指定することで関数は、 あらゆるライフタイムの参照を受け入れるのです。ライフタイム注釈は、ライフタイムに影響することなく、 複数の参照のライフタイムのお互いの関係を記述します。

ライフタイム注釈は、少しだけ不自然な記法です: ライフタイム引数の名前はアポストロフィー(')で始まらなければならず、 通常全部小文字で、ジェネリック型のようにとても短いです。多くの人は、'aという名前を使います。 ライフタイム引数注釈は、参照の&の後に配置し、注釈と参照の型を区別するために空白を1つ使用します。

例を挙げましょう: ライフタイム引数なしのi32への参照、'aというライフタイム引数付きのi32への参照、 これもライフタイム'a付きi32への可変参照です。

            // (ただの)参照
&i32        // a reference
            // 明示的なライフタイム付きの参照
&'a i32     // a reference with an explicit lifetime
            // 明示的なライフタイム付きの可変参照
&'a mut i32 // a mutable reference with an explicit lifetime

1つのライフタイム注釈それだけでは、大して意味はありません。注釈は、複数の参照のジェネリックなライフタイム引数が、 お互いにどう関係するかをコンパイラに指示することを意図しているからです。例えば、 ライフタイム'a付きのi32への参照となる引数firstのある関数があるとしましょう。 この関数にはさらに、'aのライフタイム付きのi32への別の参照となるsecondという別の引数もあります。 ライフタイム注釈は、firstsecondの参照がどちらもジェネリックなライフタイムと同じだけ生きることを示唆します。

関数シグニチャにおけるライフタイム注釈

さて、longest関数の文脈でライフタイム注釈を調査しましょう。ジェネリックな型引数同様、 関数名と引数リストの間、山カッコの中にジェネリックなライフタイム引数を宣言する必要があります。 このシグニチャで表現したい制約は、引数の全参照と戻り値が同じライフタイムになることです。 ライフタイムを'aと名付け、それから各参照に追記します。リスト10-22に示したように。

ファイル名: src/main.rs


# #![allow(unused_variables)]
#fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
#}

リスト10-22: シグニチャの全参照が同じライフタイム'aになると指定したlongest関数の定義

このコードはコンパイルでき、リスト10-20のmain関数とともに使用したら、欲しい結果になるはずです。

これで関数シグニチャは、なんらかのライフタイム'aに対して、関数は2つの引数を取り、 どちらも少なくともライフタイム'aと同じだけ生きる文字列スライスであるとコンパイラに教えています。 また、この関数シグニチャは、関数から返る文字列スライスも少なくともライフタイム'aと同じだけ生きると、 コンパイラに教えています。これらの制約は、コンパイラに強制してほしいものです。 この関数シグニチャでライフタイム引数を指定する時、渡されたり、返したりしたいかなる値のライフタイムも変更していないことを思い出してください。 むしろ、借用精査機は、これらの制約を支持しない値全てを拒否するべきと指定しています。 longest関数は、正確にxyの生存期間を知る必要はなく、何かのスコープが'aに代替され、 このシグニチャを満足することだけ知っている必要があることに注意してください。

関数でライフタイムを注釈する際、注釈は関数シグニチャに(はま)り、 関数本体には嵌りません。コンパイラは、なんの助けもなく、関数内のコードを解析できます。しかしながら、 関数に関数外やからの参照がある場合、コンパイラは引数や戻り値のライフタイムをそれだけではじき出すことはほとんど不可能になります。 ライフタイムは、関数が呼び出される度に異なる可能性があります。このために、手動でライフタイムを注釈する必要があるのです。

具体的な参照をlongestに渡すと、'aを代替する具体的なライフタイムは、yのスコープと被さるxのスコープの一部になります。 言い換えると、ジェネリックなライフタイム'aは、xyのライフタイムのうち、小さい方に等しい具体的なライフタイムになるのです。 返却される参照を同じライフタイム引数'aで注釈したので、返却される参照もxyのライフタイムの小さい方と同じだけ有効になるでしょう。

ライフタイム注釈が異なる具体的なライフタイムになる参照を渡すことでlongest関数を制限する方法を見ましょう。 リスト10-23は、率直な例です。

ファイル名: src/main.rs

# fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
#     if x.len() > y.len() {
#         x
#     } else {
#         y
#     }
# }
#
fn main() {
    // 長い文字列は長い
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
}

リスト10-23: 異なる具体的なライフタイムのString値への参照でlongest関数を使用する

この例において、string1は外側のスコープの終わりまで有効で、string2は内側のスコープの終わりまで有効、 そしてresultは内側のスコープの終わりまで有効な何かを参照しています。このコードを実行すると、 借用精査機がこのコードに賛成するのがわかるでしょう。要するに、コンパイルでき、 The longest string is long string is longと出力するのです。

次に、resultの参照のライフタイムが2つの引数の小さい方のライフタイムになることを示す例を試しましょう。 result変数の宣言を内側のスコープの外に移すものの、result変数への代入はstring2のスコープ内に残したままにします。 それからresultを使用するprintln!を内側のスコープの外、内側のスコープが終わった後に移動します。 リスト10-24のコードはコンパイルできません。

ファイル名: src/main.rs

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

リスト10-24: string2がスコープを抜けてからresultを使用しようとする

このコードのコンパイルを試みると、こんなエラーになります:

error[E0597]: `string2` does not live long enough
  --> src/main.rs:15:5
   |
14 |         result = longest(string1.as_str(), string2.as_str());
   |                                            ------- borrow occurs here
15 |     }
   |     ^ `string2` dropped here while still borrowed
16 |     println!("The longest string is {}", result);
17 | }
   | - borrowed value needs to live until here

このエラーは、resultprintln!文に対して有効になるために、string2が外側のスコープの終わりまで有効である必要があることを示しています。 関数引数と戻り値のライフタイムを同じライフタイム引数'aで注釈したので、コンパイラはこのことを知っています。

人間からしたら、このコードを見てstring1string2よりも長いことが確認でき、 故にresultstring1への参照を含んでいます。まだstring1はスコープを抜けていないので、 それでもstring1への参照はprintln!にとって有効でしょう。ですが、コンパイラはこの場合、 参照が有効であると見なせません。longest関数から返ってくる参照のライフタイムは、 渡した参照のうちの小さい方と同じだとコンパイラに指示しました。それ故に、 借用精査機は、リスト10-24のコードを無効な参照がある可能性があるとして許可しないのです。

試しに値やlongest関数に渡される参照のライフタイムや返される参照の使用法が異なる実験をもっと企ててみてください。 自分の実験がコンパイル前に借用精査機を通るかどうか仮説を立ててください; そして、正しいか確かめてください!

ライフタイムの観点で思考する

ライフタイム引数を指定する必要のある手段は、関数が行っていることによります。例えば、 longest関数の実装を最長の文字列スライスではなく、常に最初の引数を返すように変更したら、 y引数に対してライフタイムを指定する必要はなくなるでしょう。以下のコードはコンパイルできます:

ファイル名: src/main.rs


# #![allow(unused_variables)]
#fn main() {
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}
#}

この例では、引数xと戻り値に対してライフタイム引数'aを指定しましたが、引数yには指定していません。 yのライフタイムはxや戻り値のライフタイムとは何の関係もないからです。

関数から参照を返す際、戻り値型のライフタイム引数は、引数のうちどれかのライフタイム引数と一致する必要があります。 返される参照が引数のどれかを参照していなければ、この関数内で生成された値を参照しているに違いなく、 これは、その値が関数の末端でスコープを抜けるので、ダングリング参照になるでしょう。 コンパイルできないこのlongest関数の未遂の実装を考えてください:

ファイル名: src/main.rs

fn longest<'a>(x: &str, y: &str) -> &'a str {
    // 本当に長い文字列
    let result = String::from("really long string");
    result.as_str()
}

ここでは、たとえ、戻り値型にライフタイム引数'aを指定していても、戻り値のライフタイムは、 引数のライフタイムと全く関係がないので、この実装はコンパイルできないでしょう。 こちらが、得られるエラーメッセージです:

error[E0597]: `result` does not live long enough
 --> src/main.rs:3:5
  |
3 |     result.as_str()
  |     ^^^^^^ does not live long enough
4 | }
  | - borrowed value only lives until here
  |
note: borrowed value must be valid for the lifetime 'a as defined on the
function body at 1:1...
(注釈: 借用された値は、関数本体1行目1文字目で定義されているようにライフタイム'aに対して有効でなければなりません)
 --> src/main.rs:1:1
  |
1 | / fn longest<'a>(x: &str, y: &str) -> &'a str {
2 | |     let result = String::from("really long string");
3 | |     result.as_str()
4 | | }
  | |_^

問題は、resultlongest関数の末端でスコープを抜け、片付けられてしまうことです。 また、関数からresultを返そうともしています。ダングリング参照を変えるであろうライフタイム引数を指定する手段はなく、 コンパイラは、ダングリング参照を生成させてくれません。今回の場合、最善の修正案は、 呼び出し元の関数が値の片付けに責任を持てるよう、参照ではなく所有されたデータ型を返すことでしょう。

究極的にライフタイム記法は、関数のいろんな引数と戻り値のライフタイムを接続することに関するのです。 一旦、繋がりができたら、メモリ安全な処理を許可するのに十分な情報がコンパイラにはあり、 ダングリングポインタを生成するであろう処理を不認可し、さもなくばメモリ安全性を侵害するのです。

構造体定義のライフタイム注釈

ここまで、所有された型を保持する構造体だけを定義してきました。構造体に参照を保持させることもできますが、 その場合、構造体定義の全参照にライフタイム注釈を付け加える必要があるでしょう。 リスト10-25には、文字列スライスを保持するImportantExcerptという構造体があります。

ファイル名: src/main.rs

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    // 僕をイシュマエルとお呼び。何年か前・・・
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.')
        .next()
        .expect("Could not find a '.'");  // '.'が見つかりませんでした
    let i = ImportantExcerpt { part: first_sentence };
}

リスト10-25: 参照を含む構造体なので、定義にライフタイム注釈が必要

この構造体には文字列スライスを保持する1つのフィールド、partがあり、これは参照です。 ジェネリックなデータ型同様、構造体名の後、山カッコの中にジェネリックなライフタイム引数の名前を宣言するので、 構造体定義の本体でライフタイム引数を使用できます。この注釈は、ImportantExcerptのインスタンスが、 partフィールドに保持している参照よりも長生きしないことを意味します。

ここのmain関数は、変数novelに所有されるStringの最初の文への参照を保持する