Rust by Example
Rust は安全性、速度、並列性にフォーカスした現代的なシステムプログラミング用のプログラミング言語です。ガベージコレクション無しでメモリ安全であることが、これを可能にしています。
Rust by Example(RBE)はRustの実行可能なサンプルスクリプト集で、ここではRustの様々なコンセプトと標準ライブラリを紹介していきます。この例をより活用するためにはRustをローカルにインストールし、公式ドキュメントをチェックすることをおすすめします。興味がある方はこのサイト自体のソースのチェックもどうぞ。
それでははじめましょう!
-
Hello World - お決まりのHello Worldプログラムから始めましょう。
-
基本データ型 - 符号付き整数や符号無し整数、その他の基本データ型について学びましょう。
-
カスタム型 -
struct
とenum
について。 -
変数の束縛 - ミュータブルな束縛、スコープ、シャドーイングについて。
-
型 - 型を変更したり定義したりすることを学びましょう。
-
型変換 - 文字列や整数、浮動小数点数など様々な型から型への変換について。
-
式 - 式とその使い方について学びましょう。
-
制御フロー -
if
やelse
、for
など。 -
関数 - メソッド、クロージャ、高階関数について。
-
モジュール - プログラムをモジュールを使って整理しましょう。
-
クレート - クレートは、Rustにおいてコンパイルされる単位です。ライブラリの作り方について学びます。
-
Cargo - Rustの公式パッケージマネージャの基本的な機能を学びます。
-
アトリビュート - アトリビュートは、モジュールやクレート、要素に適用されるメタデータです。
-
ジェネリクス - 様々な型の引数を取れる関数やデータ型を書く方法を学びましょう。
-
スコープの規則 - スコープは所有権、借用、ライフタイムにおいて重要な役割を果たします。
-
トレイト - トレイトとは、未知の型
Self
に対して定義された一連のメソッドです。 -
マクロ - マクロはコードを書くためのコードです。メタプログラミングとしても知られています。
-
エラーハンドリング - 失敗に対処するRust流のやり方を学びましょう。
-
標準ライブラリの型 -
std
ライブラリによって提供されるいくつかのカスタム型について学びます。 -
標準ライブラリのその他 - ファイルハンドリングとスレッドのためのカスタム型について。
-
テスト - Rustにおけるテストのすべて。
-
安全でない操作 - 安全でない操作について学びましょう。
-
互換性 - Rustの進化と互換性について。
-
周辺情報 - ドキュメント、ベンチマークの方法。
Hello World
ここでは伝統的な "Hello World!" プログラムのソースを紹介します。
println!
は文字列をコンソールに出力するための マクロ です。
バイナリファイルはrustc
と呼ばれるRustコンパイラを用いて生成することができます。
$ rustc hello.rs
するとhello
という名前の実行可能なバイナリファイルができます。
$ ./hello
Hello World!
演習
上に書いている 'Run' をクリックしてアウトプットを見てみましょう。次に、println!
マクロをもう一行追加してアウトプットがどうなるか見てみましょう。
Hello World!
I'm a Rustacean!
コメント
あらゆるプログラムにはコメントが必要です。Rustには何種類かのコメントがあります
- 通常のコメント これはコンパイラによって完全に無視されます。
// 行末までコメントアウト
/* ブロックによって囲まれた部分をコメントアウト */
- ドキュメンテーションコメント ライブラリのドキュメンテーションとしてHTMLにパースされます。
/// このコメントの下の内容に関するドキュメントとなります。
//! このコメントを含むソースのドキュメントになります。
参照
フォーマットして出力
Printing is handled by a series of macros
defined in std::fmt
some of which are:
format!
:フォーマットされたテキストをString
に書き込みます。print!
:format!
と同様ですが、コンソール (io::stdout) にそのテキストを出力します。println!
:print!
と同じですが改行が付け加えられます。eprint!
:format!
と同様ですが、標準エラー出力 (io::stderr) にそのテキストを出力します。eprintln!
:eprint!
と同じですが改行が付け加えられます。
すべて同じやり方でテキストをパースし、正しくフォーマットできるかコンパイル時にチェックします。
std::fmt
はいくつものトレイトを持ち、それによってどのようにディスプレイに表示されるかが決まります。特に大事な形式は以下の2つです。
fmt::Debug
:{:?}
というマーカーを使用し、デバッグ目的に使われます。fmt::Display
:{}
というマーカーを使用し、より美しく、ユーザフレンドリーに表示します。
この例で用いられている型は、標準ライブラリに含まれているため、ここではfmt::Display
を使用しています。カスタム型をテキストとして表示する場合は、さらに手順が必要です。
fmt::Display
トレイトを実装すると、自動的にToString
トレイトが実装されます。これによりString
型への型変換ができるようになります。
43行目 の#[allow(dead_code)]
は、直後のモジュールにのみ適用されるアトリビュートです。
演習
- 上の例(FIXME を参照)を実行した際に生じるエラーを修復しましょう。
Structure
構造体をフォーマットする行をアンコメントしてみましょう。(TODO を参照)println!
マクロを追加し、表示される小数部の桁数を調整してPi is roughly 3.142
という文字列を出力しましょう。ただし、円周率の値はlet pi = 3.141592
を使ってください。(ヒント:小数部の桁数を調整する方法については、std::fmt
をチェックする必要があるかもしれません。)
参照
std::fmt
, マクロ, 構造体, トレイト, dead_code
Debug
std::fmt
のフォーマット用トレイト
を使用したい型は、出力できるように実装されている必要があります。std
ライブラリの型のように自動で出力可能なものもありますが、他はすべて 手動で実装する必要があります。
fmt::Debug
というトレイト
はこれを簡略化します。 すべての 型はfmt::Debug
の実装を導出(derive)
、(すなわち自動で作成)することができるためです。fmt::Display
の場合はやはり手動で実装しなくてはなりません。
std
ライブラリの型の場合は、自動的に{:?}
により出力可能になっています。
fmt::Debug
は確実に出力可能にしてくれるのですが、一方である種の美しさを犠牲にしています。Rustは{:#?}
による「見栄えの良い出力」も提供します。
手動でfmt::Display
を実装することで出力結果を思い通りにできます。
参照
アトリビュート, derive
, std::fmt
, 構造体
Display
fmt::Debug
はコンパクトでクリーンであるようには見えませんね。大抵の場合は、アウトプットの見た目をカスタマイズしたほうが好ましいでしょう。これは{}
を使用するfmt::Display
を手動で実装することで可能です。実装はこのようになります。
fmt::Display
はfmt::Debug
より綺麗かもしれませんが、std
ライブラリの場合は問題が生じます。曖昧な型はどのように表示すれば良いでしょう?例えば、std
ライブラリがあらゆるVec<T>
に対して単一のスタイルを提供していた場合、どのようなスタイルに整形すればよいでしょう?以下の2つのどちらかを選ぶべきでしょうか?
Vec<path>
:/:/etc:/home/username:/bin
(:
で分割)Vec<number>
:1,2,3
(,
で分割)
答えはNOです。あらゆる型に対して理想的なスタイルなどというものはありませんし、std
ライブラリによってそれが提供されているわけでもありません。fmt::Display
はVec<T>
のようなジェネリックなコンテナ用に定義されているわけではありませんので、このような場合はfmt::Debug
を使用するべきです。
ジェネリック でない コンテナ型の場合は、このような問題は生じませんので問題なくfmt::Display
を実装することができます。
fmt::Display
は実装されていますが、fmt::Binary
はされていないので使用できません。std::fmt
にはそのようなトレイトが数多くあり、それぞれに独自の実装が必要です。詳しくはstd::fmt
を参照してください。
演習
上記の例のアウトプットを確認し、Point2D
構造体を参考として、複素数を格納するためのComplex
構造体を定義しましょう。うまく行けば以下のように出力されるはずです。
Display: 3.3 + 7.2i
Debug: Complex { real: 3.3, imag: 7.2 }
参照
導出, std::fmt
, マクロ, 構造体, トレイト, use
テストケース:リスト
構造体のそれぞれの要素を別々に扱うfmt::Display
を実装するのはトリッキーです。というのも、それぞれのwrite!
が別々のfmt::Result
を生成するためです。適切に処理するためには すべての 結果に対して処理を書かなくてはなりません。このような場合は?
演算子が使えます。
以下のように?
をwrite!
に対して使用します。
// `write!`を実行し、エラーが生じた場合はエラーを返します。そうでなければ実行を継続します。
write!(f, "{}", value)?;
?
を使用すれば、Vec
用のfmt::Display
はより簡単に実装できます。
演習
上記のプログラムを変更して、ベクタの各要素のインデックスも表示するようにしてみましょう。変更後の出力は次のようになります。
[0: 1, 1: 2, 2: 3]
参照
for
, ref
, Result
, 構造体, ?
, vec!
フォーマット
これまで、文字列がどのようにフォーマットされるかは フォーマット文字列 によって決まるということを見てきました 。
format!("{}", foo)
->"3735928559"
format!("0x{:X}", foo)
->"0xDEADBEEF"
format!("0o{:o}", foo)
->"0o33653337357"
ここでは(foo
)という単一の変数がX
、o
、 指定なし 、という様々な 引数タイプ に応じてフォーマットされています。
フォーマットの機能はそれぞれの引数タイプごとに個別のトレイトを用いて実装されています。最も一般的なトレイトはDisplay
で、これは引数タイプが未指定(たとえば{}
)の時に呼び出されます。
フォーマット用トレイトの全リストはこちらから、引数タイプについてはstd::fmt
のドキュメンテーションから参照できます。
演習
上にあるソースコード中のColor
という構造体のためのfmt::Display
トレイトの実装を追加しましょう。出力は以下のようになるはずです。
RGB (128, 255, 90) 0x80FF5A
RGB (0, 3, 254) 0x0003FE
RGB (0, 0, 0) 0x000000
詰まったら以下の3つがヒントになります。
- RGB色空間で色を計算する式は
RGB = (R*65536)+(G*256)+B (R は RED, G は GREEN and B は BLUE)
です。詳細はRGB color format & calculationを参照。 - それぞれの色を2回以上記述する必要があるかもしれません。
:0>2
で、幅を2に指定し、空白を0で埋める事ができます。
参照
基本データ型
Rustは様々な基本データ型の使用をサポートしています。以下がその例です。
スカラー型
- 符号付き整数:
i8
,i16
,i32
,i64
,i128
,isize
(ポインタのサイズ) - 符号無し整数:
u8
,u16
,u32
,u64
,u128
,usize
(ポインタのサイズ) - 浮動小数点数:
f32
,f64
char
:'a'
,'α'
,'∞'
などのUnicodeのスカラー値(それぞれ4バイト)bool
:true
またはfalse
- ユニット型
()
:唯一の値として空のタプル()
を持つ
ユニット型はその値がタプルですが、複合型とはみなされません。内部に複数の値を含んでいるわけではないからです。
複合型
- 配列: 例えば
[1, 2, 3]
- タプル:例えば
(1, true)
変数は常に 型指定 できます。数値型の場合はさらにサフィックスでの指定も可能です。指定しない場合デフォルトになります。整数はi32
が、浮動小数点はf64
がデフォルトです。また、Rustは文脈から型を推論することもできます。
参照
リテラルと演算子
整数1
、浮動小数点数1.2
、文字'a'
、文字列"abc"
、ブーリアンtrue
、ユニット()
は、リテラルを使って表すことが可能です。
また整数型の場合、プレフィックスに0x
、0o
、0b
を指定することでそれぞれ16進数、8進数、2進数を使って表すことができます。
可読性のため、_
(アンダースコア)を数値リテラルの間に挿入することができます。例えば1_000
は1000
と、0.000_001
は0.000001
とそれぞれ同一です。
また、Rustは1e6
や7.6e-4
などの科学的なE表記をサポートしています。この表記はf64
になります。
コンパイラに、リテラルの型を伝えたい場合があります。現在の仕様では、リテラルが32ビット符号無し整数であることを伝える場合、u32
サフィックスを、符号付き32ビット整数であればi32
サフィックスを使用します。
Rustで使用可能な演算子とその優先順位は、Cなどの言語のものとほぼ同じです。
タプル
タプルは異なる型の値の集合です。括弧()
を用いて生成します。タプル自体がそのメンバに対する型シグネチャを保持していますので、明示すると(T1, T2, ...)
のようになります。タプルは大きさに制限がありませんので、関数が複数の値を返したい時に使われます。
演習
-
復習 :上にある
Matrix
という構造体に、fmt::Display
トレイトを追加しましょう。デバッグフォーマット{:?}
ではなくディスプレイフォーマット{}
で出力すれば次のようになるはずです。( 1.1 1.2 ) ( 2.1 2.2 )
必要に応じてディスプレイのページに戻りましょう。
-
reverse
関数を雛形にしたtranspose
関数を実装してください。この関数はMatrix
を引数として受け取り、要素のうち2つを入れ替えたものを返します。つまりprintln!("Matrix:\n{}", matrix); println!("Transpose:\n{}", transpose(matrix));
は以下の様な出力になります。
Matrix: ( 1.1 1.2 ) ( 2.1 2.2 ) Transpose: ( 1.1 2.1 ) ( 1.2 2.2 )
配列とスライス
配列はT
という単一の型のオブジェクトの集合です。それらのオブジェクトはメモリ上の連続した領域に保存されます。配列は[]
を用いて生成されます。長さはコンパイル時には決定されていて、[T; length]
という形で指定できます。
スライスは配列に似ていますが、コンパイル時に長さが決定されていません。スライスは2ワードからなるオブジェクトであり、最初のワードがデータへのポインタ、2番目のワードがスライスの長さです。ワード長はusize
と同一で、プロセッサのアーキテクチャによって決まります。例えばx86-64では64ビットです。スライスは配列の一部を借用するのに使用され、&[T]
という型シグネチャを持ちます。
カスタム型
Rustでのカスタムデータ型の作成は主に以下の2つのキーワードを介して行われます。
struct
:構造体を定義するenum
:列挙型を定義する
const
、あるいはstatic
というキーワードによって定数を定義することもできます。
構造体
struct
というキーワードを用いて作成できる構造体には3種類あります。
- タプル構造体。(すなわちタプルに名前が付いたようなもの)
- クラシックなC言語スタイルの構造体。
- ユニット構造体。これはフィールドを持たず、ジェネリック型を扱う際に有効です。
演習
Rectangle
の面積を計算するrect_area
関数を追加してください。ネストしたデストラクトを使ってみましょう。Point
とf32
を引数とし、Rectangle
を返すsquare
関数を追加してください。Rectangle
の左上の点がPoint
になり、f32
がRectangle
の幅と高さになります。
参照
列挙型
列挙型(enum
)はいくつかの異なる要素型の中から1つを選ぶような場合に使用します。構造体(struct
)の定義を満たすものならば何でもenum
の要素型として使用できます。
型エイリアス
型エイリアスを用いると、列挙型の要素型を別名で参照できます。これは列挙型の名前があまりに長かったり、あまりに一般的だったりで改名したい場合に役立ちます。
このやり方がもっともよく見られるのは、impl
ブロックでSelf
という別名を使用する場合です。
列挙型や型エイリアスについて詳しく学びたい人は、この機能が安定してRustに取り込まれたときのstabilization reportを読んでください。
参照
match
, 関数, 文字列, "Type alias enum variants" RFC
use
use
を使用すれば変数のスコープを絶対名で指定する必要がなくなります。
参照
C言語ライクな列挙型
列挙型はC言語の列挙型のような使い方をする事もできます。
参照
テストケース:連結リスト
enum
の使用が適切なパターンのひとつに、連結リストを作成する場合があります。
参照
定数
Rustには2種類の定数があり、いずれもグローバルスコープを含む任意のスコープで宣言することができます。また、いずれも型を明示しなくてはなりません。
const
:不変の値(通常はこちらを使用します)static
:'static
ライフタイムを持つ変更可能な値。スタティックライフタイムは推論され、明示する必要はありません。可変なスタティック値へのアクセスや変更は安全ではありません。
参照
const
/static
のRFC, 'static
ライフタイム
変数束縛
Rustは静的な型付けによる型安全性を提供します。変数束縛は宣言時に型を指定できます。とはいえたいていの場合は、コンパイラは変数の型をコンテキストから推測することができますので、型指定の負担を大幅に軽減できます。
値(リテラルなど)はlet
を用いて変数に束縛することができます。
ミュータビリティ
変数はデフォルトでイミュータブル(変更不可能)ですがmut
構文を使用することで変更可能になります。
コンパイラはミュータビリティに関するエラーの詳細を出してくれます。
スコープとシャドーイング
変数はスコープを持つため、 ブロック の中に閉じ込められています。ブロックとは{}
で囲まれた領域のことです。
変数のシャドーイングも可能です。
前方宣言
It is possible to declare variable bindings first and initialize them later, but all variable bindings must be initialized before they are used: the compiler forbids use of uninitialized variable bindings, as it would lead to undefined behavior.
It is not common to declare a variable binding and initialize it later in the function. It is more difficult for a reader to find the initialization when initialization is separated from declaration. It is common to declare and initialize a variable binding near where the variable will be used.
値の凍結
データを同じ名前のイミュータブルな変数に束縛しなおすと、データは_凍結_されます。_凍結_したデータは、イミュータブルな束縛がスコープ外になるまで変更できません。
型
Rustには、基本データ型やユーザ定義型を定義したり変換したりする様々な方法があります。この章は以下の内容を扱います。
型キャスト
Rustは基本データ型について暗黙的な型変換(coerction
)を行うことはありません。しかし明示的な型変換(casting
)は可能です。その場合as
キーワードを使用します。
整数型から整数型へ型変換する場合、C言語で可能なケースの場合はC言語と同じです。C言語で未定義の場合の挙動も、Rustでは完全に定義されています。
リテラル
数値型リテラルはサフィックスにより型を指定することが可能です。例えば、42
というリテラルに対してi32
型を指定するには42i32
とします。
サフィックスを指定しない数値型リテラルの場合、その型がどのように使用されるかに依存して決められます。デフォルトでは整数型の場合i32
が、浮動小数点数型にはf64
が使われます。
上のコードには現時点では解説していない考えがいくつか使用されています。気になる方のために簡単に説明をしておきましょう。
std::mem::size_of_val
は関数ですが、 絶対パス で呼び出されています。ソースコードは論理的に区切られた モジュール と呼ばれるものにわけられることができます。今回の場合はsize_of_val
関数はmem
モジュール内で定義されており、mem
モジュールはstd
クレート 内で定義されています。より詳しくはモジュールとクレートを参照してください。
型推論
Rustの型推論エンジンはなかなか賢くできています。初期化の際に評価値の型をチェックするだけでなく、その後にどのような使われ方をしているかを見て推論します。以下がその例です。
このように、変数の型アノテーションは必要ありません。これでコンパイラもプログラマもハッピーですね!
エイリアス
type
文を使用することで既存の型に新しい名前を付けることができます。その場合、名前はUpperCamelCase
でなくてはなりません。さもなくばコンパイラがエラーを出します。唯一の例外はusize
やf32
のような基本データ型です。
このようにエイリアスを付ける一番の理由はボイラープレートを減らすことです。例えばio::Result<T>
型はResult<T, io::Error>
の別名です。
参照
型変換
基本データ型同士はキャストを用いて変換できます。
Rustはカスタム型(例えばstruct
やenum
)間の変換をトレイトを用いて行います。ジェネリックな型変換にはFrom
およびInto
トレイトを使用します。しかし、よくあるケースにおいて、特にString
との相互の型変換では、特殊なトレイトが使用されます。
From
とInto
From
トレイトとInto
トレイトは本質的に結びついており、そのことが実際の実装に反映されています。もし型Aが型Bからの型変換をサポートしているのであれば、型Bは型Aへの型変換ができると思うのが自然です。
From
From
トレイトは、ある型に対し、別の型からその型を作る方法を定義できるようにするものです。そのため、複数の型の間で型変換を行うための非常にシンプルな仕組みを提供しています。標準ライブラリでは、基本データ型やよく使われる型に対して、このトレイトが多数実装されています。
例えば、str
からString
への型変換は簡単です。
We can do something similar for defining a conversion for our own type.
Into
Into
トレイトは、単にFrom
トレイトの逆の働きをし、ある型を他の型に変換する方法を定義します。
into()
の呼び出し時にはほとんどの場合戻り値の型をコンパイラが決定できません。そのため戻り値の型は明示的に指定する必要があります。
From
and Into
are interchangeable
From
と Into
は相補的にデザインされているので両方のトレイトを実装する必要はありません。From
トレイトを実装すれば、 Into
は必要なときに呼ばれます。ただし、逆は真ではない点に注意してください。 Into
が実装されていても、 From
が自動的に提供されるわけではありません。
TryFrom
とTryInto
From
とInto
と同様に、TryFrom
とTryInto
も型変換を行うジェネリックなトレイトです。From
/Into
と異なり、TryFrom
/TryInto
トレイトは失敗する可能性のある型変換に用いられるので、Result
を返します。
Stringとの型変換
Stringへの型変換
To convert any type to a String
is as simple as implementing the ToString
trait for the type. Rather than doing so directly, you should implement the fmt::Display
trait which automatically provides ToString
and also allows printing the type as discussed in the section on print!
.
Stringの解析
文字列からの型変換において、数値への型変換はよく行われるものの一つです。これを行うイディオムはparse
関数を使用することですが、このときに型を推論できるようにするか、もしくはターボフィッシュ構文(::<>
)を使用して型を指定するかのいずれかを行います。以下の例では、どちらの方法も紹介しています。
This will convert the string into the type specified as long as the FromStr
trait is implemented for that type. This is implemented for numerous types within the standard library.
To obtain this functionality on a user defined type simply implement the FromStr
trait for that type.
式
Rustのプログラムは(ほとんどの場合)文の連続でできています
文にはいくつかの種類があります。最も一般的なのは変数の束縛と;
付きの式です。
コードブロックも式の一種です。よってブロックを丸ごと値として扱うことができます。その場合ブロック内の最後の式が場所を表す式(例えばローカル変数)に代入されます。ただし、ブロック内の最後の式が;
で終わる場合は返り値は()
になります。
制御フロー
処理の流れをコントロールすることはあらゆるプログラミング言語において重要な要素です。if
/else
、for
等です。Rustの文法を見ていきましょう。
if/else
if
-else
を用いた条件分岐は他の言語に似ています。多くの言語では条件式の中を括弧でくくる必要がありますが、Rustではその必要はありません。条件式の直後にはブロックが続きます。if
-else
は式の一種で、いずれの分岐先でも返り値の型は同一でなくてはなりません。
loop
Rustにはloop
というキーワードが存在します。これは無限ループを作成するのに使用します。
ループから抜けだす時はbreak
、即座に次のループに移るときはcontinue
が使用できます。
ネストとラベル
ネストしたループを回している時に外側のループをbreak
またはcontinue
したい場合があります。こういった場合には'label
を用いてループにラベルを貼り、break
/continue
にそのラベルを渡します。
loopが返す値
loop
の用途のひとつに「成功するまである処理を再試行する」ことがあります。もしその処理が値を返すならば、それをコードの他の部分に渡す必要があるでしょう。break
の後に値を置くと、それがloop
式の値として返されます。
while
while
キーワードは条件が真である限り実行され続けるループのために使用します。
悪名高いFizzBuzz問題をwhile
を用いて解いてみましょう。
forループ
for と range
for in
構文を用いることで、イテレータのそれぞれの要素に対して処理をすることが可能です。イテレータを作る最も単純な方法はa..b
のような範囲記法です。これは「a
」から「b
のひとつ前」までの要素を順に生成します。
ではwhile
の代わりにfor
を用いてFizzBuzzを書いてみましょう。
上記の代わりにa..=b
を用いると、両端の値を含む範囲を指定できます。上記の例は次のように書けます。
forとイテレータ
for in
構文はイテレータとさまざまな方法でやり取りできます。Iteratorトレイトの章で説明したように、デフォルトではfor
ループにおいてinto_iter
関数がコレクションに対して適用されます。しかし、コレクションをイテレータに変換する方法はこれだけではありません。
into_iter
、iter
、iter_mut
はいずれもコレクションのイテレータへの変換を行いますが、データの「見せ方」の違いにより、そのやり方はそれぞれ異なります。
iter
- この関数は、各周回においてコレクションの要素を借用します。よってコレクションには手を加えないので、ループの実行後もコレクションを再利用できます。
into_iter
- この関数はコレクションからデータを取り出すので、各周回において要素のデータそのものが提供されます。データを取り出してしまうと、データはループ内に「移動」してしまうので、ループ実行後にコレクションを再利用することはできません。
iter_mut
- この関数はコレクションの各要素をミュータブル(変更可能)で借用するので、コレクションの要素をその場で変更できます。
上記に示した3つのコードにおいて、match
の選択肢の型の違いに注意してください。ここがそれぞれの方法の違いを生む鍵になっています。型が異なれば、当然ながらそれに対して行える処理も変わります。
参照
match
Rustはmatch
を用いて、C言語におけるswitch
のようなパターンマッチングを行うことができます。マッチする最初のアームが評価され、取りうるすべての値はカバーされていなければなりません。
デストラクト
match
は値をさまざまなやり方でデストラクトすることができます。
タプル
以下のように、タプルはmatch
を用いてデストラクトすることができます。
参照
配列とスライス
タプル同様、配列とスライスも以下のようにデストラクトできます。
参照
列挙型
列挙型も似たやり方でデストラクトすることができます。
参照
ポインタとref
Rustのポインタは、C/C++のポインタとは異なる概念なので、デストラクトとデリファレンスを同じようなやり方で扱うことはできません。
- デリファレンスには
*
を用います。 - デストラクトには
&
,ref
,ref mut
を用います。
参照
構造体
以下のようにして、構造体も同様にデストラクトすることができます。
参照
ガード
match
内の条件文をフィルタリングするために、 ガード を使用することができます。
パターンが全てカバーされているかどうかを判断する際に、ガード条件は考慮されないことに注意してください。
参照
束縛
いくつかの変数をまとめてマッチ対象とした場合、そのうちの一つを分岐先で使用することはそのままでは不可能です。match
内では@
マークを使用して変数を束縛することができます。
Option
のような、列挙型の値をデストラクトするためにも、束縛を利用できます。
参照
if let
列挙型をマッチさせるとき、場合によってはmatch
を使用すると不自然な書き方になってしまう場合があります。例えば
この場合はif let
を用いたほうが美しく、失敗時の処理も柔軟に行うことができます。
同様にif let
は任意の列挙型のマッチに使えます。
もう一つのメリットはif let
がパラメータを持たない列挙型にも使えることです。列挙型がPartialEq
を実装または導出していなくても問題ありません。その場合、列挙型のインスタンスは比較できないのでif Foo::Bar == a
はコンパイルエラーとなりますが、if let
は引き続き使えます。
試してみましょう。以下の例をif let
を使って直してみてください。
参照
let-else
🛈 Rust 1.65で安定化。
🛈 エディションを指定するには
rustc --edition=2021 main.rs
のようにします。
let
-else
を使うと反駁できるパターンにマッチさせつつ、通常のlet
のように変数束縛することができます。マッチしなかった場合は(break
、return
、panic!
のように)処理を分岐させます。
use std::str::FromStr;
fn get_count_item(s: &str) -> (u64, &str) {
let mut it = s.split(' ');
let (Some(count_str), Some(item)) = (it.next(), it.next()) else {
panic!("Can't segment count item pair: '{s}'");
};
let Ok(count) = u64::from_str(count_str) else {
panic!("Can't parse integer: '{count_str}'");
};
(count, item)
}
fn main() {
assert_eq!(get_count_item("3 chairs"), (3, "chairs"));
}
束縛した変数名のスコープがmatch
やif let
-else
式との主な違いです。match
やif let
-else
でも似たようなことができますが、残念ながらコードの繰り返しや追加のlet
が必要になってしまいます。
参照
Option, match, if let, let-else RFC.
while let
if let
と同様に、while let
も不格好なmatch
処理を多少マシにしてくれます。例えば、以下のi
をインクリメントする処理を見てください。
while let
を使ってすっきり書くことができます。
参照
関数
関数はfn
キーワードを用いて定義することができます。引数は変数と同様に型を指定する必要があり、もし関数が値を返すならば->
の後にその型も指定する必要があります。
関数内の最後の式が返り値となります。関数の途中で値を返したい場合はreturn
文を使用します。ループの最中やif
文の中からも値を返すことができます。
では、今度は関数を使ってFizzBuzz問題を解いてみましょう!
関連関数とメソッド
関数には特定の型に紐づいたものがあります。これには関連関数とメソッドの2つの形式があります。メソッドは特定のインスタンスに関連付けて呼ばれる関数であるのに対し、関連関数は型全体に対して定義される関数です。
クロージャ
Rustにおけるクロージャは、その外側の環境を捕捉した関数のことです。例えば、次のコードは変数x
を捕捉したクロージャです。
|val| val + x
クロージャの構文や機能は、その場限りの用途で何かを作るのに便利です。クロージャの呼び出しは関数の呼び出しと全く同じです。しかし、入力の型と戻り値の型は推論させることができますが、入力変数の名前は必ず指定しなくてはなりません。
クロージャの他の特徴を以下に示します。
- 入力変数を囲むのに、
()
の代わりに||
を用います。 - 本体が単一の式の場合は、本体の区切り文字(
{}
)を省略できます。(それ以外の場合は必須です) - 外側の環境にある変数を捕捉することができます。
要素の捕捉
クロージャはとてもフレキシブルに動作するように出来ています。クロージャにおいて型アノテーションをする必要が無いのは前述の仕組みのためですが、この仕組みのおかげでユースケースに応じて参照を取得したり値そのものを取得したりといった動作が可能になります。クロージャは外側の環境にある要素を、以下の形で取得することができます。
- 参照:
&T
- ミュータブルな参照:
&mut T
- 値そのもの:
T
クロージャは出来る限り参照を取得しようとし、その他2つは必要なときのみ取得します。
バーティカルパイプ(訳注:縦線記号||
)の前にmove
を使用することで、キャプチャする変数の所有権を取ることをクロージャに強制します。
参照
捕捉時の型推論
Rustはたいていの場合、型アノテーションなしでも変数を捕捉する方法を臨機応変に選択してくれますが、関数を書く場合にはこの曖昧さは許されません。引数のパラメータとしてクロージャを取る場合、そのクロージャの完全な型はいくつかのtraits
の中の1つを使って明示されなければなりません。どれが使われるかは、捕捉された値でクロージャが何をするかによって決まります。制限の少ない順に並べると、下記の通りです。
Fn
:参照(&T
)によって捕捉するクロージャFnMut
:ミュータブルな参照(&mut T
)によって捕捉するクロージャFnOnce
:値(T
)によって捕捉するクロージャ
変数ごとに、コンパイラは可能な限り制約の少ない方法でその変数を捕捉します。
例えば、FnOnce
というアノテーションの付けられたパラメータを考えてみましょう。これはそのクロージャが&T
、&mut T
もしくはT
の どれか で捕捉することを指定するものですが、コンパイラは捕捉した変数がそのクロージャの中でどのように使用されるかに基づき、最終的に捕捉する方法を選択することになります。
これは、もし移動が可能であれば、いずれの種類の借用であっても同様に可能だからです。その逆は正しくないことに注意してください。パラメータがFn
としてアノテーションされている場合、変数を&mut T
やT
で捕捉することは許可されません。しかし&T
は許可されます。
以下の例で、Fn
、FnMut
、およびFnOnce
を入れ替えて、何が起こるのかを見てみましょう。
参照
std::mem::drop
, Fn
, FnMut
, ジェネリクス, where, FnOnce
匿名型
クロージャが周辺の環境から変数を取得するやり方は非常に明瞭です。何か注意すべき点はあるのでしょうか?もちろんです。関数内でクロージャを使う場合、ジェネリクスを使用する必要があります。詳しく見ていきましょう。
クロージャが定義されると、コンパイラは裏側で、無名の構造体を作り、そこにクロージャによって使用される外側の変数を入れます。同時にFn
、FnMut
、FnOnce
という名のトレイトのいずれか一つを介してこの構造体に関数としての機能を実装し、実際に呼び出されるまで待ちます。
この無名構造体は型が未指定なため、関数を実行する際にはジェネリクスが必要とされます。とはいえ、<T>
で指定するだけでは、まだ曖昧です。(訳注:&self
、&mut self
、self
のいずれをとるのかがわからないため)そのため、Fn
、FnMut
、FnOnce
のいずれか一つを実装することで対応しています。
参照
関数を受け取る関数
これまで、クロージャを引数として渡せることを見てきました。すると次の疑問が浮かんできます。「クロージャではない普通の関数を引数として渡すことは可能なのだろうか?」可能です!もしパラメータとしてクロージャを取る関数を定義すれば、そのクロージャのトレイト境界を満たす任意の関数をパラメータとして渡すことができます。
クロージャによる変数の捕捉がどのように行われているかを詳しく見たいときはFn
、FnMut
、FnOnce
を参照してください。
参照
クロージャを返す関数
クロージャを引数のパラメータとして用いることができるのと同様に、クロージャを戻り値として返すことも可能です。しかし無名のクロージャの型はその定義上、不明であるため、クロージャを返すためにはimpl Trait
を使用する必要があります。
クロージャを返すために有効なトレイトは下記の通りです。
Fn
FnMut
FnOnce
更に、move
というキーワードを使用し、全ての捕捉が値でおこなわれることを明示しなければなりません。これは、関数を抜けると同時に参照による捕捉がドロップされ、無効な参照がクロージャに残ってしまうのを防ぐためです。
参照
Fn
, FnMut
, ジェネリクス, impl Trait.
std
における使用例
この節ではstd
ライブラリを用いて、クロージャの利用例を幾つかお見せします。
Iterator::any
iterator::any
は、イテレータ内に一つでも条件を満たす要素があれば、true
を返し、さもなくばfalse
を返すイテレータです。以下がそのシグネチャです
pub trait Iterator {
// イテレートされる値の型
type Item;
// `any`は`&mut self`を取るため、イテレータを呼び出した値を借用し
// 変更しますが、消費することはありません。
fn any<F>(&mut self, f: F) -> bool where
// `FnMut`はクロージャによって捕捉される変数が変更される
// 事はあっても消費されることはないということを示します。
// `Self::Item`はクロージャが変数を値として取ることを示します。
F: FnMut(Self::Item) -> bool;
}
参照
イテレータによる検索
Iterator::find
はイテレータを辿る関数で、条件を満たす最初の値を探します。もし条件を満たす値がなければNone
を返します。型シグネチャは以下のようになります。
pub trait Iterator {
// イテレートされる値の型
type Item;
// `find`は`&mut self`を取るため、イテレータを呼び出した値を借用し
// 変更しますが、消費することはありません。
fn find<P>(&mut self, predicate: P) -> Option<Self::Item> where
// `FnMut`はクロージャによって捕捉される変数が変更される
// 事はあっても消費されることはないということを示します。
// `&Self::Item`はクロージャが変数を参照として取ることを示します。
P: FnMut(&Self::Item) -> bool;
}
Iterator::find
は要素への参照を返します。要素の インデックス を使用したい場合、Iterator::position
を使用してください。
参照
std::iter::Iterator::rposition
高階関数
Rustには高階関数(Higher Order Functions, HOF
)を扱う機能が備わっています。
Option と イテレータ には高階関数が使用されています。
発散する関数
発散する関数は決してリターンしない関数です。こうした関数は !
を使って、空の型であることが示されます。
他の全ての型と異なり、この型はインスタンス化できません。この型が持ちうる全ての値の集合は空です。この型は()
型とは異なることに注意してください。()
型は値をただ1つだけ持つ型です。
例えば、この関数は通常どおりリターンしますが、戻り値には何の情報も含みません。
fn some_fn() {
()
}
fn main() {
let _a: () = some_fn();
println!("This function returns and you can see this line.");
}
一方、この関数は呼び出し元に決してリターンしません。
#![feature(never_type)]
fn main() {
let x: ! = panic!("This call never returns.");
println!("You will never see this line!");
}
Although this might seem like an abstract concept, it is actually very useful and often handy. The main advantage of this type is that it can be cast to any other type, making it versatile in situations where an exact type is required, such as in match branches. This flexibility allows us to write code like this:
fn main() {
fn sum_odd_numbers(up_to: u32) -> u32 {
let mut acc = 0;
for i in 0..up_to {
// 変数"addition"の型がu32であるため、
// このmatch式はu32をリターンしなければならないことに注意。
let addition: u32 = match i%2 == 1 {
// 変数"i"はu32型であるため、全く問題ありません。
true => i,
// 一方、"continue"式はu32をリターンしませんが、これでも問題ありません。
// 決してリターンしないため、このmatch式が要求する型に違反しないからです。
false => continue,
};
acc += addition;
}
acc
}
println!("Sum of odd numbers up to 9 (excluding): {}", sum_odd_numbers(9));
}
この型は、ネットワークサーバのような永遠にループする関数(例:loop {}
)の戻り値の型や、プロセスを終了させる関数(例:exit()
)の戻り値の型としても使用されます。
モジュール
Rustにはコードを階層的に分割し、お互いの機能を隠蔽・公開するための強力なモジュールシステムがあります。
モジュールは関数、構造体、トレイト、impl
ブロック、さらには他のモジュールなどの要素の集合です。
可視性
デフォルトでは、モジュール内の要素はプライベートですが、これはpub
で修飾することでパブリックな属性にすることができます。パブリックな属性のみがモジュールの外のスコープからアクセスできます。
構造体の場合
構造体はそれ自身に加え、フィールドごとにもパブリック・プライベートを設定することができます。デフォルトではプライベートですが、pub
宣言をすることで、フィールドをパブリックにすることができます。これは、構造体がモジュールの外から参照される時に限り意味のあるもので、情報の隠蔽(カプセル化)を達成するための機能です。
参照
use
宣言
use
宣言をすることで、要素の絶対パスを新しい名前に束縛することができ、より簡潔な記述が可能になります。例えば以下のように使えます。
as
キーワードを使用することで、インポートを別名に束縛することができます。
super
とself
super
及びself
キーワードは、要素にアクセスする際に、曖昧さをなくし、不必要なハードコーディングを避けるために使用できます。
ファイルの階層構造
モジュールはファイル・ディレクトリの階層構造と対応関係にあります。以下の様なファイルで可視性の例を詳しく見ていきましょう。
$ tree .
.
├── my
│ ├── inaccessible.rs
│ └── nested.rs
├── my.rs
└── split.rs
split.rs
は以下のようになります。
// このように宣言すると、`my.rs`という名のファイルを探し、
// その内容をこのファイル中で`my`という名から使用することができるようにします。
mod my;
fn function() {
println!("called `function()`");
}
fn main() {
my::function();
function();
my::indirect_access();
my::nested::function();
}
my.rs
は以下のようになります。
// 同様に`mod inaccessible`、`mod nested`によって、`nested.rs`、`inaccessible.rs`
// の内容をこの中で使用することができるようになります。
// 訳注:`pub`をつけないかぎり、この中でしか使用できません。
mod inaccessible;
pub mod nested;
pub fn function() {
println!("called `my::function()`");
}
fn private_function() {
println!("called `my::private_function()`");
}
pub fn indirect_access() {
print!("called `my::indirect_access()`, that\n> ");
private_function();
}
my/nested.rs
は以下のようになります。
pub fn function() {
println!("called `my::nested::function()`");
}
#[allow(dead_code)]
fn private_function() {
println!("called `my::nested::private_function()`");
}
my/inaccessible.rs
は以下のようになります。
#[allow(dead_code)]
pub fn public_function() {
println!("called `my::inaccessible::public_function()`");
}
では、以前と同じように実行できるか確認しましょう。
$ rustc split.rs && ./split
called `my::function()`
called `function()`
called `my::indirect_access()`, that
> called `my::private_function()`
called `my::nested::function()`
クレート
クレートはRustにおけるコンパイルの単位です。rustc some_file.rs
が呼ばれると、some_file.rs
は必ず クレートファイル として扱われます。もしsome_file.rs
がmod
宣言を含んでいるのならば、コンパイルの 前に 、そのモジュールファイルの中身がmod
の位置に挿入されます。言い換えると、それぞれのモジュールが独立にコンパイルされるということはありませんが、それぞれのクレートは互いに独立にコンパイルされるということです。
クレートはバイナリあるいはライブラリ形式でコンパイルされることが可能です。デフォルトではrustc
はクレートからバイナリを作り出しますが、この振る舞いは--crate-type
フラグにlib
を渡すことでオーバーライドできます。
ライブラリの作成
ではライブラリを作成し、それを別のクレートにリンクする方法を見ていきましょう。
rary.rs
は以下のようになります。
pub fn public_function() {
println!("called rary's `public_function()`");
}
fn private_function() {
println!("called rary's `private_function()`");
}
pub fn indirect_access() {
print!("called rary's `indirect_access()`, that\n> ");
private_function();
}
$ rustc --crate-type=lib rary.rs
$ ls lib*
library.rlib
ライブラリは「lib」が頭につき、デフォルトでは、その後ろに元となったクレートファイル名をつけます。(訳注: ここではlib
+ rary
)この振る舞いはcrate_name
アトリビュートを用いてオーバーライドできます。
ライブラリの利用
クレートをこの新しいライブラリにリンクするには、rustc
の--extern
フラグを利用します。クレートの要素を全てライブラリと同じ名前のモジュールにインポートします。一般に、このモジュールは他のモジュールと同じように振る舞います。
// extern crate rary; // Rust 2015以前で必要。
fn main() {
rary::public_function();
// エラー!`private_function`はプライベート。
//rary::private_function();
rary::indirect_access();
}
# Where library.rlib is the path to the compiled library, assumed that it's
# in the same directory here:
$ rustc executable.rs --extern rary=library.rlib && ./executable
called rary's `public_function()`
called rary's `indirect_access()`, that
> called rary's `private_function()`
Cargo
cargo
はRustの公式パッケージ管理ツールです。とても便利な機能が多くあり、コードの品質や開発速度の向上に役立ちます。以下はその例です。
- 依存関係の管理とcrates.io(Rustの公式パッケージレジストリ)との統合
- ユニットテスト
- ベンチマーク
この章では、簡単な基本機能を説明します。包括的なドキュメントはThe Cargo Bookを参照してください。
依存関係
ほとんどのプログラムはライブラリに依存関係を持ちます。もし依存関係を手動で管理したことがあれば、それがどれだけ苦痛であるか分かるでしょう。幸運なことに、Rustのエコシステムにはcargo
が標準装備されています!cargo
によってプロジェクトの依存関係を管理することができます。
Rustのプロジェクトを新しく作るには下記のようにします。
# バイナリ
cargo new foo
# ライブラリ
cargo new --lib bar
この章の残りでは、ライブラリではなくバイナリを作ることを想定しますが、コンセプトはすべて同じです。
上のコマンドを実行すると、次のようなファイル階層ができます。
.
├── bar
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
└── foo
├── Cargo.toml
└── src
└── main.rs
main.rs
がこの新規プロジェクト foo
のルートのソースファイルです。なにも新しいことはありませんね。Cargo.toml
はこのプロジェクトのcargo
の設定ファイルです。中を見てみるとこのようになっています。
[package]
name = "foo"
version = "0.1.0"
authors = ["mark"]
[dependencies]
[package]
の下のname
フィールドがプロジェクトの名前を決定します。これはクレートを公開するときにcrates.io
によって使われます(詳細は後述)。またコンパイルしたときの出力ファイルの名前でもあります。
version
フィールドはクレートのバージョン番号で、セマンティックバージョニングを使っています。
authors
フィールドは作者のリストで、クレートを公開するときに使われます。
[dependencies]
セクションにはプロジェクトの依存関係を追加できます。
例えば、プログラムに素晴らしいCLIが欲しいとします。crates.io(Rustの公式パッケージレジストリ)には素晴らしいパッケージがたくさんあります。よくある選択肢の1つはclapです。この記事を書いている時点でのclap
の最新の公開バージョンは2.27.1
です。依存関係をプログラムに追加するには、Cargo.toml
の[dependencies]
の下にclap = "2.27.1"
と単に書き加えます。これだけです!clap
をプログラム内で使用できます。
cargo
は他の形式の依存関係もサポートしています。その一部を示します。
[package]
name = "foo"
version = "0.1.0"
authors = ["mark"]
[dependencies]
clap = "2.27.1" # from crates.io
rand = { git = "https://github.com/rust-lang-nursery/rand" } # from online repo
bar = { path = "../bar" } # from a path in the local filesystem
cargo
は依存管理ツール以上のこともできます。Cargo.toml
のformat specificationに全ての設定オプションがリストアップされています。
プロジェクトをビルドするには、プロジェクトディレクトリのどこか(サブディレクトでも!)でcargo build
を実行します。またcargo run
でビルドと実行をできます。これらのコマンドは、全ての依存関係の解決、必要なクレートのダウンロード、自分のクレートを含む全てのビルドを行うことに注意してください。(make
と同様、まだビルドしていないものだけをビルドします。)
Voila!これで完成です!
規約
前の章ではこのようなディレクトリ階層がありました。
foo
├── Cargo.toml
└── src
└── main.rs
しかし同じプロジェクトで2つのバイナリが欲しいとします。その場合は?
cargo
はこれもサポートしています。以前見た通りデフォルトのバイナリ名はmain
ですが、bin/
ディレクトリに置くことで他のバイナリを追加できます。
foo
├── Cargo.toml
└── src
├── main.rs
└── bin
└── my_other_bin.rs
このバイナリだけをコンパイルや実行するようにcargo
に伝えるには、cargo
に--bin my_other_bin
フラグを渡します。ここではmy_other_bin
が対象のバイナリの名前です。
バイナリの追加に加えて、cargo
はベンチマークやテスト、サンプルなどのその他の機能もサポートしています。
次の章ではテストについてより詳しく見ていきます。
テスト
知っての通り、テストはどんなソフトウェアにも不可欠です!Rustはユニットテストと統合テストを第一級にサポートしています(TRPLのこの章を参照してください)。
上のリンク先のテストの章では、ユニットテストと統合テストの書き方を紹介しています。ユニットテストはテスト対象のモジュール内に、統合テストはtests/
ディレクトリ内に置きます。
foo
├── Cargo.toml
├── src
│ └── main.rs
│ └── lib.rs
└── tests
├── my_test.rs
└── my_other_test.rs
tests
内の各ファイルは個別の統合テストです。これはライブラリを依存クレートから呼ばれたかのようにテストできます。
テストの章は3つの異なるテストスタイルについて解説しています。単体テスト、ドキュメンテーションテスト、そして結合テストです。
cargo
は、全てのテストを簡単に実行する方法を提供します。
$ cargo test
出力はこのようになります。
$ cargo test
Compiling blah v0.1.0 (file:///nobackup/blah)
Finished dev [unoptimized + debuginfo] target(s) in 0.89 secs
Running target/debug/deps/blah-d3b32b97275ec472
running 4 tests
test test_bar ... ok
test test_baz ... ok
test test_foo_bar ... ok
test test_foo ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
パターンにマッチする名前のテストを実行することもできます。
$ cargo test test_foo
$ cargo test test_foo
Compiling blah v0.1.0 (file:///nobackup/blah)
Finished dev [unoptimized + debuginfo] target(s) in 0.35 secs
Running target/debug/deps/blah-d3b32b97275ec472
running 2 tests
test test_foo ... ok
test test_foo_bar ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out
注意:Cargoは複数のテストを並列で実行することがありますので、それらが互いに競合しないようにしてください。
並行性が問題を引き起こす一例として、以下のように、2つのテストが1つのファイルに出力するケースがあります。
以下のような結果を得ようと意図しています。
$ cat ferris.txt
Ferris
Ferris
Ferris
Ferris
Ferris
Corro
Corro
Corro
Corro
Corro
しかし、実際にferris.txt
に出力されるのは、以下の通りです。
$ cargo test test_file && cat ferris.txt
Corro
Ferris
Corro
Ferris
Corro
Ferris
Corro
Ferris
Corro
Ferris
ビルドスクリプト
cargo
による通常のビルドでは十分でないことが時々あります。コード生成や、コンパイルが必要なネイティブコードなど、cargo
がクレートをうまくコンパイルするにはなんらかの前提条件が必要かもしれません。この問題を解決するため、Cargoが実行できるビルドスクリプトがあります。
ビルドスクリプトをパッケージに追加するには、以下のようにCargo.toml
の中で指定できます。
[package]
...
build = "build.rs"
それ以外の場合、Cargoはデフォルトでプロジェクトディレクトリからbuild.rs
を探します。
ビルドスクリプトの使い方
ビルドスクリプトは単にRustのファイルの1つで、パッケージ内の他のファイルをコンパイルする前にコンパイルされて起動されます。そのため、クレートの前提条件を満たすために使用できます。
Cargoは、ここで指定された環境変数を介してスクリプトに入力を与えます。
スクリプトは標準出力に出力します。出力される行は全て、target/debug/build/<pkg>/output
に書き込まれます。さらに、行頭にcargo:
がついた行はCargoに直接解釈されるため、パッケージのコンパイル時のパラメーターを定義するのに使用できます。
より詳細な仕様や例については、Cargo specificationを参照してください。
アトリビュート
アトリビュートはモジュール、クレート、要素に対するメタデータです。以下がその使用目的です。
- コンパイル時の条件分岐
- クレート名、バージョン、種類(バイナリか、ライブラリか)の設定
- リントの無効化
- コンパイラ付属の機能(マクロ、グロブ、インポートなど)の使用
- 外部ライブラリへのリンク
- ユニットテスト用の関数を明示
- ベンチマーク用の関数を明示
- アトリビュートマクロ
Attributes look like #[outer_attribute]
or #![inner_attribute]
, with the difference between them being where they apply.
-
#[outer_attribute]
applies to the item immediately following it. Some examples of items are: a function, a module declaration, a constant, a structure, an enum. Here is an example where attribute#[derive(Debug)]
applies to the structRectangle
: -
#![inner_attribute]
applies to the enclosing item (typically a module or a crate). In other words, this attribute is interpreted as applying to the entire scope in which it's placed. Here is an example where#![allow(unused_variables)]
applies to the whole crate (if placed inmain.rs
):#![allow(unused_variables)] fn main() { let x = 3; // This would normally warn about an unused variable. }
アトリビュートは以下の様な書き方で引数を取ることができます。
#[attribute = "value"]
#[attribute(key = "value")]
#[attribute(value)]
アトリビュートは複数の値を取ることができ、複数の行に分割することもできます。
#[attribute(value, value2)]
#[attribute(value, value2, value3,
value4, value5)]
dead_code
コンパイラはdead_code
と呼ばれるリント機能を持つため、使用されていない関数が存在するときに警告を出します。アトリビュート によってこの機能を無効化することができます。
実際のコード中では、使用されていないコードが有る場合はそれを除外するべきです。この文書中では随所でアトリビュートによって警告を抑制していますが、それはあくまでインタラクティブな例を皆さんに提供するためです。
クレート
crate_type
アトリビュートは、そのクレートがライブラリ、バイナリのいずれにコンパイルされるべきかをコンパイラに伝えるために使用します。ライブラリの場合は、どのタイプのライブラリであるかも伝えることができます。crate_name
はクレートの名前を決定するのに使用します。
しかし、crate_type
アトリビュートもcrate_name
アトリビュートも、RustのパッケージマネージャCargoを利用している場合は何の影響もないと知っておくことは重要です。Cargoは大半のRustプロジェクトで利用されており、実世界でのcrate_type
とcrate_name
の利用は比較的限られています。
crate_type
アトリビュートが使用されているときは、rustc
に--crate-type
フラグを伝える必要はありません。
$ rustc lib.rs
$ ls lib*
library.rlib
cfg
環境に応じたコンパイルをするには2種類の方法があります。
cfg
アトリビュート:#[cfg(...)]
をアトリビュートとして使用する。cfg!
マクロ:cfg!(...)
をブーリアンとして評価する。
前者は条件付きコンパイルを行いますが、後者はtrue
またはfalse
リテラルに評価され実行時にチェックすることが可能です。いずれの場合も適切なシンタックスで記述する必要があります。
#[cfg]
と異なり、cfg!
はコードを削除せず、trueまたはfalseに評価されるだけです。例えば、cfg!
が何を評価しているかに関係なく、cfg!
が条件に利用されるとき、if/else式の中のすべてのブロックが有効でなくてはなりません。
参照
条件の追加
target_os
のように、いくつかの条件分岐はrustc
が暗黙のうちに提供しています。条件を独自に追加する場合には--cfg
フラグを用いてrustc
に伝える必要があります。
独自のcfg
フラグを用いない場合、何が起きるかやってみてください。
cfg
フラグがある場合:
$ rustc --cfg some_condition custom.rs && ./custom
condition met!
ジェネリクス
ジェネリクスとは、型と関数の機能をより汎用的に使えるようにするための機能です。これはあらゆる局面でコードの重複を避けるために非常に役立ちますが、多少構文が複雑になります。すなわち、ジェネリック型を使いこなすには、どのようなジェネリック型がきちんと機能するかに細心の注意を払う必要があります。最もシンプルで一般的なジェネリクスの利用法は型パラメータです。
ジェネリック型の型パラメータにはかぎ括弧とアッパーキャメルケース(<Aaa, Bbb, ...>
)が使われます。ジェネリックな型パラメータはたいていの場合<T>
で示されます。Rustの場合、「ジェネリクス」には「1つ以上のジェネリックな型パラメータ<T>
を受け付けるもの」という意味もあります。ジェネリックな型パラメータを指定された場合、それは必ずジェネリック型になり、そうでなければ必ず非ジェネリック型、すなわち具象型になります。
例として、あらゆる型の引数T
をとる ジェネリック関数 foo
を定義すると
fn foo<T>(arg: T) { ... }
となります。T
はジェネリックな型パラメータに指定されているので、この場所で(arg: T)
のように使用するとジェネリック型として扱われます。これはT
という構造体がそれ以前に定義されていても同様です。
では、手を動かしながらジェネリック型の構文を体験していきましょう。
参照
関数
「型T
はその前に<T>
があるとジェネリック型になる」というルールは関数に対しても当てはまります。
ジェネリック関数を使用する際、型パラメータを明示する必要がある場合があります。返り値がジェネリック型である場合や、コンパイラが型パラメータを推論するのに十分な情報がない場合です。
型パラメータを明示したうえでの関数呼び出しの構文はfun::<A, B, ...>()
のようになります。
参照
実装
関数と同様、impl
でメソッドを実装する際にもジェネリック型特有の記法が必要です。
参照
トレイト
もちろんトレイトもジェネリクスを活用することができます。ここではDrop
トレイトをジェネリックメソッドとして再実装し、自身と引数として受け取った値の両方をdrop
するようなメソッドにします。
参照
境界
ジェネリックプログラミングをしていると、型パラメータが特定の機能を持っていることを規定するため、トレイトに境界を設ける必要があることがよくあります。例えば、以下の例では、引数のDisplay
トレイトを用いて出力を行うため、T
がDisplay
を持っていることを規定しています。つまり、「T
はDisplay
を実装 していなくてはならない 」という意味です。
// `Display`トレイトを実装している`T`を引数として取ります。
// `printer`という関数を定義。
fn printer<T: Display>(t: T) {
println!("{}", t);
}
境界は、ジェネリクスを全ての型ではなく、一定条件を満たす型に対してのみ適用するためにあります。つまり
struct S<T: Display>(T);
// エラー!`Vec<T>`は`Display`を実装していないため、この特殊化
// は失敗します。
let s = S(vec![1]);
境界のもう一つの効果は、ジェネリック型のインスタンスが、境界条件となるトレイトのメソッドにアクセスすることができるようになる点です。以下がその例です。
付け加えておくと、where
句を用いて境界を適用することもできます。場合によってはこちらの記法を使用したほうが読みやすくなる場合もあります。
参照
テストケース:空トレイト
トレイト境界の仕組みから、「トレイトがなにも機能を持っていなくとも境界条件として使用できることには変わりはない」という帰結がもたらされます。Eq
とCopy
はstd
ライブラリにおけるそのような例です。
参照
std::cmp::Eq
, std::marker::Copy
, トレイト
複数の境界
+
を用いて1つの型に複数のトレイト境界を設けることができます。複数の引数を受け取るときは、通常時と同様、,
で区切ります。
参照
Where句
トレイト境界は、{
の直前にwhere
句を導入することでも設けることができます。where
はさらに、型パラメータだけでなく任意の型に対して適用できます。
where
句のほうが有効なケースには例えば以下のようなものがあります。
- ジェネリック型とジェネリック境界に別々に制限を加えたほうが明瞭になる場合
impl <A: TraitB + TraitC, D: TraitE + TraitF> MyTrait<A, D> for YourType {}
// `where`を用いてジェネリック境界を設けます。
impl <A, D> MyTrait<A, D> for YourType where
A: TraitB + TraitC,
D: TraitE + TraitF {}
where
句の方が通常の構文より表現力が高い場合。この例ではwhere
句を使わずに書くことはできません。
参照
ニュータイプイディオム
ニュータイプイディオムは正しい型の値が与えられていることをコンパイル時に保証することができます。
例えば、年齢を年単位で確認するis_adult
には「Years」という型の値を 与えなければならない ようにすることが可能です。
最後の print文 のコメントを外して、与えられた型が Years
でなければならないことを確認してください。
newtype
の元に使われている型のデータを取得するには、以下のようにタプルやデストラクト構文を用いることで取得できます。
参照
関連要素
関連要素とは複数の型の要素に関係のある規則の総称です。トレイトの拡張機能であり、トレイトの中で新しい要素を定義することを可能にします。
そのように定義する要素の一つに 関連型 があります。これにより、ジェネリックなコンテナ型に対するトレイトを使用する際に、よりシンプルな書き方ができるようになります。
参照
関連要素が必要になる状況
コンテナ型に、その要素に対してジェネリックなトレイトを実装した場合、そのトレイトを使用する者は全てのジェネリック型を明記 しなくてはなりません 。
以下の例ではContains
トレイトはジェネリック型A
とB
の使用を許しています。その後、Container
型に対してContains
を実装していますが、その際後にfn difference()
が使用できるように、A
、B
はそれぞれi32
と明記されています。
Contains
はジェネリックトレイトなので、fn difference()
では 全ての ジェネリック型を宣言しなくてはなりません。実際のところ、A
とB
は 引数 であるC
によって決定されていて欲しいにも関わらず、です。これは次のページで紹介する関連型と呼ばれる機能によって可能です。
参照
関連型
関連型を使用すると、コンテナ型の中の要素をトレイトの中に 出力型 として書くことで、全体の可読性を上げることができます。トレイトを定義する際の構文は以下のようになります。
Contains
トレイトを使用する関数において、A
とB
を明示する必要がなくなっていることに注目しましょう。
// 関連型を使用しない場合
fn difference<A, B, C>(container: &C) -> i32 where
C: Contains<A, B> { ... }
// 使用する場合
fn difference<C: Contains>(container: &C) -> i32 { ... }
前セクションの例を関連型を使用して書きなおしてみましょう。
幽霊型パラメータ
幽霊型とは実行時には存在しないけれども、コンパイル時に静的に型チェックされるような型のことです。
構造体などのデータ型は、ジェネリック型パラメータを一つ余分に持ち、それをマーカーとして使ったりコンパイル時の型検査に使ったりすることができます。このマーカーは実際の値を何も持たず、したがって実行時の挙動そのものにはいかなる影響ももたらしません。
以下の例では、そのようなマーカーとして幽霊型(std::marker::PhantomData)を用い、それぞれ異なった型の値を持つタプルを作成します。
参照
テストケース:単位を扱う
共通の単位同士を扱う際のチェックのために、Add
を幽霊型を用いた実装にすると便利な場合があります。その場合Add
トレイトは以下のようになります。
// このように定義しておくと、`Self + RHS = Output`であることが保証され、
// かつ、impl時にRHSの型が明示されていない場合、デフォルトでSelfと同じに
// なります。
pub trait Add<RHS = Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
// `Output`は`T<U>`でなくてはならないので`T<U> + T<U> = T<U>`となります。
impl<U> Add for T<U> {
type Output = T<U>;
...
}
以下は全体を示した例です。
参照
借用 (&
), 境界 (X: Y
), 列挙型, impl & self, 演算子のオーバーロード, ref, トレイト (X for Y
), タプル構造体.
スコープの規則
所有権、借用、ライフタイムといったRustに特有の概念において、変数のスコープは重要な役割を果たします。すなわち、スコープの存在によってコンパイラは借用は可能か否か、メモリ上の資源は解放可能か、変数はいつ作成され、いつ破棄されるか。といったことを知るのです。
RAII
Rustの変数は単にデータをスタック上に保持するだけのものではありません。例えばヒープメモリを確保するBox<T>
のように、変数はメモリ上の資源を 保有 する場合もあるのです。RustはRAII(Resource Acquisition Is Initialization)を強制するので、オブジェクトがスコープを抜けると、必ずデストラクタが呼び出されてそのオブジェクトが保持していた資源が解放されます。
この振る舞いは リソースリーク バグを防ぐのに役立ちます。手動でメモリを解放したり、メモリリークバグにわずらわされたりすることはなくなるのです!簡単な例で説明しましょう。
valgrind
を用いて、メモリエラーが起きていないか2重チェックすることももちろん可能です。
$ rustc raii.rs && valgrind ./raii
==26873== Memcheck, a memory error detector
==26873== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==26873== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info
==26873== Command: ./raii
==26873==
==26873==
==26873== HEAP SUMMARY:
==26873== in use at exit: 0 bytes in 0 blocks
==26873== total heap usage: 1,013 allocs, 1,013 frees, 8,696 bytes allocated
==26873==
==26873== All heap blocks were freed -- no leaks are possible
==26873==
==26873== For counts of detected and suppressed errors, rerun with: -v
==26873== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)
リークはないみたいですね!
デストラクタ
Rustにおけるデストラクタの概念はDrop
トレイトによって提供されています。デストラクタは資源がスコープを抜けるときに呼び出されます。Drop
トレイトは型定義のたびに必ず実装する必要があるわけではなく、デストラクタに独自のロジックが必要な場合にのみ実装します。
下のコードを実行して、Drop
トレイトの動作を確認してみましょう。main
関数内の変数がスコープを抜けるときにカスタムデストラクタが呼び出されるはずです。
参照
所有権とムーブ
変数には自身の保持する資源を開放する責任があるため、資源は一度に一つの所有者 しか持つことができません。これはまた、資源を2度以上開放することができないということでもあります。ここで、全ての変数が資源を所有するわけではないということに注意しましょう。(e.g. 参照)
変数を代入する(let x = y
)際や、関数に引数を値渡しする(foo(x)
)際は、資源の 所有権 が移動します。Rustっぽく言うと、「 ムーブ 」です。
資源を移動すると、それまでの所有者(訳注:変数などのこと)を使用することはできなくなります。これによりダングリングポインタの発生を防げます。
ミュータビリティ
データのミュータビリティは所有権を移譲した際に変更できます。
部分的ムーブ
Within the destructuring of a single variable, both by-move
and by-reference
pattern bindings can be used at the same time. Doing this will result in a partial move of the variable, which means that parts of the variable will be moved while other parts stay. In such a case, the parent variable cannot be used afterwards as a whole, however the parts that are only referenced (and not moved) can still be used. Note that types that implement the Drop
trait cannot be partially moved from, because its drop
method would use it afterwards as a whole.
この例では、age
変数をヒープ上に保持し、部分的ムーブを説明しています。上記コードでref
を削除すると、person.age
の所有権がage
変数にムーブされるため、エラーになります。もしもPerson.age
がスタック上に保持されていたら、age
の定義がperson.age
をムーブすることなくデータをコピーするので、ref
は必須ではないのですが、実際にはヒープ上に保持されているためref
は必須です。
参照
借用
実際には、データの所有権を完全に受け渡すことなく一時的にアクセスしたいという場合がほとんどです。そのために、Rustでは 借用 という仕組みを用います。値そのもの(T
)を受け渡すのではなく、その参照(&T
)を渡すのです。
コンパイラは借用チェッカを用いて参照が 常に 有効なオブジェクトへの参照であることを、コンパイル時に保証します。つまり、あるオブジェクトへの参照が存在しているならば、そのオブジェクトを破壊することはできないということです。
ミュータビリティ
ミュータブルなデータは&mut T
でミュータブルに(変更可能な形で)借用することができます。これは ミュータブルな参照 と呼ばれ、読み込み・書き込みの権限を借用者に与えます。対照的に&T
はデータをイミュータブルな参照を介して借用し、借用した側はデータを読み込みはできますが書き込みはできません。
参照
エイリアス
データは一度にいくつでもイミュータブルに借用することができますが、その間オリジナルのデータをミュータブルに借用することはできません。一方でミュータブルな借用は一度に 一つ しか借用することができません。オリジナルのデータをもう一度借用できるのはミュータブルな参照が最後に使われた場所より あとで なければいけません。
refパターン
let
を介してデストラクトやパターンマッチングを行う場合、ref
キーワードを用いて構造体・タプルのフィールドへの参照を取得することができます。以下の例ではこれが有用になる例を幾つかお見せします。
ライフタイム
ライフタイム はコンパイラ(借用チェッカーと呼ばれる場合もあります)が、全ての借用に問題がないことを確認するために使用する仕組みです。正確にいうと、変数のライフタイムは作成時に開始し、破棄された時に終了します。ライフタイムとスコープは同時に語られることが多いですが、同じものではありません。
例として&
を用いて変数を借用する場合をあげましょう。借用のライフタイムは宣言時に決定し、そこから貸し手が破棄されるまで続きます。しかしながら、借用のスコープは参照が使われる際に決定します。
以下の例からこのセクションの残りでは、ライフタイムとスコープの関係、そしてそれらがいかに異なるものであるかを見ていきます。
ここで、一切の名前や型がライフタイムに代入されていないことに注意しましょう。これにより、ライフタイムの使われ方がこれから見ていくようなやり方に制限されます。
明示的アノテーション
借用チェッカーは参照がどれだけの間有効かを決定するために、明示的なアノテーションを使用します。ライフタイムが省略1されなかった場合、Rustは参照のライフタイムがどのようなものであるか、明示的なアノテーションを必要とします。
foo<'a>
// `foo`は`'a`というライフタイムパラメータを持ちます。
クロージャと同様、ライフタイムの使用はジェネリクスを必要とします。もう少し詳しく言うと、この書き方は「foo
のライフタイムは'a
のそれを超えることはない。」ということを示しており、型を明示した場合'a
は&'a T
となるということです。
ライフタイムが複数ある場合も、同じような構文になります。
foo<'a, 'b>
// `foo`は`'a`と`'b`というライフタイムパラメータを持ちます。
この場合は、foo
のライフタイムは'a
、'b
の いずれよりも 長くなってはなりません。
以下はライフタイムを明示的に書く場合の例です。
省略 はライフタイムが暗黙のうちに(プログラマから見えない形で)アノテートされることを指します。
参照
関数
省略をしない場合、ライフタイムのシグネチャ(e.g. <'a>
)を持つ関数にはいくつかの制限があります。
- 全ての参照においてライフタイムを明示しなくてはなりません。
- 返り値となる参照はすべて引数と同じライフタイムか、
static
ライフタイムを持たなくてはなりません
加えて、引数のない関数から参照を返すことは、それが結果的に無効なデータへの参照になるならば、禁止されています。ライフタイムを持つ関数の例をいくつか示します。
参照
メソッド
メソッドのライフタイムは関数に似ています。
参照
構造体
構造体におけるライフタイムも関数のそれと似ています。
参照
トレイト
トレイトのメソッドにおけるライフタイムのアノテーションは、基本的には関数に似ています。impl
にもライフタイムのアノテーションがあることに注意してください。
参照
境界
ジェネリック型に境界を与え、特定のトレイトを実装していることを保証できるのと同様、ライフタイム(それ自身ジェネリック型)にも境界を与えることができます。:
は、ここでは多少異なる意味を持ちますが+
は同じです。以下の構文の意味をチェックしてください。
T: 'a
:T
内の 全ての 参照は'a
よりも長生きでなくてはなりません。T: Trait + 'a
:上に加えてT
はTrait
という名のトレイトを実装してなくてはなりません。
上記の構文を実際に動く例で見ていきましょう。where
キーワードの後に注目してください。
参照
強制
長いライフタイムは、短いものに圧縮することで、そのままでは動作しないスコープの中でも使用できるようになります。これは、Rustコンパイラが推論の結果として圧縮する場合と、複数のライフタイムを比較して圧縮する場合があります。
スタティックライフタイム
Rustにはいくつかの予約されたライフタイム名があります。その1つがstatic
で、2つの状況で使用することがあります。
2つの状況におけるstatic
は微妙に異なる意味を持っており、Rustを学ぶときの混乱の元になっています。いくつかの例とともにそれぞれの使い方を見てみましょう。
参照のライフタイム
参照のライフタイムが'static
であることは、参照が指し示す値がプログラムの実行中に渡って生き続けることを示します。また、より短いライフタイムに圧縮することも可能です。
'static
ライフタイムを持つ変数を作るには下記の2つ方法があります。どちらの場合も、値は読み取り専用のメモリ領域に格納されます。
static
宣言とともに定数を作成します。- 文字列リテラルで
&'static str
型を持つ変数を作成します。
では、それぞれの方法の例を見ていきましょう。
Since 'static
references only need to be valid for the remainder of a program's life, they can be created while the program is executed. Just to demonstrate, the below example uses Box::leak
to dynamically create 'static
references. In that case it definitely doesn't live for the entire duration, but only from the leaking point onward.
トレイト境界
トレイト境界としての'static
は型が非静的な参照を含まないことを意味します。言い換えると、レシーバはその型をいくらでも長く保持することができ、意図的にドロップするまでは決して無効になることはないということです。
次のポイントを押さえておきましょう。所有権のある値が'static
ライフタイム境界をパスするとしても、その値への参照が'static
ライフタイム境界をパスするとは限りません。
コンパイラのメッセージはこのようになります。
error[E0597]: `i` does not live long enough
--> src/lib.rs:15:15
|
15 | print_it(&i);
| ---------^^--
| | |
| | borrowed value does not live long enough
| argument requires that `i` is borrowed for `'static`
16 | }
| - `i` dropped here while still borrowed
参照
省略
ライフタイムのパターンのうちのいくつかは、他と比べてあまりにも一般的に使用されるため、タイプ量を減らし可読性を上げるために省くことができます。これは省略として知られており、それらのパターンが一般的であるというだけの理由で存在しています。
以下のコードでは省略の例を幾つかお見せします。より完全な説明を見たい場合は、「プログラミング言語Rust」のライフタイムの省略の項を見てください。
参照
トレイト
トレイトとは任意の型となりうるSelf
に対して定義されたメソッドの集合のことです。同じトレイト内で宣言されたメソッド同士はお互いにアクセスすることができます。
トレイトはあらゆるデータ型に実装することができます。以下の例ではまずAnimal
というメソッドの集合を定義し、その後Animal
トレイトをSheep
というデータ型に対して実装します。これによりAnimal
のメソッドをSheep
が使用することが可能になります。
導出(Derive)
コンパイラには、#[derive]
アトリビュートを用いることで型に対して特定のトレイトの標準的な実装を提供する機能があります。より複雑なことを行わせたい場合には、同名のトレイトを手動で実装することも可能です。
以下はderive可能なトレイトの一覧です。
- 型の比較に関連するトレイト:
Eq
,PartialEq
,Ord
,PartialOrd
. Clone
:これはコピーによって&T
からT
を作成するトレイトCopy
:これはムーブセマンティクスの代わりにコピーセマンティクスにするためのトレイトHash
:これは&T
からハッシュ値を計算するためのトレイトDefault
:これは空っぽのインスタンスを作成するためのトレイトDebug
:これは{:?}
フォーマッタを利用して値をフォーマットするためのトレイト
参照
dyn
を利用してトレイトを返す
Rustのコンパイラはあらゆる関数のリターン型に必要なスペースを知っておく必要があります。つまり、すべての関数は具体的な型を返す必要があるのです。他の言語と違って、Animal
のようなトレイトがある場合に、Animal
を返す関数を書くことはできません。なぜなら、そのトレイトの異なる実装はそれぞれ別の量のメモリを必要とするからです。
しかし、簡単な回避策があります。直接トレイトオブジェクトを返す代わりに、Animal
を 含む Box
を返すのです。Box
はヒープ中のメモリへの単なる参照です。参照のサイズは静的に知ることができ、コンパイラは参照がヒープに割り当てられたAnimal
を指していると保証できるので、私たちは関数からトレイトを返すことができます。
ヒープにメモリを割り当てる際、Rustは可能な限り明示的であろうとします。なので、もしあなたの関数がヒープ上のトレイトへのポインタを返す場合、例えばBox<dyn Animal>
のように、リターン型にdyn
キーワードをつける必要があります。
演算子のオーバーロード
Rustでは、多くの演算子はトレイトによってオーバーロードすることができます。つまり、一部の演算子は引数となる値の型に応じて異なる役割を果たすことができるということです。これが可能なのは、演算子が実際にはメソッド呼び出しの糖衣構文にすぎないからです。例えばa + b
における+
演算子はadd
メソッドを(a.add(b)
の形で)呼び出します。このadd
メソッドはAdd
トレイトの一部です。それ故、+
はAdd
トレイトを実装している全ての型に対して有効なのです。
Add
などの、演算子をオーバーロードするトレイトの一覧はcore::ops
にあります。
参照
ドロップ
Drop
トレイトにはメソッドが一つだけしかありません。drop
です。これは、オブジェクトがスコープから抜けた時に自動で呼ばれます。Drop
トレイトの主な使用目的は、インスタンスが所有する資源を開放することです。
Drop
トレイトを実装している型の例としてはBox
、Vec
、String
、File
、Process
等があげられます。Drop
トレイトは任意の型に対して手動で実装することができます。
以下の例ではdrop
メソッドにコンソールへの出力を追加することで、drop
が呼ばれたタイミングが分かるようにしています。
For a more practical example, here's how the Drop
trait can be used to automatically clean up temporary files when they're no longer needed:
イテレータ
Iterator
トレイトは、例えば配列のような、要素の集合に対してイテレータを実装するためのトレイトです。
このトレイトはnext
の要素に相当するものを決定するためのメソッドのみを要求します。このメソッドはimpl
ブロック内で手動で実装するか、あるいは(配列やrangeのように)自動で定義されます。
サッとイテレータを使いたい時は、for
文で集合からイテレータを作成することが良くあります。これは.into_iter()
メソッドを呼び出しています。
impl Trait
impl Trait
は2つの利用方法があります:
- 引数の型
- リターン型
引数の型
あなたの関数がジェネリックなトレイトを使用していて、特定の型を意識していない場合、impl Trait
を引数の型として利用して、関数宣言をシンプルにできます。
例えば、次のコードを考えてみましょう:
parse_csv_document
はジェネリックなので、BufReadを実装する任意の型を取ることができます。例えば、BufReader<File>
や[u8]
です。R
がどんな型かは重要ではなく、src
の型宣言に使われているだけなので、この関数は以下のように書くこともできます:
impl Trait
を引数の型として利用するということは、どのような形式の関数であるか明示できないので、注意してください。例えば、parse_csv_document::<std::io::Empty>(std::io::empty())
は2番目の例では動作しません。
リターン型
あなたの関数がMyTrait
を実装する型を返す場合、リターン型を-> impl MyTrait
のように書けます。これで型シグネチャをとてもシンプルにできます。
より重要なことに、Rustの型には書き表せないものがあるのです。例えば、あらゆるクロージャは独自の無名な具象型を持ちます。impl Trait
構文がない時は、クロージャを返すにはヒープ上に置かねばなりませんでした。しかし今では次のようにすべて静的に行えます。
impl Trait
を使って、map
やfilter
クロージャを使うイテレータを返すこともできます。おかげでmap
やfilter
を簡単に使えます。クロージャ型は名前を持たないので、あなたの関数がクロージャを持つイテレータを返す場合、明示的なリターン型を書くことはできません。しかしimpl Trait
を使うことで簡単にできます:
クローン
メモリ上の資源を扱う際、変数束縛や関数呼び出しを介して移動させるのがデフォルトの挙動です。しかしながら、場合によっては資源のコピーを作るのが適切なこともあります。
Clone
トレイトはまさにこのためにあります。普通はClone
トレイトで定義されている.clone()
を用います。
スーパートレイト
Rustには"継承"はありませんが、あるトレイトを別のトレイトの上位集合として定義できます。例えば:
参照
The Rust Programming Language chapter on supertraits
トレイトの曖昧性解決
A type can implement many different traits. What if two traits both require the same name for a function? For example, many traits might have a method named get()
. They might even have different return types!
Good news: because each trait implementation gets its own impl
block, it's clear which trait's get
method you're implementing.
What about when it comes time to call those methods? To disambiguate between them, we have to use Fully Qualified Syntax.
参照
The Rust Programming Language chapter on Fully Qualified syntax
macro_rules!
Rustはメタプログラミングを可能にする、パワフルなマクロシステムを備えています。これまで見てきたように、マクロは!
で終わることを除けば関数のように見えます。関数と違うのは関数呼び出しを生成する代わりに、ソースコード中に展開され、周囲のプログラムとともにコンパイルされる点です。しかし、Cやその他の言語のマクロが文字列のプリプロセッシングをするのと異なり、Rustのマクロは抽象構文木へと展開されるので、予期せぬ演算子の優先順位のバグに出くわすことがありません。
マクロを作成するにはmacro_rules!
というマクロを使用します。
ではどうしてマクロは便利なのでしょうか?
-
同じことを繰り返し書いてはいけないから。複数の場所で、別の型だけれど似たような機能が必要な時がよくあります。しばしば、マクロはコードを繰り返し書くのを避ける有用な手段なのです(あとで詳述)。
-
ドメイン特化言語であるから。マクロを使うと、特定の目的のための特定の構文を定義することができます(あとで詳述)。
-
可変個引数によるインターフェース。取る引数の数が可変であるようなインターフェースを定義したくなることもあるでしょう。例えば、
println!
は、フォーマット文字列に依存した任意の数の引数を取ることができます(あとで詳述)!
構文
以下のサブセクションでは、Rustにおいてマクロを定義する方法を示します。3つの基本的な考え方があります:
識別子
macroの引数は$
が頭につきます。型は 識別子 でアノテーションされます。
使用できる識別子には以下のようなものがあります。
block
expr
式に使用。ident
関数、変数の名前に使用。item
literal
はリテラル定数。pat
(パターン)path
stmt
(宣言)tt
(トークンツリー)ty
(型)vis
(可視性修飾子)(訳注:pub (crate)
とか)
完全なリストを見るには、Rustリファレンスを読んでください。
オーバーロード
マクロは異なる引数の組み合わせを取るようにオーバーロードすることができるため、macro_rules!
はマッチと似たような使い方をすることができます。
繰り返し
マクロは引数のリストの中で+
を使うことができ、そうすることによって、引数が少なくとも1回以上繰り返されるということを示すことができます。同様に*
の場合は、0以上を示します。
以下の例では、マッチ対象を $(...),+
で囲むことにより、カンマで区切られた1つ以上の式とマッチします。最後のセミコロンは必須ではないことに注目しましょう。
DRY (Don't Repeat Yourself)
マクロは関数やテストなどにおいて、共通の部分を抽出することでDRYなコードを書くのに役立ちます。ここではVec<T>
に+=
、*=
、-=
を実装、テストするにあたって、マクロがどのように役立つかを見ていきます。
$ rustc --test dry.rs && ./dry
running 3 tests
test test::mul_assign ... ok
test test::add_assign ... ok
test test::sub_assign ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured
Domain Specific Languages (ドメイン特化言語、DSLs)
ここで言うDSLとはRustマクロに埋め込まれた小さな「言語」のことです。マクロ機能は通常のRustのプログラムへと展開されるので、これは完全に正当なRustなのですが、まるで小さな言語であるかのように見えます。これにより、(一定の条件のもとで)なんらかの特定の機能のための簡潔・直感的な構文を定義することができるようになります。
ちょっとした計算機APIを定義したいとしましょう。式を与えると、出力がコンソールに書き出されるようにしたいです。
出力はこうなります:
1 + 2 = 3
(1 + 2) * (3 / 4) = 0
これはとても単純な例ですが、lazy_static
やclap
のように、もっと複雑なインターフェースも開発されています。
また、マクロの中に2組の括弧があることにも注目してください。外側のは、()
や[]
に加え、macro_rules!
の構文の一部です。
可変個引数によるインターフェース
_可変個引数の_インターフェースとは、任意の数の引数を取るものです。例えば、println!
は、フォーマット文字列の定義に従い、任意の数の引数を取ることができます。
前のセクションのcalculate!
マクロを、可変個引数に拡張することができます:
出力はこうなります:
1 + 2 = 3
3 + 4 = 7
(2 * 3) + 1 = 7
エラーハンドリング
エラーハンドリングとは失敗の起きる可能性を扱うプロセスのことです。例えば、ファイルを読み込むのに失敗した際、その 誤った インプットを使い続けるのは明らかに問題です。そのようなエラーを通知して明示的に扱うことで、残りのプログラムに問題が波及することを防ぐことができるようになります。
Rustには、これからこの章で見ていく通り、エラーを処理するための様々な方法が存在します。それらは全て僅かに異なり、ユースケースも異なります。経験則として:
明示的なpanic
はテストや復旧不可能なエラーに対して効果的です。プロトタイプにも便利で、例えば未実装の関数を扱う時などに有効ですが、このような場合にはより叙述的なunimplemented
の方が良いでしょう。テストにおいてはpanic
は明示的にテストを失敗させるための良い手法になるでしょう。
Option
型は値があるとは限らない場合や、値が無いことがエラーの条件とならない場合に有効です。例えば親ディレクトリ(/
やC:
はそれを持ちません)などです。Option
を扱う際は、unwrap
がプロトタイプや値が確実に存在することが約束されるケースに使えます。しかし、expect
の方が何かが上手くいかなかった際にエラーメッセージを指定することができるため、より便利でしょう。
何かが上手くいかない可能性があったり、呼び出し元が問題を処理しなければならない時は、Result
を使いましょう。unwrap
やexpect
を実行することもできます(テストや短期的なプロトタイプ以外では使わないでください)。
より詳細なエラーハンドリングに関する議論については、オフィシャルブックの該当の章を参考にしてください。
panic
panic
は、最もシンプルなエラーハンドリングの仕組みです。エラーメッセージの出力、スタックの巻き戻し、そして多くの場合プログラムの終了を実行します。例として、エラー条件に対して明示的にpanic
を呼び出してみましょう。
The first call to drink
works. The second panics and thus the third is never called.
abort
and unwind
The previous section illustrates the error handling mechanism panic
. Different code paths can be conditionally compiled based on the panic setting. The current values available are unwind
and abort
.
Building on the prior lemonade example, we explicitly use the panic strategy to exercise different lines of code.
Here is another example focusing on rewriting drink()
and explicitly use the unwind
keyword.
The panic strategy can be set from the command line by using abort
or unwind
.
rustc lemonade.rs -C panic=abort
Option
とunwrap
以前の例では、甘いレモネードを飲んだ際にpanic
を呼び出すことによって、自由にプログラムの実行を失敗させられることが分かりました。では、何らかの飲み物を期待しているにもかかわらず、何も受け取らなかったらどうなるでしょう?これは悲惨なケースになるので、エラーハンドリングする必要があります!
このケースに対して、レモネードと同じように、空文字列(""
)と比較することもできますが、せっかくRustを使っているので、その代わりにコンパイラに飲み物がないケースを指摘させてみましょう。
std
ライブラリの中の、Option<T>
と呼ばれるenum
は、任意の型T
である変数の値が存在しない可能性がある場合に用いられます。値の状態によって、下記2つのパターンのうちの1つとして扱われます。
Some(T)
:型T
の値がある場合None
:値が存在しない場合
これらはmatch
を用いて明示的に扱うこともできますし、unwrap
で暗黙に処理することもできます。後者はSome
の中の値を返すかpanic
するかのどちらかです。
expectメソッドを用いて、panic
を手動でカスタマイズできることに触れておきましょう。これは(unwrap
をそのまま用いた場合よりも)内容が理解しやすいエラーメッセージを出力するのに役立ちます。次の例では、結果をより明示的に、可能ならいつでもpanic
できるように扱っていきます。
?
によるOption
のアンパック
Option
をアンパックするにはmatch
文を使うこともできますが、?
を使う方が簡単になることが多いでしょう。Option
のx
があるとすると、x?
を評価した値は、x
がSome
の場合はx
に格納された値となり、そうでなければ実行中の関数を終了させ、None
を返します。
多くの?
を共に使うことで、リーダブルなコードを書くことができます。
コンビネータ:map
match
はOption
を扱うのに適したメソッドです。しかし、大量にこれを使用しているとじきに億劫になってくるでしょう。引数の値が有効である(訳注: この場合はNone
でない)必要がある関数を扱う際には特にそうです。そうした場合には、コンビネータを使うと、処理の流れをモジュール化されたやり方で管理できます。
Some -> Some
あるいはNone -> None
の単純な操作を適用する必要がある場合には、Option
はmap()
というビルトインのメソッドを提供していますので、これを使用しましょう。map()
のフレキシビリティは、複数のmap()
をチェインしなければならない場合にさらに際立ちます。
以下の例では、process()
が直前の関数全てを用いた場合と同じ機能を、よりコンパクトに果たしているのがわかります。
参照
コンビネータ:and_then
先ほどはmap()
を、チェイン構文を用いてmatch
文を単純化する物として説明しました。しかしOption<T>
を返す関数に対してのmap()
の使用はネストしたOption<Option<T>>
を生じさせます。ですので、複数の関数呼び出しをチェインさせることは混乱を招く場合があります。そんな時こそand_then()
の出番です。他の言語ではflatmapと呼ばれることもあります。
and_then()
は引数として与えられた関数にラップされた値を渡しますが、その値がNone
だった場合はNone
を返します。
以下の例ではcookable_v3()
はOption<Food>
を返すため、and_then()
ではなくmap()
を使用すると最終的にOption<Option<Food>>
になります。これはeat()
には不適切な型です。
参照
クロージャ, Option
, Option::and_then()
, Option::flatten()
Unpacking options and defaults
There is more than one way to unpack an Option
and fall back on a default if it is None
. To choose the one that meets our needs, we need to consider the following:
- do we need eager or lazy evaluation?
- do we need to keep the original empty value intact, or modify it in place?
or()
is chainable, evaluates eagerly, keeps empty value intact
or()
is chainable and eagerly evaluates its argument, as is shown in the following example. Note that because or
's arguments are evaluated eagerly, the variable passed to or
is moved.
or_else()
is chainable, evaluates lazily, keeps empty value intact
Another alternative is to use or_else
, which is also chainable, and evaluates lazily, as is shown in the following example:
get_or_insert()
evaluates eagerly, modifies empty value in place
To make sure that an Option
contains a value, we can use get_or_insert
to modify it in place with a fallback value, as is shown in the following example. Note that get_or_insert
eagerly evaluates its parameter, so variable apple
is moved:
get_or_insert_with()
evaluates lazily, modifies empty value in place
Instead of explicitly providing a value to fall back on, we can pass a closure to get_or_insert_with
, as follows:
参照
closures
, get_or_insert
, get_or_insert_with
, moved variables
, or
, or_else
Result
Result
は、リッチなバージョンのOption
型で_値の不在_の可能性の代わりに_エラー_の可能性を示します。
つまり、Result<T, E>
は以下の2つの結果を持ちます。
Ok<T>
:要素T
が見つかった場合Err<E>
:要素E
とともにエラーが見つかった場合
慣例により、Ok
が期待される結果であり、Err
は期待されない結果です。
Option
と同様、Result
は多くのメソッドを持ちます。例えばunwrap()
は、T
もしくはpanic
をもたらします。エラーハンドリングでは、Result
とOption
で重複するコンビネータが多くあります。
Rustを書いていく中で、parse()
メソッドなど、Result
型を返すメソッドを目にするでしょう。文字列を他の型にパースすることは必ずしも成功する訳ではないため、Result
を返すことで失敗するケースについてもカバーできるのです。
早速、文字列をparse()
した場合の成功例と失敗例を見てみましょう。
失敗例では、parse()
がエラーを返すためunwrap()
がパニックします。そして、panic
はプログラムを終了させて不快なエラーメッセージを出力します。
エラーメッセージを改善するために、リターン型に対してもっと明確になるべきで、またエラーを明示的に処理することを考えるべきです。
main
内で使うResult
Result
型は、明示的な指定によりmain
関数のリターン型にもなります。一般に、main
関数は以下のような形になるでしょう。
fn main() {
println!("Hello World!");
}
一方main
でResult
をリターン型とすることも可能です。エラーがmain
関数内で発生した時、エラーコードを返し、エラーに関するデバッグ表記を(Debug
トレイトを使って)出力します。以下の例ではそのようなシナリオを示し、この先の節でカバーする内容に触れていきます。
Result
のmap
前の例で見たmultiply
でのパニックは、コードを強固にするためには書きません。一般に、呼び出した側がエラーをどのように対処するべきかを自由に決められるように、エラーを呼び出した場所に返すのが好ましいです。
まずは、どのようなエラー型を扱っているのかを知る必要があります。Err
型を定めるために、i32
に対しFromStr
トレイトを使って実装されたparse()
を見てみましょう。結果、Err
型はParseIntError
というものであることが分かります。
以下の例では、単純なmatch
文が全体として扱いづらいコードにしています。
幸運にも、Option
のmap
、and_then
、その他多くのコンビネータもResult
のために実装されています。Result
に全てのリストが記載されています。
Result
に対するエイリアス
特定のResult
型を何度も使いたくなるのはどんな時でしょう?Rustはエイリアスの作成をサポートしていたことを思い出してください。便利なことに、特定のResult
型に対しても定義することができます。
モジュールレベルでは、エイリアスの作成は非常に役に立ちます。特定のモジュールで見られるエラーは同じErr
型を持つため、単一のエイリアスで簡潔にResults
に関わる_全て_を定義できます。std
ライブラリが提供するもの(io::Result
)もあるほど有益なのです!
簡単な例で構文を見てみましょう。
参照
早期リターン
前の例では、コンビネータの活用によりエラーを明示的に処理しました。場合分けに対する別の対処法として、match
文と早期リターンを組み合わせて使うこともできます。
つまり、エラーが発生した時点で関数の実行を止め、エラーを返してしまうという単純な方法が使えるということです。この方法の方がより読みやすく書きやすい場合があります。早期リターンを使って実装された、前の例の新たなバージョンを考えてみましょう。
ここまでで、コンビネータと早期リターンによる明示的なエラーハンドリングについて学びました。しかし、パニックは一般に避けたいですが、全てのエラーを明示的に処理するのも厄介でしょう。
次の節では、panic
を発生させずにunwrap
する必要があるケースのための?
について紹介していきます。
?
の導入
時にはpanic
の可能性を無視して、unwrap
のシンプルさを活用したいこともあるでしょう。今までのunwrap
は、値を_取り出す_ためだけであろうとも、ネストを深く書くことを要求しました。そして、これがまさに?
の目的です。
Err
を見つけるにあたり、2つのとるべき行動があります。
- 可能な限り避けたいと決めた
panic!
Err
は処理できないことを意味するためreturn
?
は_ほぼ_1まさしく、Err
に対してpanic
するよりreturn
するという点でunwrap
と同等です。コンビネータを使った以前の例をどれだけ簡潔に書けるか見てみましょう。
try!
マクロ
?
ができる前、同様の動作をtry!
マクロによって行うことができました。現在は?
演算子が推奨されていますが、古いコードではtry!
に出会うこともあります。try!
を使って前の例と同じmultiply
関数を実装すると、以下のようになるでしょう。
詳細はre-enter ?を参照。
複数のエラー型
Result
が他のResult
と連携したり、Option
が他のOption
と連携するなど、今までの例はとても便利な物でした。
時にはOption
がResult
と連携したり、Result<T, Error1>
がResult<T, Error2>
と連携する必要もあるでしょう。そのような場面では、異なるエラー型を構成しやすく、かつ連携しやすく管理したいです。
以下のコードはunwrap
の2つのインスタンスが異なるエラー型を生成します。Vec::first
はOption
を返し、一方でparse::<i32>
はResult<i32, ParseIntError>
を返しています。
この先の節では、これらの問題を処理する方法について見ていきます。
Option
からResult
を取り出す
混在するエラー型に対する最も基本的な対処法は、単にお互いを埋め込んでしまうことです。
There are times when we'll want to stop processing on errors (like with ?
) but keep going when the Option
is None
. The transpose
function comes in handy to swap the Result
and Option
.
エラー型を定義する
異なるエラー型をマスクし単一のエラー型として扱えるようにすると、コードがシンプルになる場合があります。ここでは自前のエラー型でそれを示してみます。
Rustはユーザーによる新たなエラー型の定義をサポートします。一般に「良い」エラー型は、
- 異なるエラーをまとめて同じ型として扱う。
- ユーザーに優しいエラーメッセージを提供する。
- 他の型との比較を楽にする。
- 良い例:
Err(EmptyVec)
- 悪い例:
Err("Please use a vector with at least one element".to_owned())
- 良い例:
- エラーについての情報を保持できる。
- 良い例:
Err(BadChar(c, position))
- 悪い例:
Err("+ cannot be used here".to_owned())
- 良い例:
- 他のエラーと問題なく連携できる。
エラーをBox
する
元のエラーを維持しながらシンプルなコードを書くには、Box
してしまうと良いでしょう。欠点として、元のエラー型はランタイムまで判明せず、静的に決定されないことが挙げられます。
標準ライブラリはBox
に、From
を介してあらゆるError
トレイトを実装した型からBox<Error>
トレイトオブジェクトへの変換を実装させることで、エラーをboxしやすくしてくれます。
参照
?
の他の活用法
以前の例ではparse
の呼び出しに対するその場での対応として、エラーをライブラリのエラーからboxされたエラーへとmap
していました。
.and_then(|s| s.parse::<i32>())
.map_err(|e| e.into())
簡単でよくあるオペレーションのため、可能なら省略してしまえると便利だったでしょう。でも残念、and_then
が十分にフレキシブルでないため、それはできません。ただその代わり、?
なら使えます。
?
の挙動は、unwrap
またはreturn Err(err)
として説明されていました。これはほぼ正解で、本当はunwrap
、もしくはreturn Err(From::from(err))
という意味があります。From::from
は異なる型の間での変換ユーティリティであることから、エラーがリターン型に変換可能な場合に?
を使うことで、その変換を自動的に行ってくれます。
前の例を?
を使ったものに書き換えてみましょう。その結果、From::from
がエラー型に実装されている時map_err
は消えてなくなります。
これでかなり綺麗になりました。元のpanic
と比べ、リターン型がResult
であることを除けば、unwrap
の呼び出しを?
で置き換えたものに非常に似ています。結果、そのResult
は上のレベルで分解されなければなりません。
参照
エラーをラップする
Boxする方法の代替として、エラーを自前のエラー型としてラップする方法もあります。
これはエラーの処理のボイラープレートを増やしてしまい、全てのアプリケーションで必要になる訳では無いでしょう。これらのボイラープレートの処理を代わりにやってくれるようなライブラリもあります。
参照
Result
をイテレートする
Iter::map
オペレーションは失敗することもあります。例えば、
ここでは、この対処法についてみてみましょう。
filter_map()
を使って失敗した要素のみを無視する
filter_map
は関数を呼び出し、結果がNone
になるものだけ取り除きます。
Collect the failed items with map_err()
and filter_map()
map_err
calls a function with the error, so by adding that to the previous filter_map
solution we can save them off to the side while iterating.
collect()
で処理全体を失敗させる
Result
は、それらのベクタ(Vec<Result<T, E>>
)からベクタのそれ(Result<Vec<T>, E>
)へと変換できるようにするため、FromIterator
を実装します。Result::Err
が見つかり次第、イテレーションは終了します。
同じテクニックは、Option
を用いて行うこともできます。
partition()
を使って全ての正常な値と失敗をまとめる
結果を見てみると、まだ全てResult
にラップされていることに気づくでしょう。もう少しのボイラープレートが必要です。
標準ライブラリの型
std
ライブラリは、基本データ型を劇的に拡張するカスタム型を数多く提供します。例えば以下です。
- 拡張可能な文字列である
String
。例えば:"hello world"
- growable vectors:
[1, 2, 3]
- オプション型:
Option<i32>
- エラーハンドリング用の
Result<i32, i32>
- ヒープ上の資源へのポインタ:
Box<i32>
参照
Box、スタックとヒープ
Rustにおいて、すべての値はデフォルトでスタックに割り当てられます。Box<T>
を作成することで、値を ボックス化 、すなわちヒープ上に割り当てることができます。ボックスとは正確にはヒープ上におかれたT
の値へのスマートポインタです。ボックスがスコープを抜けると、デストラクタが呼ばれて内包するオブジェクトが破棄され、ヒープメモリが解放されます。
ボックス化された値は*
演算子を用いてデリファレンスすることができます。これにより一段と直接的な操作が可能になります。
ベクタ型
「ベクタ」はサイズを変更可能な配列です。スライスと同様、そのサイズはコンパイル時には不定ですが、いつでも要素を追加したり削除したりすることができます。ベクタは3つの要素で、その特徴が完全に決まります。
- データへのポインタ
- 長さ
- 容量
ベクタはその容量を超えない限りにおいて長くしていくことができます。超えた場合には、より大きな容量を持つように割り当てなおされます。
Vec
型のメソッドの一覧はstd::vecモジュールを見てください。
文字列
Rustには文字列を扱う型が2つあります。String
と&str
です。
String
は有効なUTF-8の配列であることを保証されたバイトのベクタ(Vec<u8>
)として保持されます。ヒープ上に保持され、伸長可能で、末端にnull文字を含みません。
&str
は有効なUTF-8の配列のスライス(&[u8]
)で、いつでもString
に変換することができます。&[T]
がいつでもVec<T>
に変換できるのと同様です。
str
/String
のメソッドをもっと見たい場合はstd::str、std::stringモジュールを参照してください。
Literals and escapes
There are multiple ways to write string literals with special characters in them. All result in a similar &str
so it's best to use the form that is the most convenient to write. Similarly there are multiple ways to write byte string literals, which all result in &[u8; N]
.
Generally special characters are escaped with a backslash character: \
. This way you can add any character to your string, even unprintable ones and ones that you don't know how to type. If you want a literal backslash, escape it with another one: \\
String or character literal delimiters occurring within a literal must be escaped: "\""
, '\''
.
Sometimes there are just too many characters that need to be escaped or it's just much more convenient to write a string out as-is. This is where raw string literals come into play.
Want a string that's not UTF-8? (Remember, str
and String
must be valid UTF-8). Or maybe you want an array of bytes that's mostly text? Byte strings to the rescue!
For conversions between character encodings check out the encoding crate.
A more detailed listing of the ways to write string literals and escape characters is given in the 'Tokens' chapter of the Rust Reference.
Option
プログラムの一部が失敗した際、panic!
するよりも、エラーを捕捉する方が望ましい場合があります。これはOption
という列挙型を用いることで可能になります。
列挙型Option<T>
には2つの値があります。
None
、これは実行の失敗か値の欠如を示します。Some(value)
、型T
のvalue
をラップするタプルです。
Result
これまでの例で、失敗する可能性のある関数の返り値として、列挙型Option
が使用でき、失敗時の返り値にはNone
を用いることを見てきました。しかし、時には なぜ そのオペレーションが失敗したのかを明示することが重要な場合があります。そのためにはResult
列挙型を使用します。
列挙型Result<T, E>
は2つの値をとりえます。
Ok(value)
... これはオペレーションが成功したことを意味し、返り値value
をラップします。(value
は型T
を持ちます。)Err(why)
... これはオペレーションの失敗を意味します。why
をラップしており、ここには失敗した理由が(必ずではありませんが)書かれています。(why
の型はE
です。)
?
マッチを利用して結果をチェインするのは中々面倒です。幸いなことに、?
マクロを使用すればイケてるコードに戻すことができます。?
はResult
を返す式の末尾で使います。Err(err)
の分岐がreturn Err(From::from(err))
という早期リターンに展開され、Ok(ok)
の分岐がok
の式に展開されるようなマッチ式と等価です。
公式ドキュメントをチェックすることをオススメします。Result
型を扱う関数やResult
型のメソッドが多く挙げられています。
panic!
panic!
マクロはパニックを生成し、スタックの巻き戻しを開始するために使用することができます。巻き戻しの間、ランタイムは、(訳注: panicを起こした)スレッドが 所有権を持つ 全ての資源のデストラクタを呼び出し、メモリ上から解放します。
今回はシングルスレッドのプログラムを実行しているので、panic!
はプログラムにパニックメッセージを表示させ、exitします。
panic!
がメモリリークを引き起こさないことを確認しましょう。
$ rustc panic.rs && valgrind ./panic
==4401== Memcheck, a memory error detector
==4401== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==4401== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==4401== Command: ./panic
==4401==
thread '<main>' panicked at 'division by zero', panic.rs:5
==4401==
==4401== HEAP SUMMARY:
==4401== in use at exit: 0 bytes in 0 blocks
==4401== total heap usage: 18 allocs, 18 frees, 1,648 bytes allocated
==4401==
==4401== All heap blocks were freed -- no leaks are possible
==4401==
==4401== For counts of detected and suppressed errors, rerun with: -v
==4401== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
ハッシュマップ
ベクタ型が値を整数のインデックスで保持するのに対し、HashMap
ではキーで保持します。HashMap
のキーはブーリアン、整数、文字列等のEq
とHash
トレイトを保持する型なら何でもOKです。次のセクションでより詳しく見ていきます。
ベクタ型と同様、伸長可能ですが、HashMap
の場合さらに、スペースが余っているときには小さくすることも可能です。HashMap
を一定の容量のエリアに作成するときはHashMap::with_capacity(uint)
を、デフォルトの容量で作成するときはHashMap::new()
を用います。後者が推奨されています。
ハッシングやハッシュマップ(ハッシュテーブルと呼ばれることもあります)の仕組みについて、より詳しく知りたい場合はWikipediaのハッシュテーブルのページを見てください。
キー型の変種
Eq
とHash
トレイトを実装している型ならば、なんでもHashMap
のキーになることができます。例えば以下です。
bool
(キーになりうる値が2つしかないので実用的ではないですが…)int
、uint
、あるいは他の整数型String
と&str
(Tips:String
をキーにしたハッシュマップを作製した場合、.get()
メソッドの引数に&str
を与えて値を取得することができます。)
f32
とf64
はHash
を実装して いない ことに注意しましょう。おそらくこれは浮動小数点演算時に誤差が発生するため、キーとして使用すると、恐ろしいほどエラーの元となるためです。
集合型は、その要素となっている全ての型がEq
を、あるいはHash
を実装している場合、必ず同じトレイトを実装しています。例えば、Vec<T>
はT
がHash
を実装している場合、Hash
を実装します。
独自の型にEq
あるいはHash
を実装するのは簡単です。以下の一行で済みます。#[derive(PartialEq, Eq, Hash)]
後はコンパイラがよしなにしてくれます。これらのトレイトの詳細をコントロールしたい場合、Eq
やHash
を自分で実装することもできます。この文書ではHash
トレイトを実装する方法の詳細については触れません。
struct
をHashMap
で扱う際の例として、とてもシンプルなユーザーログインシステムを作成してみましょう。
ハッシュ集合
値がなく、キーだけのHashMap
を想像してみてください。これはハッシュ集合(HashSet
)と呼ばれるものです。(HashSet<T>
は、実際にはHashMap<T, ()>
のラッパーです。)
「何の意味があるの?フツーにキーをVec
に入れればいいじゃん」そう思いましたね?
それは、HashSet
独自の機能として、要素に重複がないということが保証されるためです。これは全ての集合型がもつ機能です。HashSet
はその実装の1つであり、他にはBTreeSet
等があります。
HashSet
に、すでに存在する値を加えようとすると、(すなわち、加えようとしている値のハッシュ値と、要素中のいずれかの値のハッシュ値が等しい場合、)新しい値によって古い値が上書きされます。
これは、同じ値を2つ以上欲しくない場合や、すでにある値を持っているか知りたい場合にとても有効です。
しかし、集合型の機能はそれだけではありません。
集合型には4つの主要なメソッドがあり、(すべてイテレータを返します。)
-
union
:2つの集合型のどちらか一方にある値を全て取得。 -
difference
:1つ目の集合にあり、かつ2つ目には存在しない値を全て取得。 -
intersection
:両方の集合にある値のみを取得。 -
symmetric_difference
:どちらか一方の集合には存在するが、両方には ない 値を取得。
以下の例でこれらをすべて見ていきましょう。
例は公式ドキュメントから持ってきています。
Rc
When multiple ownership is needed, Rc
(Reference Counting) can be used. Rc
keeps track of the number of the references which means the number of owners of the value wrapped inside an Rc
.
Reference count of an Rc
increases by 1 whenever an Rc
is cloned, and decreases by 1 whenever one cloned Rc
is dropped out of the scope. When an Rc
's reference count becomes zero (which means there are no remaining owners), both the Rc
and the value are all dropped.
Cloning an Rc
never performs a deep copy. Cloning creates just another pointer to the wrapped value, and increments the count.
参照
std::rc and std::sync::arc.
Arc
When shared ownership between threads is needed, Arc
(Atomically Reference Counted) can be used. This struct, via the Clone
implementation can create a reference pointer for the location of a value in the memory heap while increasing the reference counter. As it shares ownership between threads, when the last reference pointer to a value is out of scope, the variable is dropped.
標準ライブラリのその他
他にも、様々な型がstdライブラリの中で提供されています。例えば以下の機能を果たすための物があります。
- スレッド
- チャネル
- ファイル I/O
これらにより基本データ型の提供する機能よりも遥かに豊かなことが実現できます。
参照
スレッド
Rustはspawn
関数を用いてOSのネイティブスレッドを開始することができます。この関数の引数はmoveクロージャ(訳注: 参照ではなく値を取るクロージャ)です。
これらのスレッドのスケジューリングはOSによって行われます。
テストケース:map-reduce
Rust makes it very easy to parallelize data processing, without many of the headaches traditionally associated with such an attempt.
The standard library provides great threading primitives out of the box. These, combined with Rust's concept of Ownership and aliasing rules, automatically prevent data races.
The aliasing rules (one writable reference XOR many readable references) automatically prevent you from manipulating state that is visible to other threads. (Where synchronization is needed, there are synchronization primitives like Mutex
es or Channel
s.)
In this example, we will calculate the sum of all digits in a block of numbers. We will do this by parcelling out chunks of the block into different threads. Each thread will sum its tiny block of digits, and subsequently we will sum the intermediate sums produced by each thread.
Note that, although we're passing references across thread boundaries, Rust understands that we're only passing read-only references, and that thus no unsafety or data races can occur. Also because the references we're passing have 'static
lifetimes, Rust understands that our data won't be destroyed while these threads are still running. (When you need to share non-static
data between threads, you can use a smart pointer like Arc
to keep the data alive and avoid non-static
lifetimes.)
Assignments
It is not wise to let our number of threads depend on user inputted data. What if the user decides to insert a lot of spaces? Do we really want to spawn 2,000 threads? Modify the program so that the data is always chunked into a limited number of chunks, defined by a static constant at the beginning of the program.
参照
- スレッド
- ベクタ型, イテレータ
- クロージャ, move semantics and
move
クロージャ - デストラクト 代入
- 型推論を補助する ターボフィッシュ記法
- unwrap と expect
- enumerate
チャネル
Rustは、スレッド間のコミュニケーションのために、非同期のチャネルを提供しています。チャネルは2つのエンドポイント、すなわち送信者と受信者を介して、情報の一方向への流れを作り出すことを可能にしています。
ファイルパス
構造体Path
は、ファイルシステム中のパスを表します。Path
には2つの変種があります。UNIXライクなファイルシステムのためのposix::Path
と、Windows用のwindows::Path
です。それぞれプラットフォームに対応したPath
をエクスポートします。
Path
はOsStr
から作ることができます。そうすればそのパスが指すファイル・ディレクトリの情報を取得するためのメソッドがいくつか使えるようになります。
Path
はイミュータブルです。Path
の所有権ありのバージョンがPathBuf
です。Path
とPathBuf
の関係は、str
とString
の関係に似ています。PathBuf
はそのまま変更でき、Path
にデリファレンスすることができます。
Path
の実態はUTF-8の文字列 ではなく 、OsString
であることに注意しましょう。したがって、Path
を&str
に変換するのは無条件 ではなく 、失敗する可能性があります。それゆえOption
型が返されます。しかしPath
からOsString
あるいは&OsStr
への変換はそれぞれinto_os_string
とas_os_str
によって無条件でできます。
他のPath
メソッド(posix::Path
とwindows::Path
)をチェックするのを忘れずに!それとMetadata
構造体も見ておくことをオススメします。
参照
ファイル I/O
File
構造体は開かれたファイルを表し(実際にはファイルディスクリプタのラッパーです)、読み込み・書き込み権限のどちらか一方、あるいは両方を提供します。
Since many things can go wrong when doing file I/O, all the File
methods return the io::Result<T>
type, which is an alias for Result<T, io::Error>
.
これはI/Oに関するオペレーションの失敗をより明瞭にします。このおかげでプログラマは直面した失敗を全て見ることができ、より生産的な方法でそれらを扱うことが可能になります。
open
open
関数を用いることで読み込み専用モードでファイルを開くことが可能です。
File
はファイルディスクリプタという資源を保持しており、drop
時にはファイルを閉じるところまで面倒を見てくれます。
以下が成功時に期待されるアウトプットです。
$ echo "Hello World!" > hello.txt
$ rustc open.rs && ./open
hello.txt contains:
Hello World!
(気が向いたなら、上記の例を様々な形で失敗させてみましょう。例えばhello.txt
が存在しないとか、読み込み権限がないとか、そういった状況で実行してみてください。)
create
create
関数はファイルを書き込み専用モードで開きます。すでにファイルが存在している場合、破棄して新しい物を作成します。
static LOREM_IPSUM: &str =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
";
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
fn main() {
let path = Path::new("lorem_ipsum.txt");
let display = path.display();
// ファイルを書き込み専用モードで開きます。返り値は`io::Result<File>`。
let mut file = match File::create(&path) {
Err(why) => panic!("couldn't create {}: {}", display, why),
Ok(file) => file,
};
// `LOREM_IPSUM`の文字列を`file`に書き込みます。返り値は`io::Result<()>`。
match file.write_all(LOREM_IPSUM.as_bytes()) {
Err(why) => panic!("couldn't write to {}: {}", display, why),
Ok(_) => println!("successfully wrote to {}", display),
}
}
以下が成功時に期待されるアウトプットです。
$ rustc create.rs && ./create
successfully wrote to lorem_ipsum.txt
$ cat lorem_ipsum.txt
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
前項の例と同じように、様々な失敗パターンをためしてみることをオススメします。
OpenOptions
構造体を利用して、ファイルの開き方を設定できます。
read lines
単純なやり方
テキストファイルの行を読み込むのを、初心者が初めて実装した場合、以下のようになるでしょう。
lines()
メソッドはファイルの各行のイテレータを返すので、インラインでマップを実行し結果を収集することもできます。そうすると、より簡潔で読みやすい表現となります。
上の例では、lines()
から返された&str
をそれぞれto_string()
とString::from
を使って、所有権のあるString
型に変換しなければならない点に注意してください。
より効率的なやり方
ここでは、開いたFile
の所有権をBufReader
構造体に渡します。BufReader
は内部的なバッファを使い、中間のメモリ割り当てを削減します。
read_lines
を更新して、それぞれの行に対してメモリ上に新しいString
オブジェクトを割り当てるのではなく、イテレータを返すようにします。
use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;
fn main() {
// hosts.txtファイルは現在のパスに存在しなければなりません。
if let Ok(lines) = read_lines("./hosts.txt") {
// イテレータを消費し、Option型のStringを返します。
for line in lines.map_while(Result::ok) {
println!("{}", line);
}
}
}
// 出力はResult型にラップされ、エラーをマッチできるようになります。
// ファイルの各行のReaderへのイテレータを返します。
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where P: AsRef<Path>, {
let file = File::open(filename)?;
Ok(io::BufReader::new(file).lines())
}
このプログラムを実行すると、単に各行を出力します。
$ echo -e "127.0.0.1\n192.168.0.1\n" > hosts.txt
$ rustc read_lines.rs && ./read_lines
127.0.0.1
192.168.0.1
File::open
はジェネリックなAsRef<Path>
を引数にとるので、ジェネリックなread_lines
メソッドも、where
キーワードを使って、同じジェネリックな制約を持たせています。
この処理は、ファイルの中身全てをメモリ上のString
にするよりも効率的です。メモリ上にString
を作ると、より大きなファイルを取り扱う際に、パフォーマンスの問題につながります。
子プロセス
process::Output
構造体は終了したプロセスのアウトプットを表し、process::Command
構造体はプロセスの作成を行います。
(余裕があれば、上の例でrustc
に不正なフラグを渡し、どうなるか見てみましょう)
パイプ
The std::process::Child
struct represents a child process, and exposes the stdin
, stdout
and stderr
handles for interaction with the underlying process via pipes.
use std::io::prelude::*;
use std::process::{Command, Stdio};
static PANGRAM: &'static str =
"the quick brown fox jumps over the lazy dog\n";
fn main() {
// `wc`コマンドを起動します。
let mut cmd = if cfg!(target_family = "windows") {
let mut cmd = Command::new("powershell");
cmd.arg("-Command").arg("$input | Measure-Object -Line -Word -Character");
cmd
} else {
Command::new("wc")
};
let process = match cmd
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn() {
Err(why) => panic!("couldn't spawn wc: {}", why),
Ok(process) => process,
};
// `wc`の`stdin`に文字列を書き込みます。
//
// `stdin`は`Option<ChildStdin>`型を持ちますが、今回は値を持っていることが
// 確かなので、いきなり`unwrap`してしまって構いません。
match process.stdin.unwrap().write_all(PANGRAM.as_bytes()) {
Err(why) => panic!("couldn't write to wc stdin: {}", why),
Ok(_) => println!("sent pangram to wc"),
}
// `stdin`は上のプロセスコールのあとには有効でないので、`drop`され、
// パイプはcloseされます。
//
// これは非常に重要です。というのもcloseしないと`wc`は
// 送った値の処理を開始しないからです。
// `stdout`フィールドも`Option<ChildStdout>`型なのでアンラップする必要があります
let mut s = String::new();
match process.stdout.unwrap().read_to_string(&mut s) {
Err(why) => panic!("couldn't read wc stdout: {}", why),
Ok(_) => print!("wc responded with:\n{}", s),
}
}
ドロップの延期
process::Child
が終了するのを待ちたい場合は、process::ExitStatus
を返すChild::wait
を呼び出さなくてはなりません。
use std::process::Command;
fn main() {
let mut child = Command::new("sleep").arg("5").spawn().unwrap();
let _result = child.wait().unwrap();
println!("reached end of main");
}
$ rustc wait.rs && ./wait
# `wait`は`sleep 5`コマンドが終了するまで5秒間実行され続けます。
reached end of main
ファイルシステムとのやり取り
std::fs
モジュールはファイルシステムとやり取りするための関数をいくつか持っています。
use std::fs;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::prelude::*;
#[cfg(target_family = "unix")]
use std::os::unix;
#[cfg(target_family = "windows")]
use std::os::windows;
use std::path::Path;
// `% cat path`のシンプルな実装
fn cat(path: &Path) -> io::Result<String> {
let mut f = File::open(path)?;
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
// `% echo s > path`の簡単な実装
fn echo(s: &str, path: &Path) -> io::Result<()> {
let mut f = File::create(path)?;
f.write_all(s.as_bytes())
}
// `% touch path`の簡単な実装(すでにファイルが存在しても無視します。)
fn touch(path: &Path) -> io::Result<()> {
match OpenOptions::new().create(true).write(true).open(path) {
Ok(_) => Ok(()),
Err(e) => Err(e),
}
}
fn main() {
println!("`mkdir a`");
// ディレクトリを作成します。返り値は`io::Result<()>`。
match fs::create_dir("a") {
Err(why) => println!("! {:?}", why.kind()),
Ok(_) => {},
}
println!("`echo hello > a/b.txt`");
// 上のmatchは`unwrap_or_else`をメソッドを用いて簡略化できます。
echo("hello", &Path::new("a/b.txt")).unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
println!("`mkdir -p a/c/d`");
// 再帰的にディレクトリを作成します。返り値は`io::Result<()>`。
fs::create_dir_all("a/c/d").unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
println!("`touch a/c/e.txt`");
touch(&Path::new("a/c/e.txt")).unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
println!("`ln -s ../b.txt a/c/b.txt`");
// シンボリックリンクを作成、返り値は`io::Result<()>`。
#[cfg(target_family = "unix")] {
unix::fs::symlink("../b.txt", "a/c/b.txt").unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
}
#[cfg(target_family = "windows")] {
windows::fs::symlink_file("../b.txt", "a/c/b.txt").unwrap_or_else(|why| {
println!("! {:?}", why.to_string());
});
}
println!("`cat a/c/b.txt`");
match cat(&Path::new("a/c/b.txt")) {
Err(why) => println!("! {:?}", why.kind()),
Ok(s) => println!("> {}", s),
}
println!("`ls a`");
// ディレクトリの内容を読み込みます。返り値は`io::Result<Vec<Path>>`。
match fs::read_dir("a") {
Err(why) => println!("! {:?}", why.kind()),
Ok(paths) => for path in paths {
println!("> {:?}", path.unwrap().path());
},
}
println!("`rm a/c/e.txt`");
// ファイルを削除。返り値は`io::Result<()>`。
fs::remove_file("a/c/e.txt").unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
println!("`rmdir a/c/d`");
// 空のディレクトリを削除。返り値は`io::Result<()>`。
fs::remove_dir("a/c/d").unwrap_or_else(|why| {
println!("! {:?}", why.kind());
});
}
以下が成功時に期待されるアウトプットです。
$ rustc fs.rs && ./fs
`mkdir a`
`echo hello > a/b.txt`
`mkdir -p a/c/d`
`touch a/c/e.txt`
`ln -s ../b.txt a/c/b.txt`
`cat a/c/b.txt`
> hello
`ls a`
> "a/b.txt"
> "a/c"
`rm a/c/e.txt`
`rmdir a/c/d`
最終的なa
ディレクトリの状態は以下です。
$ tree a
a
|-- b.txt
`-- c
`-- b.txt -> ../b.txt
1 directory, 2 files
An alternative way to define the function cat
is with ?
notation:
fn cat(path: &Path) -> io::Result<String> {
let mut f = File::open(path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
参照
引数処理
標準ライブラリ
コマンドライン引数はstd::env::args
を介して取得できます。これはそれぞれの引数を文字列として生成するイテレータを返します。
$ ./args 1 2 3
My path is ./args.
I got 3 arguments: ["1", "2", "3"].
クレート
Alternatively, there are numerous crates that can provide extra functionality when creating command-line applications. One of the more popular command line argument crates being clap
.
引数のパース
matchを用いて簡単な引数をパースできます。
If you named your program match_args.rs
and compile it like this rustc match_args.rs
, you can execute it as follows:
$ ./match_args Rust
This is not the answer.
$ ./match_args 42
This is the answer!
$ ./match_args do something
error: second argument not an integer
usage:
match_args <string>
Check whether given string is the answer.
match_args {increase|decrease} <integer>
Increase or decrease given integer by one.
$ ./match_args do 42
error: invalid command
usage:
match_args <string>
Check whether given string is the answer.
match_args {increase|decrease} <integer>
Increase or decrease given integer by one.
$ ./match_args increase 42
43
他言語関数インターフェイス
RustはCのライブラリを呼び出すために他言語関数インターフェイス(Foreign Function Interface, FFI)を持っています。他言語の関数を使用する際には、そのライブラリ名を#[link]
アトリビュートに渡し、更にそれでアノテーションされたextern
ブロック内で宣言する必要があります。
use std::fmt;
// このexternブロックはlibmライブラリをリンクします。
#[cfg(target_family = "windows")]
#[link(name = "msvcrt")]
extern {
// 他言語の関数宣言。
// この関数は単精度浮動小数の複素数型の平方根を計算するためのものです。
fn csqrtf(z: Complex) -> Complex;
fn ccosf(z: Complex) -> Complex;
}
#[cfg(target_family = "unix")]
#[link(name = "m")]
extern {
// 他言語の関数宣言。
// この関数は単精度浮動小数の複素数型の平方根を計算するためのものです。
fn csqrtf(z: Complex) -> Complex;
fn ccosf(z: Complex) -> Complex;
}
// 型安全にするためのラッパ
fn cos(z: Complex) -> Complex {
unsafe { ccosf(z) }
}
fn main() {
// z = -1 + 0i
let z = Complex { re: -1., im: 0. };
// calling a foreign function is an unsafe operation
let z_sqrt = unsafe { csqrtf(z) };
println!("the square root of {:?} is {:?}", z, z_sqrt);
// calling safe API wrapped around unsafe operation
println!("cos({:?}) = {:?}", z, cos(z));
}
// 単精度浮動小数の複素数型の最小限の実装
#[repr(C)]
#[derive(Clone, Copy)]
struct Complex {
re: f32,
im: f32,
}
impl fmt::Debug for Complex {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.im < 0. {
write!(f, "{}-{}i", self.re, -self.im)
} else {
write!(f, "{}+{}i", self.re, self.im)
}
}
}
テスト
Rustはとても正確性を配慮したプログラミング言語であり、ソフトウェアテストを書くためのサポートを言語自身が含んでいます。
テストには3つの種類があります。
またRustではテストのために追加の依存パッケージを指定することもできます。
参照
- The Book のテストの章
- API Guidelines on doc-testing
ユニットテスト
テストは、テスト以外のコードが想定通りに動いているかを確かめるRustの関数です。一般にテスト関数は、準備をしてからテストしたいコードを実行し、そしてその結果が期待したものであるか確認します。
大抵の場合ユニットテストは#[cfg(test)]
アトリビュートを付けたtests
モジュールに配置されます。テスト関数には#[test]
アトリビュートを付与します。
テスト関数内部でパニックするとテストは失敗となります。次のようなマクロが用意されています。
assert!(expression)
- 式を評価した結果がfalse
であればパニックします。assert_eq!(left, right)
とassert_ne!(left, right)
- 左右の式を評価した結果が、それぞれ等しくなること、ならないことをテストします。
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// 誤った加算をする関数がテストに通らないことを示します。
#[allow(dead_code)]
fn bad_add(a: i32, b: i32) -> i32 {
a - b
}
#[cfg(test)]
mod tests {
// 外部のスコープから(mod testsに)名前をインポートする便利なイディオム。
use super::*;
#[test]
fn test_add() {
assert_eq!(add(1, 2), 3);
}
#[test]
fn test_bad_add() {
// このアサーションはパニックして、テストは失敗します。
// プライベートな関数もテストすることができます。
assert_eq!(bad_add(1, 2), 3);
}
}
cargo test
でテストを実行できます。
$ cargo test
running 2 tests
test tests::test_bad_add ... FAILED
test tests::test_add ... ok
failures:
---- tests::test_bad_add stdout ----
thread 'tests::test_bad_add' panicked at 'assertion failed: `(left == right)`
left: `-1`,
right: `3`', src/lib.rs:21:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failures:
tests::test_bad_add
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
テストと?
ここまでに例示したユニットテストは返り値の型を持っていませんでしたが、Rust 2018ではユニットテストがResult<()>
を返し、内部で?
を使えるようになりました!これにより、ユニットテストをさらに簡潔に記述できます。
詳細はエディションガイドを参照してください。
パニックをテストする
ある条件下でパニックすべき関数をテストするには、#[should_panic]
アトリビュートを使います。このアトリビュートはパニックメッセージをオプションの引数expected =
で受け取れます。パニックの原因が複数あるときに、想定した原因でパニックが発生したことを確認できます。
pub fn divide_non_zero_result(a: u32, b: u32) -> u32 {
if b == 0 {
panic!("Divide-by-zero error");
} else if a < b {
panic!("Divide result is zero");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_divide() {
assert_eq!(divide_non_zero_result(10, 2), 5);
}
#[test]
#[should_panic]
fn test_any_panic() {
divide_non_zero_result(1, 0);
}
#[test]
#[should_panic(expected = "Divide result is zero")]
fn test_specific_panic() {
divide_non_zero_result(1, 10);
}
}
テストを実行すると、次の結果を得られます。
$ cargo test
running 3 tests
test tests::test_any_panic ... ok
test tests::test_divide ... ok
test tests::test_specific_panic ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests tmp-test-should-panic
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
実行するテストを指定する
cargo test
にテストの名前を与えると、そのテストだけが実行されます。
$ cargo test test_any_panic
running 1 test
test tests::test_any_panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out
Doc-tests tmp-test-should-panic
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
テスト名の一部を指定すると、それにマッチするすべてのテストが実行されます。
$ cargo test panic
running 2 tests
test tests::test_any_panic ... ok
test tests::test_specific_panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out
Doc-tests tmp-test-should-panic
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
テストを除外する
テストを実行から除外するには、#[ignore]
アトリビュートを使います。また、cargo test -- --ignored
で、除外したテストのみを実行できます。
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 2), 4);
}
#[test]
fn test_add_hundred() {
assert_eq!(add(100, 2), 102);
assert_eq!(add(2, 100), 102);
}
#[test]
#[ignore]
fn ignored_test() {
assert_eq!(add(0, 0), 0);
}
}
$ cargo test
running 3 tests
test tests::ignored_test ... ignored
test tests::test_add ... ok
test tests::test_add_hundred ... ok
test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out
Doc-tests tmp-ignore
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
$ cargo test -- --ignored
running 1 test
test tests::ignored_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests tmp-ignore
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
ドキュメンテーションテスト
Rustのプロジェクトでは、ソースコードに注釈する形でドキュメントを書くのが主流です。ドキュメンテーションコメントの記述はCommonMark Markdown specificationで行い、コードブロックも使えます。Rustは正確性を重視しているので、コードブロックもコンパイルされ、テストとして使われます。
/// 最初の行には関数の機能の短い要約を書きます。
///
/// 以降で詳細なドキュメンテーションを記述します。コードブロックは三重のバッククォートで始まり、
/// 暗黙的に`fn main()`と`extern crate <クレート名>`で囲われます。
/// `doccomments`クレートをテストしたいときには、次のように記述します。
///
/// ```
/// let result = doccomments::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
/// 一般的に、ドキュメンテーションコメントは
/// "Examples", "Panics", "Failures" という章から成ります。
///
/// 次の関数は除算を実行します。
///
/// # Examples
///
/// ```
/// let result = doccomments::div(10, 2);
/// assert_eq!(result, 5);
/// ```
///
/// # Panics
///
/// 第2引数がゼロであればパニックします。
///
/// ```rust,should_panic
/// // ゼロで除算するとパニックします
/// doccomments::div(10, 0);
/// ```
pub fn div(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("Divide-by-zero error");
}
a / b
}
ドキュメンテーションコメント中のコードブロックは、cargo test
コマンドで自動的にテストされます。
$ cargo test
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests doccomments
running 3 tests
test src/lib.rs - add (line 7) ... ok
test src/lib.rs - div (line 21) ... ok
test src/lib.rs - div (line 31) ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
ドキュメンテーションテストの目的
ドキュメンテーションテストの主な目的は、実行例を示すことであり、これは最も大切なガイドラインの一つにもなっています。これにより、ドキュメントの例を実際に動くコードとして使うことができます。しかしながら、main
が()
を返すために、?
を使うとコンパイルに失敗してしまいます。ドキュメンテーションでコードブロックの一部を隠す機能で、この問題に対処できます。つまり、fn try_main() -> Result<(), ErrorType>
を定義しておきながらそれを隠し、暗黙のmain
の内部でunwrap
するのです。複雑なので、例を見てみましょう。
/// ドキュメンテーションテストで、`try_main`を隠して使います。
///
/// ```
/// # // 行頭に `#` を置くと行が隠されるが、コンパイルには成功します。
/// # fn try_main() -> Result<(), String> { // ドキュメントの本体を囲う行
/// let res = doccomments::try_div(10, 2)?;
/// # Ok(()) // try_mainから値を返します
/// # }
/// # fn main() { // unwrap()を実行します。
/// # try_main().unwrap(); // try_mainを呼びunwrapすると、エラーの場合にパニックします。
/// # }
/// ```
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Divide-by-zero"))
} else {
Ok(a / b)
}
}
参照
- RFC505 ドキュメンテーションのスタイルについて
- API Guidelines ドキュメンテーションのガイドラインについて
統合テスト
ユニットテストは、独立したモジュールを一つずつテストするものであり、テストは小さく、プライベートなコードについてもテストすることができます。統合テストはクレートの外側にあるもので、他の外部のコードと同様に、パブリックなインタフェースだけを使います。統合テストの目的は、ライブラリのそれぞれのモジュールが連携して正しく動作するかどうかテストすることです。
Cargoは、src
ディレクトリと並んで配置されたtests
ディレクトリを統合テストとして扱います。
ファイルsrc/lib.rs
:
// `adder`という名前のクレートの内部で、次の関数を定義します。
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
テストを含むファイルtests/integration_test.rs
:
#[test]
fn test_add() {
assert_eq!(adder::add(3, 2), 5);
}
cargo test
コマンドでテストを実行します。
$ cargo test
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running target/debug/deps/integration_test-bcd60824f5fbfe19
running 1 test
test test_add ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
tests
ディレクトリにあるRustのソースファイルは別のクレートとしてコンパイルされます。統合テストの間でコードを共有するには、パブリックな関数をモジュールに入れて、それぞれのテストでインポートして利用する方法があります。
ファイルtests/common.rs
:
pub fn setup() {
// 必要なファイル・ディレクトリの作成やサーバの起動といった準備を行うコードを記述します。
}
テストを含むファイルtests/integration_test.rs
:
// 共通のモジュールをインポートします。
mod common;
#[test]
fn test_add() {
// 共通のコードを利用します。
common::setup();
assert_eq!(adder::add(3, 2), 5);
}
モジュールをtests/common.rs
に記述することも可能ですが、tests/common.rs
中のテストも自動的に実行されてしまうため非推奨です。
開発中の依存関係
テスト(あるいは例やベンチマーク)のためだけに、あるクレートに依存しなければならないことがあります。このような依存関係は、Cargo.toml
の[dev-dependencies]
セクションに追加します。このセクションに追加した依存関係は、このパッケージに依存するパッケージには適用されません。
そのようなクレートの例として、pretty_assertions
クレートが挙げられます。これは、標準のassert_eq!
とassert_ne!
マクロを拡張して、差分をカラフルに表示するものです。
ファイルCargo.toml
:
# 本節の内容に関係のない行は省略しています。
[dev-dependencies]
pretty_assertions = "1"
ファイルsrc/lib.rs
:
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq; // テストのためのクレートであり、
// テスト以外のコードには使えません。
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}
参照
Cargo 依存関係の指定について
安全でない操作
この章の内容を見る前に、公式ドキュメントから引用した次の文章をお読みください。「コードベース中の、アンセーフな操作をするコードの量は、可能な限り小さく無くてはならない。」この戒めを頭に叩き込んだ上で、さあはじめましょう!Rustにおいて、アンセーフなブロックはコンパイラのチェックをスルーするために使われます。具体的には以下の4つの主要なユースケースがあります。
- 生ポインタのデリファレンス
- 安全でない関数やメソッドの呼び出し(FFI経由の関数の呼び出しを含む (詳細は 本書のFFIに関する説明 を参照ください))
- 静的なミュータブル変数へのアクセスや変更
- 安全でないトレイトの実装
生ポインタ
生ポインタ*
と参照&T
はよく似た機能を持ちますが、後者は必ず有効なデータを指していることが借用チェッカーによって保証されているので、常に安全です。生ポインタのデリファレンスはアンセーフなブロックでしか実行できません。
安全でない関数呼び出し
関数は unsafe
として宣言できます。これはコンパイラの代わりにプログラマの責任で正しさを保証することを意味します。例として std::slice::from_raw_parts
があります。この関数は最初の要素へのポインタと長さを指定してスライスを作成します。
slice::from_raw_parts
は、次のふたつの仮定に基づいて処理します。ひとつは渡されたポインタが有効なメモリ位置を指していること、もうひとつはそのメモリに格納された値が正しい型であることです。この仮定を満たさない場合、プログラムの動作は不定となり、何が起こるかわかりません。
インラインアセンブリ
Rustはasm!
マクロによってインラインアセンブリをサポートしています。コンパイラが生成するアセンブリに、手書きのアセンブリを埋め込むことができます。一般的には必要ありませんが、要求されるパフォーマンスやタイミングを達成するために必要な場合があります。カーネルコードのような、低レベルなハードウェアの基本要素にアクセスする場合にも、この機能が必要でしょう。
注意: 以下の例はx86/x86-64アセンブリで書かれていますが、他のアーキテクチャもサポートされています。
インラインアセンブリは現在以下のアーキテクチャでサポートされています。
- x86とx86-64
- ARM
- AArch64
- RISC-V
基本的な使い方
最も単純な例から始めましょう。
これは、コンパイラが生成したアセンブリに、NOP(no operation)命令を挿入します。すべてのasm!
呼び出しは、unsafe
ブロックの中になければいけません。インラインアセンブリは任意の命令を挿入でき、不変条件を壊してしまうからです。挿入される命令は、文字列リテラルとしてasm!
マクロの第一引数に列挙されます。
入力と出力
何もしない命令を挿入しても面白くありません。実際にデータを操作してみましょう。
これはu64
型の変数x
に5
の値を書き込んでいます。命令を指定するために利用している文字列リテラルが、実はテンプレート文字列になっています。これはRustのフォーマット文字列と同じルールに従います。ですが、テンプレートに挿入される引数は、みなさんがよく知っているものとは少し違っています。まず、変数がインラインアセンブリの入力なのか出力なのかを指定する必要があります。上記の例では出力となっています。out
と書くことで出力であると宣言しています。また、アセンブリが変数をどの種類のレジスタに格納するかについても指定する必要があります。上の例では、reg
を指定して任意の汎用レジスタに格納しています。コンパイラはテンプレートに挿入する適切なレジスタを選び、インラインアセンブリの実行終了後、そのレジスタから変数を読みこみます。
入力を利用する別の例を見てみましょう。
この例では、変数i
の入力に5
を加え、その結果を変数o
に書き込んでいます。このアセンブリ特有のやり方として、はじめにi
の値を出力にコピーし、それから5
を加えています。
この例はいくつかのことを示します。
まず、asm!
では複数のテンプレート文字列を引数として利用できます。それぞれの文字列は、改行を挟んで結合されたのと同じように、独立したアセンブリコードとして扱われます。このおかげで、アセンブリコードを容易にフォーマットできます。
つぎに、入力はout
ではなくin
と書くことで宣言されています。
そして、他のフォーマット文字列と同じように引数を番号や名前で指定できます。インラインアセンブリのテンプレートでは、引数が2回以上利用されることが多いため、これは特に便利です。より複雑なインラインアセンブリを書く場合、この機能を使うのが推奨されます。可読性が向上し、引数の順序を変えることなく命令を並べ替えることができるからです。
上記の例をさらに改善して、mov
命令をやめることもできます。
inout
で入力でもあり出力でもある引数を指定しています。こうすることで、入力と出力を個別に指定する場合と違って、入出力が同じレジスタに割り当てられることが保証されます。
inout
のオペランドとして、入力と出力それぞれに異なる変数を指定することも可能です。
遅延出力オペランド
Rustコンパイラはオペランドの割り当てに保守的です。out
はいつでも書き込めるので、他の引数とは場所を共有できません。しかし、最適なパフォーマンスを保証するためには、できるだけ少ないレジスタを使うことが重要です。そうすることで、インラインアセンブリブロックの前後でレジスタを保存したり再読み込みしたりする必要がありません。これを達成するために、Rustはlateout
指定子を提供します。全ての入力が消費された後でのみ書き込まれる出力に利用できます。この指定子にはinlateout
という変化形もあります。
以下は、release
モードやその他の最適化された場合に、inlateout
を利用 できない 例です。
In unoptimized cases (e.g. Debug
mode), replacing inout(reg) a
with inlateout(reg) a
in the above example can continue to give the expected result. However, with release
mode or other optimized cases, using inlateout(reg) a
can instead lead to the final value a = 16
, causing the assertion to fail.
というのも、最適化されている場合、コンパイラはb
とc
が同じ値だと知っているので、b
とc
の入力に同じレジスタを割り当てる場合があります。もしinlateout
が使われていたら、a
とc
に同じレジスタが割り当てられ、最初のadd
命令によってc
の値が上書きされるでしょう。inout(reg) a
の使用によりa
に対する独立したレジスタ割り当てが保証されるのとは対照的です。
しかし、次の例では、全ての入力レジスタが読み込まれた後でのみ出力が変更されるので、inlateout
を利用できます。
このアセンブリコードは、a
とb
が同じレジスタに割り当てられても、正しく動作します。
明示的なレジスタオペランド
いくつかの命令では、オペランドが特定のレジスタにある必要があります。したがって、Rustのインラインアセンブリでは、より具体的な制約指定子を提供しています。reg
は一般的にどのアーキテクチャでも利用可能ですが、明示的レジスタはアーキテクチャに強く依存しています。たとえば、x86の汎用レジスタであるeax
、ebx
、ecx
、edx
、ebp
、esi
、edi
などは、その名前で指定できます。
この例では、out
命令を呼び出して、cmd
変数の中身を0x64
ポートに出力しています。out
命令はeax
とそのサブレジスタのみをオペランドとして受け取るため、eax
の制約指定子を使わなければなりません。
注意: 他のオペランドタイプと異なり、明示的なレジスタオペランドはテンプレート文字列中で利用できません。
{}
を使えないので、レジスタの名前を直接書く必要があります。また、オペランドのリストの中で他のオペランドタイプの一番最後に置かれなくてはなりません。
x86のmul
命令を使った次の例を考えてみましょう。
mul
命令を使って2つの64ビットの入力を128ビットの結果に出力しています。唯一の明示的なオペランドはレジスタで、変数a
から入力します。2つ目のオペランドは暗黙的であり、rax
レジスタである必要があります。変数b
からrax
レジスタに入力します。計算結果の下位64ビットはrax
レジスタに保存され、そこから変数lo
に出力されます。上位64ビットはrdx
レジスタに保存され、そこから変数hi
に出力されます。
クロバーレジスタ
多くの場合、インラインアセンブリは出力として必要のない状態を変更することがあります。これは普通、アセンブリでスクラッチレジスタを利用する必要があったり、私たちがこれ以上必要としていない状態を命令が変更したりするためです。この状態を一般的に"クロバー"(訳注:上書き)と呼びます。私たちはコンパイラにこのことを伝える必要があります。なぜならコンパイラは、インラインアセンブリブロックの前後で、この状態を保存して復元しなくてはならない可能性があるからです。
上の例では、cpuid
命令を使い、CPUベンタIDを読み込んでいます。この命令はeax
にサポートされている最大のcpuid
引数を書き込み、ebx
、edx
、ecx
の順にCPUベンダIDをASCIIコードとして書き込みます。
eax
は読み込まれることはありません。しかし、コンパイラがアセンブリ以前にこれらのレジスタにあった値を保存できるように、レジスタが変更されたことをコンパイラに伝える必要があります。そのために、変数名の代わりに_
を用いて出力を宣言し、出力の値が破棄されるということを示しています。
このコードはebx
がLLVMによって予約されたレジスタであるという制約を回避しています。LLVMは、自身がレジスタを完全にコントロールし、アセンブリブロックを抜ける前に元の状態を復元しなくてはならないと考えています。そのため、コンパイラがin(reg)
のような汎用レジスタクラスを満たすために使用する場合 を除いて ebx
を入力や出力として利用できません。つまり、予約されたレジスタを利用する場合に、reg
オペランドは危険なのです。入力と出力が同じレジスタを共有しているので、知らないうちに入力や出力を破壊してしまうかもしれません。
これを回避するために、rdi
を用いて出力の配列へのポインタを保管し、push
でebx
を保存し、アセンブリブロック内でebx
から読み込んで配列に書き込み、pop
でebx
を元の状態に戻しています。push
とpop
は完全な64ビットのrbx
レジスタを使って、レジスタ全体を確実に保存しています。32ビットの場合、push
とpop
においてebx
がかわりに利用されるでしょう。
アセンブリコード内部で利用するスクラッチレジスタを獲得するために、汎用レジスタクラスとともに使用することもできます。
シンボル・オペランドとABIクロバー
デフォルトでは、asm!
は、出力として指定されていないレジスタはアセンブリコードによって中身が維持される、と考えます。asm!
に渡されるclobber_abi
引数は、与えられた呼び出し規約のABIに従って、必要なクロバーオペランドを自動的に挿入するようコンパイラに伝えます。そのABIで完全に保存されていないレジスタは、クロバーとして扱われます。複数の clobber_abi
引数を指定すると、指定されたすべてのABIのクロバーが挿入されます。
レジスタテンプレート修飾子
テンプレート文字列に挿入されるレジスタの名前のフォーマット方法について、細かい制御が必要な場合があります。アーキテクチャのアセンブリ言語が、同じレジスタに別名を持っている場合です。典型的な例としては、レジスタの部分集合に対する"ビュー"があります(例:64ビットレジスタの下位32ビット)。
デフォルトでは、コンパイラは常に完全なレジスタサイズの名前を選択します(例:x86-64ではrax
、x86ではeax
、など)。
この挙動は、フォーマット文字列と同じように、テンプレート文字列のオペランドに修飾子を利用することで上書きできます。
この例では、reg_abcd
レジスタクラスを用いて、レジスタアロケータを4つのレガシーなx86レジスタ(ax
, bx
, cx
, dx
)に制限しています。このうち最初の2バイトは独立して指定できます。
レジスタアロケータがx
をax
レジスタに割り当てることにしたと仮定しましょう。h
修飾子はそのレジスタの上位バイトのレジスタ名を出力し、l
修飾子は下位バイトのレジスタ名を出力します。したがって、このアセンブリコードはmov ah, al
に展開され、値の下位バイトを上位バイトにコピーします。
より小さなデータ型(例:u16
)をオペランドに利用し、テンプレート修飾子を使い忘れた場合、コンパイラは警告を出力し、正しい修飾子を提案してくれます。
メモリアドレスオペランド
アセンブリ命令はオペランドがメモリアドレスやメモリロケーション経由で渡される必要なこともあります。そのときは手動で、ターゲットのアーキテクチャによって指定されたメモリアドレスのシンタックスを利用しなくてはなりません。例えば、Intelのアセンブリシンタックスを使うx86/x86_64の場合、入出力を[]
で囲んで、メモリオペランドであることを示さなくてはなりません。
ラベル
名前つきラベルの再利用は、ローカルかそうでないかに関わらず、アセンブラやリンカのエラーを引き起こしたり、変な挙動の原因となります。名前つきラベルの再利用は以下のようなケースがあります。
- 明示的再利用:同じラベルを1つの
asm!
ブロック中で、または複数のブロック中で2回以上利用する場合です。 - インライン化による暗黙の再利用:コンパイラは
asm!
ブロックの複数のコピーをインスタンス化する場合があります。例えば、asm!
ブロックを含む関数が複数箇所でインライン化される場合です。 - LTO(訳注:Link Time Optimizationの略)による暗黙の再利用:LTOは 他のクレート のコードを同じコード生成単位に配置するため、同じ名前のラベルを持ち込む場合があります。
そのため、インラインアセンブリコードの中では、GNUアセンブラの 数値型ローカルラベルのみ使用してください。アセンブリコード内でシンボルを定義すると、シンボル定義の重複により、アセンブラやリンカのエラーが発生する可能性があります。
さらに、x86でデフォルトのIntel構文を使用する場合、LLVMのバグによって、0
、11
、101010
といった0
と1
だけで構成されたラベルは、バイナリ値として解釈されてしまうため、使用してはいけません。options(att_syntax)
を使うと曖昧さを避けられますが、asm!
ブロック 全体 の構文に影響します。(options
については、後述のオプションを参照してください。)
このコードは、{0}
のレジスタの値を10から3にデクリメントし、2を加え、a
にその値を保存します。
この例は、以下のことを示しています。
- まず、ラベルとして同じ数字を複数回、同じインラインブロックで利用できます。
- つぎに、数字のラベルが参照として(例えば、命令のオペランドに)利用された場合、"b"("後方")や"f"("前方")の接尾辞が数字のラベルに追加されなくてはなりません。そうすることで、この数字の指定された方向の最も近いラベルを参照できます。
オプション
デフォルトでは、インラインアセンブリブロックは、カスタム呼び出し規約をもつ外部のFFI関数呼び出しと同じように扱われます:メモリを読み込んだり書き込んだり、観測可能な副作用を持っていたりするかもしれません。しかし、多くの場合、アセンブリコードが実際に何をするかという情報を多く与えて、より最適化できる方が望ましいでしょう。
先ほどのadd
命令の例を見てみましょう。
オプションはasm!
マクロの最後の任意引数として渡されます。ここでは3つのオプションを利用しました:
pure
は、アセンブリコードが観測可能な副作用を持っておらず、出力は入力のみに依存することを意味します。これにより、コンパイラオプティマイザはインラインアセンブリの呼び出し回数を減らしたり、インラインアセンブリを完全に削除したりできます。nomem
は、アセンブリコードがメモリの読み書きをしないことを意味します。デフォルトでは、インラインアセンブリはアクセス可能なメモリアドレス(例えばオペランドとして渡されたポインタや、グローバルなど)の読み書きを行うとコンパイラは仮定しています。nostack
は、アセンブリコードがスタックにデータをプッシュしないことを意味します。これにより、コンパイラはx86-64のスタックレッドゾーンなどの最適化を利用し、スタックポインタの調整を避けることができます。
これにより、コンパイラは、出力が全く必要とされていない純粋なasm!
ブロックを削除するなどして、asm!
を使ったコードをより最適化できます。
利用可能なオプションとその効果の一覧はリファレンスを参照してください。
互換性
The Rust language is evolving rapidly, and because of this certain compatibility issues can arise, despite efforts to ensure forwards-compatibility wherever possible.
生識別子
Rustは多くのプログラミング言語と同様に、「キーワード」の概念を持っています。これらの識別子は言語にとって何かしらの意味を持ちますので、変数名や関数名、その他の場所で使用することはできません。生識別子は、通常は許されない状況でキーワードを使用することを可能にします。これは、新しいキーワードを導入したRustと、古いエディションのRustを使用したライブラリが同じ名前の変数や関数を持つ場合に特に便利です。
例えば、2015年エディションのRustでコンパイルされたクレートfoo
がtry
という名前の関数をエクスポートしているとします。このキーワードは2018年エディションで新機能として予約されていますので、生識別子がなければ、この関数を名付ける方法がありません。
extern crate foo;
fn main() {
foo::try();
}
このエラーが出ます:
error: expected identifier, found keyword `try`
--> src/main.rs:4:4
|
4 | foo::try();
| ^^^ expected identifier, found keyword
これは生識別子を使って書くことができます:
extern crate foo;
fn main() {
foo::r#try();
}
周辺情報
この章では、プログラミングそれ自体に関係はないけれども、色々と人々の役に立つ機能やインフラについて説明していきます。例えば:
- ドキュメンテーション: Rust付属コマンド
rustdoc
を用いて、ライブラリのドキュメントを生成します。 - プレイグラウンド: あなたのドキュメンテーションにRust Playgroundを組み込めます。
ドキュメンテーション
Use cargo doc
to build documentation in target/doc
, cargo doc --open
will automatically open it in your web browser.
Use cargo test
to run all tests (including documentation tests), and cargo test --doc
to only run documentation tests.
These commands will appropriately invoke rustdoc
(and rustc
) as required.
ドキュメンテーションコメント
ドキュメンテーションコメントとはrustdoc
を使用した際にドキュメントにコンパイルされるコメントのことです。///
によって普通のコメントと区別され、ここではMarkdownを使用することができます。ドキュメンテーションコメントは大規模なプロジェクトの際に非常に有用です。
To run the tests, first build the code as a library, then tell rustdoc
where to find the library so it can link it into each doctest program:
$ rustc doc.rs --crate-type lib
$ rustdoc --test --extern doc="libdoc.rlib" doc.rs
Doc attributes
Below are a few examples of the most common #[doc]
attributes used with rustdoc
.
inline
Used to inline docs, instead of linking out to separate page.
#[doc(inline)]
pub use bar::Bar;
/// bar docs
pub mod bar {
/// the docs for Bar
pub struct Bar;
}
no_inline
Used to prevent linking out to separate page or anywhere.
// Example from libcore/prelude
#[doc(no_inline)]
pub use crate::mem::drop;
hidden
Using this tells rustdoc
not to include this in documentation:
For documentation, rustdoc
is widely used by the community. It's what is used to generate the std library docs.
参照
- The Rust Book: Making Useful Documentation Comments
- The rustdoc Book
- The Reference: Doc comments
- RFC 1574: API Documentation Conventions
- RFC 1946: Relative links to other items from doc comments (intra-rustdoc links)
- Is there any documentation style guide for comments? (reddit)
プレイグラウンド
Rust Playgroundでは、RustのコードをWebのインターフェースを通じて実験できます。
mdbook
と組み合わせる
mdbook
では、コード例を実行・編集可能にできます。
これにより、読者はあなたのコード例を実行するだけでなく、変更することもできます。editable
という単語をカンマで区切って、あなたのコードブロックに追加するのがキーです。
```rust,editable
//...place your code here
```
加えて、mdbook
がビルドやテストを実行するときに、あなたのコードをスキップさせたい場合は、ignore
を追加できます。
```rust,editable,ignore
//...place your code here
```
ドキュメントと組み合わせる
Rustの公式ドキュメントには、「実行(Run)」ボタンがある場合があります。クリックすると、新しいタブでRust Playgroundが開き、コード例が表示されます。この機能は、#[doc]アトリビュートのhtml_playground_url
の値で有効化できます。
#![doc(html_playground_url = "https://play.rust-lang.org/")]
//! ```
//! println!("Hello World");
//! ```