Rust 入門してる #3 : オブジェクト指向っぽい部分?
#2 に続いて Rust のドキュメントをぼちぼち読んでます。 このブログ記事はメモと感想程度のものですので、詳細は各見出しの下のリンク先を見てください。
今回は構造体とか列挙型とかメソッドとかトレイトとかそこら辺のオブジェクト指向っぽい部分 (の途中まで) を読みました。 パターンマッチとか文字列とかも出てきました。
構造体 (Structs)
// こういう感じで定義して、 struct Point { x: i32, y: i32, } // こういう感じでインスタンスを生成。 let origin = Point { x: 0, y: 0 }; // `..` に続けて別のインスタンスを指定すると、残りのフィールドにそのインスタンスの値を渡せるぽい。 let x1 = Point { x: 1, .. origin };
Tuple に名前を付けたような Tuple Structs というものもある。 型としては Tuple 型らしい。
struct Color(i32, i32, i32); let black = Color(0, 0, 0);
Unit-like Structs というものもある。 これはフィールドを持たない構造体で、Unit-like Structs を定義すると、同時にその構造体と同名の定数も定義される。
// Unit-like Structs
struct Cookie;
上の定義は、下のコードと同等。
struct Cookie {}
const Cookie: Cookie = Cookie {};
単に構造体を定義してフィールドの値を読み書きする、というぐらいであれば簡単っぽいけど、実践的に使うためにはトレイトと組み合わせたりしないとだめそうな気がする。 (まだよくわかってない。)
列挙型 (Enums)
列挙型は、複数の変種 (variant) の中の 1 つのデータを表す型。 列挙型の中のそれぞれの変種は、それに関連するデータを保持することもできる。 列挙型の値としては、その列挙型の任意の変種を取りうる。 代数的データ型でいうところの直和型 (sum type) に相当するっぽい。
enum Message { Quit, ChangeColor(i32, i32, i32), Move { x: i32, y: i32 }, Write(String), }
上の例を見るとわかるように、各変種を定義するための構文は、構造体を定義するための構文に似ている。
それぞれの変種は列挙型の名前でスコープ化されているので、::
を使って各変種の名前を使用する。
let message: Message = Message::Move { x: 10, y: 5 };
列挙型のコンストラクタは、関数としても扱えるらしい。 例えば、上の例の Message::Write
のコンストラクタは fn (String) -> Message
という型の値として扱える。 クロージャとかと同様に他の関数に渡したりして使う場合に便利っぽい。
Match
match
式はパターンによって分岐するものである。 単純な例だと、以下のように if
/else
の連なりを書きかえることができる。
let x = 3; let val = match x { 1 => "one", 2 => "two", 3 => "three", _ => "else", };
match
には、「網羅的なチェック」 を強制するという利点がある。 例えば上の例で 「_
」 を除くとコンパイルエラーになる。
パターン (Patterns)
変数束縛や match
式、他の場所にもパターンは現れる。 下記のものを組み合わせてパターンが表現される。
- 複式パターン :
1 | 2
- 範囲 :
1 ... 5
- 変数への束縛 :
x
- 値の型に応じてコピーか移動になる。
- 参照を束縛 :
ref x
(ref mut x
) - サブパターンで束縛 :
e @ 1 ... 5
- デストラクチャリング :
Point { x, y }
- フィールドの値に別名を付けて束縛 :
Point { x: x1, y: ref y1 }
- フィールドの値に別名を付けて束縛 :
- ワイルドカード (
..
) :Point { x: x1, .. }
- プレイスホルダ (
_
) :Err(_)
パターンガードもある。
let message = match val { Some(x) if x < 5 => "Less than five", Some(x) => "Else", None => "None", };
メソッド
impl
キーワードである型に対するメソッドを定義できる。 第 1 引数は self
か &self
か &mut self
。 impl
は同じ型に対していくつも定義できるらしい。
struct Point { x: i32, y: i32 } impl Point { fn p(&self) { println!("({}, {})", self.x, self.y); } } let point = Point { x: 1, y: 2 }; point.p();
関連する関数 (static メソッドぽいもの) も定義できる。 第 1 引数が self
(または &self
、&mut self
) でない場合は自動的にそうなる。
impl Point { fn new(x: i32, y: i32) -> Point { Point { x: x, y: y } } } let point = Point::new(10, 2);
ここら辺は Perl とかやってると馴染みのある感じ。 既存の構造体に対して新しいメソッドを定義することもできそうだし、そこら辺は便利そうな気がする。 (動的型付けだとどこでメソッドが定義されたのかわからなくて困りそうだけど、Rust だとまあ大丈夫そう?)
Vectors
以下のようなイテレーションの例があるけど、vector の指定方法に応じて i
の型も変わるのが慣れるまで大変そう。
for i in &v { println!("A reference to {}", i); } for i in &mut v { println!("A mutable reference to {}", i); } for i in v { println!("Take ownership of the vector and its element {}", i); }
文字列
Rust の文字列は UTF-8 エンコードされた Unicode スカラ値の連なり。 全ての文字列は UTF-8 列として正しい。
型としては主に 2 種類。
&str
: 文字列スライス (string slices)- サイズ固定で変更不可。
String
: ヒープ割り当てされた文字列。- 変更可能。
文字列リテラルは static にメモリ割り当てされるので、それへの参照として &str
を使用できる。
&str
の値のto_string()
メソッドを呼ぶことでString
の値を得られる。 (メモリ割り当てするのでコストがかかる。)String
の値の参照を取ることで&str
として扱える。 (&String
から&str
への自動的な型強制がかかるため。 コストはかからない。)
バイトごとや文字ごとのイテレーションをしたい場合は as_bytes()
メソッドや chars()
メソッドを使うぽい。
String
の後ろに &str
を結合できる。
let s1: String = "こんにちは".to_string(); let s2: &str = "世界"; println!("{}", s1 + s2);
こういう結合をしたときの所有権の動きがいまいちよくわかってない。
トレイト
Rust のトレイトは、メソッドを宣言するだけで実装は含まないから *1 トレイトというよりインターフェイスなのでは? と思ったけど、impl
キーワードでトレイトに対する実装を提供する、という感じだからインターフェイスじゃなくてトレイトなのか。 impl HasArea for Circle { ... }
という感じ。
Java なんかの Circle
が HasArea
を実装する、というのとは逆で、Circle
に対する HasArea
の実装を提供する、という感じなのが面白い。 こういう風になっているおかげで、既存の構造体に対して追加でトレイトの実装を提供できて便利ぽい。 (と思ったけど、トレイトと実装対象の型のどちらかは自分で実装したものではないとだめっぽいから、自由になんでもできるわけではなさそう。)
ジェネリック関数の境界にトレイトを使用できる。
fn print_area<T: HasArea>(shape: T) { ... }
複数の境界を指定することもできる。 Java 8 の intersection types っぽい。
fn foo<T: Clone + Debug>(x: T) { ... }
ジェネリック型の実装のジェネリックパラメータの境界にトレイトを使用した場合は、その実装はその条件を満たすジェネリックパラメータの場合のみにその実装が提供される。
impl<T: PartialEq> Rectangle<T> { fn is_square(&self) -> bool { self.width == self.height } }
上の例だと、ジェネリックパラメータ T
に対して PartialEq
の実装が提供されている場合にのみ Rectangle<T>
に is_square
メソッドが生える。
ジェネリックパラメータの宣言場所での境界の定義だけでなく、where
句での定義も可能。
fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) { ... } // 上と同じ。 fn foo<T, K>(x: T, y: K) where T: Clone, K: Clone + Debug { ... }
トレイトはメソッドのデフォルト実装を持てる。 また、継承も可能。
一部のトレイトは、属性を使って自動的に実装できる。
#[derive(Debug)] struct Foo;
*1:後ろの方に書いたけどデフォルト実装を持てます。 suzak さん指摘ありがとうございます!