データ型
この投稿では、Rustのデータ型について論じていこう。データ型は、ほぼC++のクラス、構造体、enumに等しい。Rustの方の違いは、データと振る舞いがC++(やJava、さらには他のオブジェクト指向言語)のものよりはるかに厳密に分けられていることにある。
振る舞いは関数によって定義され、関数はtraitとimpl(implementationのこと)両方で定義できるが、traitはデータを含むことができない。その点においてtraitは、Javaのインターフェースに近い。
traitとimplに関しては、また別記事を立てて解説する。今回は、データについてのみ述べる。
構造体
Rustの構造体はメソッドのないCやC++の構造体に似て、単純な名前付きフィールドのリストである。この見た目は、例をあげればよくわかるだろう。
struct S {
field1: int,
field2: SomeOtherStruct
}
ここで、2つフィールドのあるSという構造体を定義している。フィールドはカンマ区切りで記述する。好みによっては、最後のフィールドもカンマで区切らせることができる。構造体は型を導入する。上記の例では、識別子Sを型として使用することができる。SomeOtherStructは別の構造体という想定(上述の例では、型になっている)であり、(C++同様に)値としてフィールド化している。つまり、メモリ上にある構造体オブジェクトを指すポインタではないということ。
構造体のフィールドは、.演算子とフィールド名でアクセスする。以下、構造体の使用例。
fn foo(s1: S, s2: &S) {
let f = s1.field1;
if f == s2.field1 {
println!("field1 matches!")
}
}
引数s1は値渡しの構造体オブジェクト、引数s2は参照渡しの構造体オブジェクトである。メソッド呼び出し同様、.演算子だけで値渡しのオブジェクトだろうが、参照渡しのオブジェクトだろうが、フィールドにアクセスできる。->演算子を使用する必要はない。構造体は、構造体初期化式で作成する。構造体初期化式は、構造体名とフィールド値の組み合わせである。
fn foo(sos: SomeOtherStruct) {
let x = S { field1: 45, field2: sos }; // xを構造体初期化式で作成
println!("x.field1 = {}", x.field1);
}
構造体は、循環参照できない。つまり、宣言やフィールドの型名に繰り返して構造体名を使うことはできないということだ。これは、構造体の値の取り扱い方のせいである。故に例えば、struct R { r: Option<R> }は不正となり、コンパイルエラーが発生する(Option型について詳しくは後述)。そのような構造が必要ならば、何かしらポインタを使用しなければならない。ポインタならば、循環参照が許されている。
struct R {
r: Option<Box<R>>
}
上記の構造体にOption型を含めていないと、これをインスタンス化する手段がなくなって、コンパイラがエラーを吐くことになる。フィールドを持たない構造体は、定義においても、初期化においても、かっこは使用しない。ただ、宣言ではセミコロンで区切る必要がある。まあ、構文解析上の問題だけどね。
struct Empty;
fn foo() {
let e = Empty;
}
タプル
タプルは、名前のない、混種の連続データである。型としては、かっこに型名を並べて定義する。特に名前がないため、タプルは構造で識別される。例として、(int, int)は一組みのint、(int, f32, S)は3要素タプルである。タプルオブジェクトは、宣言と同じような書き方をするが、型の代わりに代入する値を入れ込むことで(4, 5)のように生成される。
// foo関数は、構造体を引数にとってタプルを返す
fn foo(x: SomeOtherStruct) -> (i32, f32, S) {
(23, 45.82, S { field1: 54, field2: x })
}
タプルは、let式で分解することで使うことができる。fn bar(x: (int, int)) {
let (a, b) = x;
println!("x was ({}, {})", a, b);
}
分解(非構造化)については、次回詳しく話す。タプル構造体
タプル構造体は、名前付きタプルである。また、逆の言い方をすれば、名前なしフィールドを持つ構造体とも表現できる。宣言は、structキーワード、かっこ内に型を記述し、セミコロンで閉じて行い、この名前が型として定義される。タプル構造体のフィールドは、名前よりも(タプル同様に)分解でアクセスする必要がある。
タプル構造体は、あまり使われない。
struct IntPoint (int, int);
fn foo(x: IntPoint) {
let IntPoint(a, b) = x; // 分解するのにタプル構造体の名前が必要なことに注目
println!("x was {(}, {})", a, b);
}
Enums
enum(イーナム)は複数の値を取りうるという点において、C++のenumや複合体(union)のような型である。最も単純なenumは、全くC++のenumと変わらない。
enum E1 {
Var1,
Var2,
Var3
}
fn foo() {
let x: E1 = Var2;
match x {
Var2 => println!("var2"),
_ => {}
}
}
しかしながら、Rustのenumはこれよりもはるかに強力である。各状態はデータを含むことができる。タプルのように、こちらは型を並べて定義する。こうなると、enumはC++のenumよりも複合体に近くなる。Rustのenumは(C++のような)タグなしのenumというよりは、タグ付きのenumだ。なので、enumの各状態を実行時に取り違えることがない。
enum Expr {
Add(int, int),
Or(bool, bool),
Lit(int)
}
fn foo() {
let x = Or(true, false); // xの型はExpr
}
Rustにおいて、多くのオブジェクト指向的多様性は、enumを使うことで取り回しが良くなる。enumを使う際、大抵はmatch式を使用する。
match式はC++のswitch文に似ていることを覚えているだろうか。match式や他の非構造化手段については、次回詳しく話すことにしよう。
fn bar(e: Expr) {
match e {
Add(x, y) => println!("Addノード: {} + {}", x, y),
Or(..) => println!("Orノード"),
_ => println!("それ以外(今回は、Litノード)"),
}
}
match式の各項が、Expr型の状態に合致しており、全状態が網羅されていなければならない。最終項(_)が、残りの全状態をカバーしている。まあ、今回はLitしかないけどね。各状態のデータは、変数に紐付けることができる。Add項で、変数xとyにデータを紐付けているのがその例だ。データに興味がなければ、..と書いておけばいい。例内のOr項でしているようにね。
Option型
Rustにおいて、特別よく使われるenumがOption型である。Option型には二つの状態がある。SomeとNoneだ。
Noneはデータを含まず、Someは型Tのフィールドを持っている(Option型はジェネリックなenumだ。これがどういうものか後ほど詳述するが、基本的なコンセプトはC++から明らかであると嬉しい)。
Option型は、データがあるかもしれないし、ないかもしれないという状態を表すのに使われる。C++で、何かしら未定義だったり、未初期化だったり、falseだったりする値を表すのにnullポインタを使ったが、Rustでは、Option型を使って表すのがベストであろう。
使用前に必ず値の存在確認を行わなければならないので、Option型は、より安全だ。nullポインタを被参照するようなことはない。その上、Option型は、より一般的であり、ポインタだけでなく、値に対しても使用することができる。
use std::rc::Rc;
struct Node {
parent: Option<Rc<Node>>,
value: int
}
fn is_root(node: Node) -> bool {
match node.parent {
Some(_) => false,
None => true
}
}
ここで、parentフィールドはNoneかRc<Node>型の値を持つSomeたりうる。例では、この内包データを使用していないが、実際は違うだろう。Option型には、便利なメソッドがある。なので、is_root関数の本体は、node.is_none()や!node.is_some()とも書ける。
可変性の伝播とCell/RefCell
Rustのローカル変数は、標準で不変となり、mut属性を付けることで可変にできる。構造体やenumのフィールドに属性付けは行わない。こちらの可変性は伝播するのだ。要するに、構造体のフィールドが可変か不変になるかは、構造体のオブジェクト自体が可変か不変かに依存するということだ。
struct S1 {
field1: int,
field2: S2
}
struct S2 {
field: int
}
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: int
}
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というパラメータは、ライフタイムを表す。もうすぐ解説します)たまに、オブジェクト自体は論理的に不変(翻訳者注:原文ではmutableだがimmutableのtypoか?)なのに、内部的に可変であるべき部分を含むことがある。キャッシュや参照カウントを考えてほしい(これらは、本当の不変性はもたらさない。なぜなら、参照カウントの変更の効果が、デストラクタ経由で出てくるからだ)。
C++でなら、mutableキーワードを使ってオブジェクト自体がconstであっても、変更することができる。Rustでは、CellかRefCell構造体が使える。これらを使って不変なオブジェクトの一部を可変にすることができる。これは便利だが、ともすると、Rustで不変なオブジェクトを見かけたら注意を要するということである。一部が実際は可変かもしれないからね。
CellやRefCellを使えば、Rustの可変性や代入性に関する厳密なルールを回避することができる。CellやRefCellを使用するのは安全だ。なぜなら、コンパイラが静的にRustの不変性をチェックできなくても、実行時に保ってくれるからだ。CellもRefCellもシングルスレッド向けオブジェクトである。
Cell型は、コピー機構を持つ型(組み込み型のことだ)に使おう。Cell型には、保持している値を変更するget,setメソッドと、値でインスタンスを初期化するnewメソッドがある。
Cell型は、とても単純なオブジェクトだ。つまり、コピー機構を持つオブジェクトは(Rustでは)他に参照されるはずがなく、スレッド間で共有されることがないため、何も高度なことをする可能性がない。おかしな事態になりようがないのだ。
RefCell型は、ムーブ機構を持つ型に使おう。これは、Rustのほぼ全てを意味し、構造体オブジェクトがよく使われる例だ。
RefCell型もnewメソッドを使って生成し、setメソッドを持っている。RefCellオブジェクトの値を取り出すには、メソッド(borrow, borrow_mut, try_borrow, try_borrow_mut)を使って無所有権参照しなければならない。これらのメソッドは、RefCellに格納されたオブジェクトへの所有権なし参照を返す。
また、これらのメソッドも静的参照と同じルールに従い、可変無所有権参照は一つしか作れず、可変なものと不変なものを同時には作成できない。ただ、コンパイルエラーではなく、実行時エラーになる。
try_で始まるメソッドは、Option型を返し、成功時にはSome(val)を、失敗時にはNoneを得る。
値が参照されている間は、setメソッド呼び出しは失敗する。
次の例では、参照カウント式ポインタでRefCellオブジェクトを参照している(よく使われるユースケース)。
use std::rc::Rc;
use std::cell::RefCell;
struct S {
field: int
}
fn foo(x: Rc<RefCell<S>>) {
{
let s = x.borrow();
println!("フィールド、2回参照 {} {}", s.f, x.borrow().field);
// let s= x.borrow_mut(); // エラー - xの中身はすでに無所有権参照中
}
let s = x.borrow_mut(); // OK。先ほどの無所有権参照はすでにスコープ外
s.f = 45;
// println!("フィールド {}", x.borrow().field); // エラー - 同時に可変参照と不変参照はできない
println!("フィールド {}", s.f);
}
CellかRefCellオブジェクトを使っているなら、なるべく小さなオブジェクトに配置すべきだ。つまり、構造体全体に置くよりも構造体の数フィールドに配置するのを選べということだ(翻訳者注:原文を読んでもよく意味が読み取れなかった)。これらはシングルスレッドのロックオブジェクトと見なせばいい。1回のロックで処理がかち合う可能性が減るから、洗練されたロックの方がいい。
原文: https://github.com/nrc/r4cppp/blob/master/data%20types.md
0 件のコメント:
コメントを投稿
なにか意見や感想、質問などがあれば、ご自由にお書きください。