高度な型
Rustの型システムには、この本で触れたけれども、まだ議論していない機能があります。ニュータイプが何故型として有用なのかを調査するため、
一般化してニュータイプを議論することから始めます。そして、型エイリアスに移ります。ニュータイプに類似しているけれども、
多少異なる意味を持つ機能です。また、!
型と動的サイズ決定型も議論します。
注釈: 次の節は、前節「外部の型に外部のトレイトを実装するニュータイプパターン」を読了済みであることを前提にしています。
型安全性と抽象化を求めてニュータイプパターンを使用する
ここまでに議論した以上の作業についてもニュータイプパターンは有用で、静的に絶対に値を混同しないことを強制したり、
値の単位を示すことを含みます。ニュータイプを使用して単位を示す例をリスト19-23で見かけました:
Millimeters
とMeters
構造体は、u32
値をニュータイプにラップしていたことを思い出してください。
型Millimeters
を引数にする関数を書いたら、誤ってその関数を型Meters
や普通のu32
で呼び出そうとするプログラムはコンパイルできないでしょう。
型の実装の詳細を抽象化する際にニュータイプパターンを使用するでしょう: 例えば、新しい型を直接使用して、 利用可能な機能を制限したら、非公開の内部の型のAPIとは異なる公開APIを新しい型は露出できます。
ニュータイプはまた、内部の実装を隠匿することもできます。例を挙げれば、People
型を提供して、
人のIDと名前を紐づけて格納するHashMap<i32, String>
をラップすることができるでしょう。
People
を使用するコードは、名前の文字列をPeople
コレクションに追加するメソッドなど、
提供している公開APIとだけ相互作用するでしょう; そのコードは、内部でi32
IDを名前に代入していることを知る必要はないでしょう。
ニュータイプパターンは、カプセル化を実現して実装の詳細を隠匿する軽い方法であり、
実装の詳細を隠匿することは、第17章の「カプセル化は実装詳細を隠蔽する」節で議論しましたね。
型エイリアスで型同義語を生成する
ニュータイプパターンに付随して、Rustでは、既存の型に別の名前を与える型エイリアス(type alias: 型別名)を宣言する能力が提供されています。
このために、type
キーワードを使用します。例えば、以下のようにi32
に対してKilometers
というエイリアスを作れます。
#![allow(unused)] fn main() { type Kilometers = i32; }
これで、別名のKilometers
はi32
と同義語になりました; リスト19-23で生成したMillimeters
とMeters
とは異なり、
Kilometers
は個別の新しい型ではありません。型Kilometers
の値は、型i32
の値と同等に扱われます。
#![allow(unused)] fn main() { type Kilometers = i32; let x: i32 = 5; let y: Kilometers = 5; println!("x + y = {}", x + y); }
Kilometers
とi32
が同じ型なので、両方の型の値を足し合わせたり、Kilometers
の値をi32
引数を取る関数に渡せたりします。
ですが、この方策を使用すると、先ほど議論したニュータイプパターンで得られる型チェックの利便性は得られません。
型同義語の主なユースケースは、繰り返しを減らすことです。例えば、こんな感じの長い型があるかもしれません:
Box<Fn() + Send + 'static>
この長ったらしい型を関数シグニチャや型注釈としてコードのあちこちで記述するのは、面倒で間違いも起きやすいです。 リスト19-32のそのようなコードで溢れかえったプロジェクトがあることを想像してください。
#![allow(unused)] fn main() { let f: Box<Fn() + Send + 'static> = Box::new(|| println!("hi")); fn takes_long_type(f: Box<Fn() + Send + 'static>) { // --snip-- } fn returns_long_type() -> Box<Fn() + Send + 'static> { // --snip-- Box::new(|| ()) } }
型エイリアスは、繰り返しを減らすことでこのコードをより管理しやすくしてくれます。リスト19-33で、
冗長な型にThunk
(注釈
: 塊)を導入し、その型の使用全部をより短い別名のThunk
で置き換えることができます。
#![allow(unused)] fn main() { type Thunk = Box<Fn() + Send + 'static>; let f: Thunk = Box::new(|| println!("hi")); fn takes_long_type(f: Thunk) { // --snip-- } fn returns_long_type() -> Thunk { // --snip-- Box::new(|| ()) } }
このコードの方が遥かに読み書きしやすいです!型エイリアスに意味のある名前を選択すると、 意図を伝えるのにも役に立つことがあります(thunkは後ほど評価されるコードのための単語なので、 格納されるクロージャーには適切な名前です)。
型エイリアスは、繰り返しを減らすためにResult<T, E>
型ともよく使用されます。標準ライブラリのstd::io
モジュールを考えてください。
I/O処理はしばしば、Result<T, E>
を返して処理がうまく動かなかった時を扱います。このライブラリには、
全ての可能性のあるI/Oエラーを表すstd::io::Error
構造体があります。std::io
の関数の多くは、
Write
トレイトの以下の関数のようにE
がstd::io::Error
のResult<T, E>
を返すでしょう:
#![allow(unused)] fn main() { use std::io::Error; use std::fmt; pub trait Write { fn write(&mut self, buf: &[u8]) -> Result<usize, Error>; fn flush(&mut self) -> Result<(), Error>; fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>; fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>; } }
Result<..., Error>
が何度も繰り返されてます。そんな状態なので、std::io
にはこんな類のエイリアス宣言があります:
type Result<T> = Result<T, std::io::Error>;
この宣言はstd::io
モジュール内にあるので、フルパスエイリアスのstd::io::Result<T>
を使用できます。
つまり、E
がstd::io::Error
で埋められたResult<T, E>
です。その結果、Write
トレイトの関数シグニチャは、
以下のような見た目になります:
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: Arguments) -> Result<()>;
}
型エイリアスは、2通りの方法で役に立っています: コードを書きやすくすることとstd::io
を通して首尾一貫したインターフェイスを与えてくれることです。
別名なので、ただのResult<T, E>
であり、要するにResult<T, E>
に対して動くメソッドはなんでも使えるし、
?
演算子のような特殊な記法も使えます。
never型は絶対に返らない
Rustには、!
という名前の特別な型があります。それは型理論の専門用語では Empty型 と呼ばれ値なしを表します。私たちは、
関数が値を返すことが決して (never) ない時に戻り値の型を記す場所に使われるので、never type(訳注
: 日本語にはできないので、never型と呼ぶしかないか)と呼ぶのが好きです。
こちらが例です:
fn bar() -> ! {
// --snip--
}
このコードは、「関数bar
はneverを返す」と解読します。neverを返す関数は、発散する関数(diverging function)と呼ばれます。
型!
の値は生成できないので、bar
からリターンする(呼び出し元に制御を戻す)ことは決してできません。
ですが、値を絶対に生成できない型をどう使用するのでしょうか?リスト2-5のコードを思い出してください; リスト19-34に一部を再掲します。
#![allow(unused)] fn main() { let guess = "3"; loop { let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; break; } }
この時点では、このコードの詳細の一部を飛ばしました。第6章の「match
制御フロー演算子」節で、
match
アームは全て同じ型を返さなければならないと議論しました。従って、例えば以下のコードは動きません:
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "hello",
}
このコードのguess
は整数かつ文字列にならなければならないでしょうが、Rustでは、guess
は1つの型にしかならないことを要求されます。
では、continue
は何を返すのでしょうか?どうやってリスト19-34で1つのアームからはu32
を返し、別のアームでは、
continue
で終わっていたのでしょうか?
もうお気付きかもしれませんが、continue
は!
値です。つまり、コンパイラがguess
の型を計算する時、
両方のmatchアームを見て、前者はu32
の値、後者は!
値となります。!
は絶対に値を持ち得ないので、
コンパイラは、guess
の型はu32
と決定するのです。
この振る舞いを解説する公式の方法は、型!
の式は、他のどんな型にも型強制され得るということです。
このmatch
アームをcontinue
で終えることができます。何故なら、continue
は値を返さないからです;
その代わりに制御をループの冒頭に戻すので、Err
の場合、guess
には絶対に値を代入しないのです。
never型は、panic!
マクロとも有用です。Option<T>
値に対して呼び出して、値かパニックを生成したunwrap
関数を覚えていますか?
こちらがその定義です:
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
このコードにおいて、リスト19-34のmatch
と同じことが起きています: コンパイラは、val
の型はT
で、
panic!
の型は!
なので、match
式全体の結果はT
と確認します。panic!
は値を生成しないので、
このコードは動きます。つまり、プログラムを終了するのです。None
の場合、unwrap
から値は返さないので、
このコードは合法なのです。
型が!
の最後の式は、loop
です:
// 永遠に
print!("forever ");
loop {
// さらに永遠に
print!("and ever ");
}
ここで、ループは終わりませんので、!
が式の値です。ところが、break
を含んでいたら、これは真実にはならないでしょう。
break
に到達した際にループが終了してしまうからです。
動的サイズ決定型とSized
トレイト
コンパイラが特定の型の値1つにどれくらいのスペースのメモリを確保するのかなどの特定の詳細を知る必要があるために、 Rustの型システムには混乱を招きやすい細かな仕様があります: 動的サイズ決定型の概念です。時としてDSTやサイズなし型とも称され、 これらの型により、実行時にしかサイズを知ることのできない値を使用するコードを書かせてくれます。
str
と呼ばれる動的サイズ決定型の詳細を深掘りしましょう。本を通して使用してきましたね。
そうです。&str
ではなく、str
は単独でDSTなのです。実行時までは文字列の長さを知ることができず、
これは、型str
の変数を生成したり、型str
を引数に取ることはできないことを意味します。
動かない以下のコードを考えてください:
// こんにちは
let s1: str = "Hello there!";
// 調子はどう?
let s2: str = "How's it going?";
コンパイラは、特定の型のどんな値に対しても確保するメモリ量を知る必要があり、ある型の値は全て同じ量のメモリを使用しなければなりません。
Rustでこのコードを書くことが許容されたら、これら2つのstr
値は、同じ量のスペースを消費する必要があったでしょう。
ですが、長さが異なります: s1
は、12バイトのストレージが必要で、s2
は15バイトです。このため、
動的サイズ決定型を保持する変数を生成することはできないのです。
では、どうすればいいのでしょうか?この場合、もう答えはご存知です: s1
とs2
の型をstr
ではなく、
&str
にすればいいのです。第4章の「文字列スライス」節でスライスデータ構造は、
開始地点とスライスの長さを格納していると述べたことを思い出してください。
従って、&T
は、T
がどこにあるかのメモリアドレスを格納する単独の値だけれども、&str
は2つの値なのです:
str
のアドレスとその長さです。そのため、コンパイル時に&str
のサイズを知ることができます:
usize
の長さの2倍です。要するに、参照している文字列の長さによらず、常に&str
のサイズがわかります。
通常、このようにしてRustでは動的サイズ決定型が使用されます: 動的情報のサイズを格納する追加のちょっとしたメタデータがあるのです。
動的サイズ決定型の黄金規則は、常に動的サイズ決定型の値をなんらかの種類のポインタの背後に配置しなければならないということです。
str
を全ての種類のポインタと組み合わせられます: 例を挙げれば、Box<str>
やRc<str>
などです。
実際、これまでに見かけましたが、異なる動的サイズ決定型でした: トレイトです。全てのトレイトは、
トレイト名を使用して参照できる動的サイズ決定型です。第17章の「トレイトオブジェクトで異なる型の値を許容する」節で、
トレイトをトレイトオブジェクトとして使用するには、&Trait
やBox<Trait>
(Rc<Trait>
も動くでしょう)など、
ポインタの背後に配置しなければならないことに触れました。
DSTを扱うために、RustにはSized
トレイトと呼ばれる特定のトレイトがあり、型のサイズがコンパイル時にわかるかどうかを決定します。
このトレイトは、コンパイル時にサイズの判明する全てのものに自動的に実装されます。加えて、
コンパイラは暗黙的に全てのジェネリックな関数にSized
の境界を追加します。つまり、こんな感じのジェネリック関数定義は:
fn generic<T>(t: T) {
// --snip--
}
実際にはこう書いたかのように扱われます:
fn generic<T: Sized>(t: T) {
// --snip--
}
既定では、ジェネリック関数はコンパイル時に判明するサイズがある型に対してのみ動きます。 ですが、以下の特別な記法を用いてこの制限を緩めることができます:
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
?Sized
のトレイト境界は、Sized
のトレイト境界の逆になります: これを「T
はSized
かもしれないし、違うかもしれない」と解読するでしょう。
この記法は、Sized
にのみ利用可能で、他のトレイトにはありません。
また、t
引数の型をT
から&T
に切り替えたことにも注目してください。型はSized
でない可能性があるので、
なんらかのポインタの背後に使用する必要があるのです。今回は、参照を選択しました。
次は、関数とクロージャについて語ります!