データ型
この記事では Rust のデータ型について説明します。これらはおおよそ C++ の
クラス、構造体、列挙型に相当します。Rust と C++ の違いの 1 つは、Rust では
データと振る舞いが C++(または Java、その他の
オブジェクト指向言語)よりもはるかに厳密に分離されていることです。振る舞いは関数によって定義され、それらは
トレイトや impl(実装)で定義できますが、トレイトはデータを含むことができず、
その点では Java のインターフェイスに似ています。トレイトと impl については後の記事で扱います。この記事はデータについてのものです。
構造体
Rust の構造体は、C の構造体やメソッドを持たない C++ の構造体に似ています。単に 名前付きフィールドのリストです。構文は例で見るのが最もわかりやすいでしょう。
#![allow(unused)] fn main() { struct S { field1: i32, field2: SomeOtherStruct } }
ここでは、2 つのフィールドを持つ S という構造体を定義しています。フィールドはカンマで
区切られます。好みに応じて、最後のフィールドの末尾にもカンマを付けることができます。
構造体は型を導入します。この例では、S を型として使用できます。
SomeOtherStruct は別の構造体(この例では型として使用されている)であると仮定しており、
(C++ と同様に)値として含まれます。つまり、メモリ内の別の構造体オブジェクトへのポインタは
存在しません。
構造体のフィールドには、. 演算子とフィールド名を使ってアクセスします。構造体を使用する例を示します。
#![allow(unused)] fn main() { fn foo(s1: S, s2: &S) { let f = s1.field1; if f == s2.field1 { println!("field1 matches!"); } } }
ここで s1 は値渡しされた構造体オブジェクトで、s2 は参照渡しされた構造体オブジェクトです。
メソッド呼び出しの場合と同様に、どちらのフィールドにアクセスする場合も同じ . を使用し、
-> は必要ありません。
構造体は構造体リテラルを使って初期化します。構造体リテラルは構造体名と 各フィールドの値で構成されます。たとえば、
#![allow(unused)] fn main() { fn foo(sos: SomeOtherStruct) { let x = S { field1: 45, field2: sos }; // 構造体リテラルで x を初期化する println!("x.field1 = {}", x.field1); } }
構造体を再帰的にすることはできません。つまり、定義とフィールド型に関わる構造体名の循環を
持つことはできません。これは構造体の値セマンティクスによるものです。
したがって、たとえば struct R { r: Option<R> } は不正であり、
コンパイラエラーになります(Option については後述します)。このような構造が必要な場合は、
何らかのポインタを使用するべきです。ポインタを使った循環は許可されています。
#![allow(unused)] fn main() { struct R { r: Option<Box<R>> } }
上の構造体に Option がなければ、その構造体をインスタンス化する方法がなく、
Rust はエラーを通知します。
フィールドを持たない構造体では、定義でもリテラルでの使用でも波括弧を使いません。 ただし、定義には終端のセミコロンが必要です。おそらくこれは単に パースを容易にするためです。
#![allow(unused)] fn main() { struct Empty; fn foo() { let e = Empty; } }
タプル
タプルは、匿名で異種混在のデータ列です。型としては、
括弧内に型の列として宣言されます。名前がないため、構造によって識別されます。
たとえば、型 (i32, i32) は整数のペアであり、(i32, f32, S) は 3 要素の組です。
タプル値はタプル型の宣言と同じ方法で初期化されますが、
構成要素には型ではなく値を使います。たとえば (4, 5) です。例を示します。
#![allow(unused)] fn main() { // foo は構造体を受け取り、タプルを返す fn foo(x: SomeOtherStruct) -> (i32, f32, S) { (23, 45.82, S { field1: 54, field2: x }) } }
タプルは let 式による分配束縛を使って利用できます。たとえば、
#![allow(unused)] fn main() { fn bar(x: (i32, i32)) { let (a, b) = x; println!("x was ({}, {})", a, b); } }
分配束縛については次回さらに説明します。
タプル構造体
タプル構造体は名前付きタプル、あるいは別の言い方をすれば、名前のないフィールドを持つ構造体です。
これらは struct キーワード、括弧内の型のリスト、
そしてセミコロンを使って宣言されます。このような宣言は、その名前を型として導入します。
フィールドには名前ではなく、(タプルのように)分配束縛によってアクセスする必要があります。
タプル構造体はあまり一般的ではありません。
#![allow(unused)] fn main() { struct IntPoint (i32, i32); fn foo(x: IntPoint) { let IntPoint(a, b) = x; // タプルを分配束縛するには、その名前が必要であることに注意 // 構造体を分配束縛する。 println!("x was ({}, {})", a, b); } }
列挙型
列挙型は C++ の列挙型や共用体のような型で、複数の値を取り得る型です。 最も単純な種類の列挙型は、C++ の列挙型とまったく同じようなものです。
#![allow(unused)] fn main() { enum E1 { Var1, Var2, Var3 } fn foo() { let x: E1 = Var2; match x { Var2 => println!("var2"), _ => {} } } }
しかし、Rust の列挙型はそれよりもはるかに強力です。各バリアントは データを含むことができます。タプルと同様に、これらは型のリストによって定義されます。この場合、 C++ の列挙型というより共用体に近いものです。Rust の列挙型は、タグなし共用体(C++ におけるもの)ではなくタグ付き共用体です。 つまり、実行時に列挙型のあるバリアントを別のバリアントと取り違えることはありません1。例を示します。
#![allow(unused)] fn main() { enum Expr { Add(i32, i32), Or(bool, bool), Lit(i32) } fn foo() { let x = Or(true, false); // x の型は Expr } }
オブジェクト指向のポリモーフィズムの単純なケースの多くは、Rust では 列挙型を使う方がうまく扱えます。
列挙型を使うには、通常 match 式を使用します。これらは C++ の switch 文に似ていることを思い出してください。 これらや、データを分配束縛する他の方法については次回さらに詳しく説明します。例を示します。
#![allow(unused)] fn main() { fn bar(e: Expr) { match e { Add(x, y) => println!("An `Add` variant: {} + {}", x, y), Or(..) => println!("An `Or` variant"), _ => println!("Something else (in this case, a `Lit`)"), } } }
match 式の各アームは Expr のバリアントにマッチします。すべてのバリアントを
網羅しなければなりません。最後のケース(_)は残りのすべてのバリアントを扱いますが、この
例では Lit だけです。バリアント内の任意のデータは変数に束縛できます。
Add アームでは、Add 内の 2 つの i32 を x と y に束縛しています。
データに関心がない場合は、Or で行っているように .. を使って任意のデータにマッチできます。
Option
Rust で特に一般的な列挙型の 1 つが Option です。これには Some と
None という 2 つのバリアントがあります。None はデータを持たず、Some は型 T の単一のフィールドを持ちます
(Option はジェネリックな列挙型で、これについては後で扱いますが、
一般的な考え方は C++ から理解できるはずです)。Option は、値が
存在するかもしれないし、存在しないかもしれないことを示すために使われます。C++ で null ポインタを使う任意の場所2、
つまり何らかの形で未定義、未初期化、または false である値を示すために使う場所では、
Rust ではおそらく Option を使うべきです。Option を使う方が安全なのは、
使用前に必ず確認しなければならないためです。null ポインタをデリファレンスするのと同等のことを行う方法はありません。
また、Option はより汎用的であり、ポインタだけでなく値にも使用できます。例を示します。
#![allow(unused)] fn main() { use std::rc::Rc; struct Node { parent: Option<Rc<Node>>, value: i32 } fn is_root(node: Node) -> bool { match node.parent { Some(_) => false, None => true } } }
ここで、parent フィールドは None か、Rc<Node> を含む Some のどちらかです。
この例では、そのペイロードを実際には使用していませんが、実際の場面では
通常は使用するでしょう。
Option には便利なメソッドもあるため、is_root の本体を
node.parent.is_none() または !node.parent.is_some() と書くこともできます。
継承された可変性と Cell/RefCell
Rust のローカル変数はデフォルトではイミュータブルであり、mut を使ってミュータブルとしてマークできます。構造体や列挙型のフィールドをミュータブルとしてマークすることはありません。それらのミュータビリティは継承されます。これは、構造体オブジェクト内のフィールドがミュータブルかイミュータブルかは、そのオブジェクト自体がミュータブルかイミュータブルかによって決まることを意味します。例:
struct S1 { field1: i32, field2: S2 } struct S2 { field: i32 } fn main() { let s = S1 { field1: 45, field2: S2 { field: 23 } }; // s は深くイミュータブルであり、以下の変更は禁止される // s.field1 = 46; // s.field2.field = 24; let mut s = S1 { field1: 45, field2: S2 { field: 23 } }; // s はミュータブルであり、これらは OK s.field1 = 46; s.field2.field = 24; }
Rust における継承されたミュータビリティは、参照で止まります。これは C++ と似ており、const オブジェクトからのポインタを介して非 const オブジェクトを変更できます。参照フィールドをミュータブルにしたい場合は、そのフィールド型に &mut を使う必要があります:
struct S1 { f: i32 } struct S2<'a> { f: &'a mut S1 // ミュータブルな参照フィールド } struct S3<'a> { f: &'a S1 // イミュータブルな参照フィールド } fn main() { let mut s1 = S1{f:56}; let s2 = S2 { f: &mut s1}; s2.f.f = 45; // s2 がイミュータブルであっても合法 // s2.f = &mut s1; // 不正 - s2 はミュータブルではない let s1 = S1{f:56}; let mut s3 = S3 { f: &s1}; s3.f = &s1; // 合法 - s3 はミュータブル // s3.f.f = 45; // 不正 - s3.f はイミュータブル }
(S2 と S3 の 'a パラメータはライフタイムパラメータです。これについてはすぐに扱います)。
オブジェクトは論理的にはイミュータブルであっても、内部的にミュータブルである必要がある部分を持つことがあります。さまざまな種類のキャッシュや参照カウントを考えてみてください(参照カウントの変更の影響はデストラクタを通じて観測できるため、真の論理的イミュータビリティは得られません)。C++ では、オブジェクトが const であってもそのような変更を許可するために mutable キーワードを使います。Rust には Cell 構造体と RefCell 構造体があります。これらにより、イミュータブルなオブジェクトの一部を変更できます。これは便利ですが、Rust でイミュータブルなオブジェクトを見たとき、その一部が実際にはミュータブルである可能性があることを認識しておく必要がある、ということでもあります。
RefCell と Cell は、Rust の変更とエイリアス可能性に関する厳格なルールを回避できるようにします。これらが安全に使えるのは、コンパイラがそれらの不変条件が静的に成り立つことを保証できない場合でも、Rust の不変条件が動的に尊重されることを保証するためです。Cell と RefCell はどちらもシングルスレッドのオブジェクトです。
コピーセマンティクスを持つ型(ほぼプリミティブ型だけ)には Cell を使います。Cell には、格納された値を変更するための get メソッドと set メソッド、および値でセルを初期化するための new メソッドがあります。Cell は非常に単純なオブジェクトです。コピーセマンティクスを持つオブジェクトは(Rust では)他の場所への参照を保持できず、スレッド間で共有することもできないため、問題が起きる余地があまりなく、賢いことをする必要がありません。
ムーブセマンティクスを持つ型には RefCell を使います。これは Rust のほぼすべてを意味し、構造体オブジェクトが一般的な例です。RefCell も new を使って作成され、set メソッドを持ちます。RefCell 内の値を取得するには、borrow メソッド(borrow, borrow_mut, try_borrow, try_borrow_mut)を使って借用しなければなりません。これらは RefCell 内のオブジェクトへの借用参照を返します。これらのメソッドは静的な借用と同じルールに従います。つまり、ミュータブルな借用は 1 つだけしか持てず、同時にミュータブルにもイミュータブルにも借用することはできません。ただし、コンパイルエラーではなく、実行時の失敗になります。try_ バリアントは Option を返します。値を借用できる場合は Some(val) を、できない場合は None を得ます。値が借用されている場合、set の呼び出しも失敗します。
RefCell への参照カウント付きポインタを使った例を示します(一般的なユースケースです):
use std::rc::Rc; use std::cell::RefCell; struct S { field: i32 } fn foo(x: Rc<RefCell<S>>) { { let s = x.borrow(); println!("the field, twice {} {}", s.field, x.borrow().field); // let s = x.borrow_mut(); // エラー - x の内容はすでに借用している } let mut s = x.borrow_mut(); // OK、以前の借用はスコープ外 s.field = 45; // println!("The field {}", x.borrow().field); // エラー - ミュータブル借用とイミュータブル借用は同時にできない println!("The field {}", s.field); } fn main() { let s = S{field:12}; let x: Rc<RefCell<S>> = Rc::new(RefCell::new(s)); foo(x.clone()); println!("The field {}", x.borrow().field); }
Cell/RefCell を使う場合は、できるだけ小さいオブジェクトに配置するようにするべきです。つまり、構造体全体ではなく、構造体のいくつかのフィールドに配置することを好んでください。これらをシングルスレッドのロックのように考えてください。より細粒度のロックのほうが、ロックの衝突を避けられる可能性が高いため、より良いです。