はじめに
前回に引き続き、Rustの入門編です。今回は構造体とトレイトについて触れます。
構造体
Rustの構造体について見ていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // User構造体の定義 struct User { name: String, email: String, sign_in_count: u64, } fn main() { // user構造体のインスタンスを作成 let user = User { name: String::from("Taro Yamada"), email: String::from("hoge@example.com"), sign_in_count: 1, }; println!("{} {} {}", user.name, user.email, user.sign_in_count); // mutではないので、userのフィールドを変更するとエラーになる user.email = String::from("foobar@example.com"); } |
構造体の宣言は
struct
キーワードで始まり、カッコの中に、フィールド名と型を定義していきます。
インスタンスを作成するには、
User {}
という形で、カッコ内にフィールド名と値を指定します。
構造体も変数と同じように、デフォルトでは不変であり、フィールドの中身を書き換えることもできません。フィールドの中身を書き換えるには、構造体の変数宣言時に
mut
キーワードを使用します。
ちなみに、不変のまま構造体の値を更新するには、構造体のインスタンス自体を作り直すことになりますが、その時に「構造体更新記法」を使用すると、コードを省略することができます。
1 2 3 4 | let user2 = User{ email: String::from("foobar@example.com"), // 他のフィールドはuserと同じ ..user } |
メソッド
構造体にメソッドを定義するには、構造体を定義した後
impl User {}
という形で、カッコの中に関数と似たシンタックスで定義していきます。まずは、コードを例示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | struct User { name: String, email: String, sign_in_count: u64, } // User構造体にメソッドを実装 impl User { fn greeting(&self) { println!("Hello, {}!", self.name); } fn update_name(&mut self, name: String) { self.name = name; } } fn main() { // user構造体のインスタンスを作成 let user = User { name: String::from("Taro Yamada"), email: String::from("hoge@example.com"), sign_in_count: 1, }; user.greeting(); // 仮引数が&mut selfとなっているメソッドは、構造体の変数宣言もmutである必要があるので、エラー user.update_name(String::from("Jiro Yamada")); } |
各メソッドの第一仮引数には
&self
とします。このselfを経由して、自身のフィールドにアクセスすることができます。
また、自身のフィールドを変更する場合は
&mut self
とします。その場合、構造体そのものの変数定義時に、mutキーワードを使用する必要があります。
self
として、所有権を奪うこともできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | impl User { fn greeting(self) { println!("Hello, {}!", self.name); } } fn main() { // user構造体のインスタンスを作成 let user = User { name: String::from("Taro Yamada"), email: String::from("hoge@example.com"), sign_in_count: 1, }; user.greeting(); // greetingはselfの所有権を奪うのでエラー user.greeting(); } |
この場合、メソッドを呼び出すと呼び出すと所有権が移ってしまうため、その後インスタンスを再利用することはできません。
回避方法としては、関数の時と同じで、メソッドで
return self
することで所有権を返すか、インスタンスをcloneしてからメソッドを呼ぶことが考えられます。
関連メソッド
selfにアクセスする必要のない「関連メソッド」を定義することもできます。例えば、以下のようなものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | impl User { fn new(first_name: String, last_name: String, email: String) -> User { User { first_name, last_name, email, sign_in_count: 1, } } } fn main() { // newを使ってインスタンス生成 let user = User::new( String::from("Taro"), String::from("Yamada"), String::from("hoge@example.com"), ); } |
Rustでの慣習で、自身のインスタンスを生成する関数は、
fn new()
というように、関連メソッドで定義します。関連メソッドの呼び出しは、
User::new()
という形で呼び出します。
トレイト
Rustでのトレイトは次のように
trait
キーワードを使って定義できます。先ほどのコードを変更してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // トレイトの定義 trait FullName { fn full_name(&self) -> String; } struct User { first_name: String, last_name: String, email: String, sign_in_count: u64, } // FullNameトレイトの実装 impl FullName for User { fn full_name(&self) -> String { format!("{} {}", self.first_name, self.last_name) } } fn main() { let user = User { first_name: String::from("Taro"), last_name: String::from("Yamada"), email: String::from("hoge@example.com"), sign_in_count: 1, }; print_fullname(&user); } // 引数をトレイトで受ける fn print_fullname(full_name: &impl FullName) { println!("{}", full_name.full_name()); } |
トレイトを宣言するには
trait
キーワードを使用し、かっこの中にメソッドを定義します。構造体がトレイトを実装するには、
impl トレイト名 for 構造体名
とし、かっこの中にトレイトで定義されているメソッドを実装します。
また、引数として受ける場合にトレイトを使用する場合には、
&impl トレイト名
として受けます。
このようにトレイトを使用することで、ポリモーフィズムを実現することができます。
構造体のフィールドの1つとして、トレイトのインスタンスを持つ場合
構造体のフィールドの1つとして、トレイトのインスタンスを持つ場合
dyn
キーワードを使用します。また、合わせてBoxを使う必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | struct Hoge { full_name: Box<dyn FullName>, } fn main() { let user = User { first_name: String::from("Taro"), last_name: String::from("Yamada"), email: String::from("hoge@example.com"), sign_in_count: 1, }; Hoge { full_name: Box::new(user), }; } |
Rustの構造体は、基本的にはコンパイル時に必要なデータサイズを計算できるようになっている必要があります。ですが、トレイトの場合は、実際に注入されるインスタンスの実装によって必要なデータサイズが異なるため、コンパイル時には必要なメモリ量を知ることができません。
そこで使用するのが、ヒープです。ヒープは、可変長データを扱うためのメモリ領域で、Boxを使うとヒープに実データを格納することができます。Box以外にも、VecやStringといった可変長データもヒープを使用します。
なぜ、コンパイル時に構造体に必要なデータサイズを知って置ける必要があるのでしょうか。それは、Rustでは基本的にスタック領域を使用するためです。スタック領域は、固定長のデータを扱うことができる代わりに、動作が高速です。
また、所有権システムを使用しているために、コンパイル時にスタックのデータ容量を計算することが可能になるのです。
対して、ヒープは可変長である代わりに、動作が低速です。そのため、必要な時にだけ使用するようにします。
Box自体は所有権システムによって管理されるため、スコープを抜けるとスタックに格納されているBoxと、ヒープに格納されているデータの両方が解放されます。
参考: Box<T>を使ってヒープにデータを格納する
スタックとヒープについては詳しくはこちらが参考になります。
https://blog.dcs.co.jp/rust/20201217-rust-5.html
dynについてはこちら
http://doc.rust-jp.rs/rust-by-example-ja/trait/dyn.html
derive属性
derive属性とは、マクロによってトレイトの既定の振る舞いを実装させることができる機能です。例えば
Debug
や
PartialEq
、
Copy
などがあります。
例えば、構造体動詞を
==
演算子で比較する場合、
PartialEq
のトレイトの実装が必要です。そのためには
fn eq()
メソッドを実装する必要がありますが、ほとんどの場合で全フィールドが等しい場合のみ等価、そうでなければ等価ではない、という実装になります。
このようなコードを毎回書くと冗長になってしまうため、derive属性によって、マクロによりコードを生成させることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | // User構造体の定義 #[derive(Debug, PartialEq)] struct User { first_name: String, last_name: String, email: String, sign_in_count: u64, } fn main() { let user1 = User { first_name: String::from("Taro"), last_name: String::from("Yamada"), email: String::from("hoge@example.com"), sign_in_count: 1, }; let user2 = User { first_name: String::from("Jiro"), last_name: String::from("Yamada"), email: String::from("hoge@example.com"), sign_in_count: 1, }; // PartialEqを実装しているので、比較できる if user1 == user2 { println!("equal"); } else { println!("not equal"); } // Debugを実装しているので、 フォーマット文字列{:?}で内容を出力できる print!("{:?}", user); } |
User構造体にderive属性を使って、DebugとPartialEqを実装しました。コードはマクロが生成してくれるので、実装コードを書く必要はありませんが、PartialEqによって比較や、Debugによって構造体のフィールドを出力できるようになっています。