構造体を定義し、インスタンス化する
構造体は、「タプル型」節で説明したタプルに似ています。どちらも複数の関連する値を保持するためです。タプルと同様に、構造体を構成する各データはそれぞれ異なる型にできます。タプルとは異なり、構造体では各データ片に名前を付けるので、値が何を意味するのかが明確になります。こうした名前があることで、構造体はタプルより柔軟になります。インスタンスの値を指定したりアクセスしたりする際に、データの順序に頼る必要がありません。
構造体を定義するには、キーワード struct を記述し、構造体全体に名前を付けます。構造体の名前は、ひとまとめにされるデータ片の意味を表すものであるべきです。続いて、波括弧の内側で、各データ片の名前と型を定義します。これらを_フィールド_と呼びます。たとえば、リスト 5-1 はユーザーアカウントに関する情報を格納する構造体を示しています。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {}
定義した構造体を使うには、各フィールドに具体的な値を指定して、その構造体の_インスタンス_を生成します。インスタンスを作るには、構造体名を書いたあとに、波括弧の中に_key: value_ の組を並べます。ここで、キーはフィールド名、値はそのフィールドに格納したいデータです。フィールドは、構造体で宣言した順番どおりに指定する必要はありません。言い換えると、構造体定義はその型の一般的なテンプレートのようなものであり、各インスタンスはそのテンプレートに具体的なデータを埋め込むことで、その型の値を作ります。たとえば、リスト 5-2 に示すように、特定のユーザーを宣言できます。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
構造体から特定の値を取得するには、ドット記法を使います。たとえば、このユーザーのメールアドレスにアクセスするには、user1.email を使います。インスタンスが可変なら、ドット記法を使って特定のフィールドに代入することで値を変更できます。リスト 5-3 は、可変な User インスタンスの email フィールドの値を変更する方法を示しています。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
}
インスタンス全体が可変でなければならないことに注意してください。Rust では、特定のフィールドだけを可変としてマークすることはできません。どんな式でもそうであるように、関数本体の最後の式として構造体の新しいインスタンスを構築すれば、その新しいインスタンスが暗黙に返されます。
リスト 5-4 は、与えられた email と username を受け取り、User インスタンスを返す build_user 関数を示しています。active フィールドには true が入り、sign_in_count には 1 が入ります。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
}
関数パラメータに構造体のフィールドと同じ名前を付けるのは理にかなっていますが、email と username のフィールド名と変数名を繰り返し書かなければならないのは少々面倒です。構造体にさらに多くのフィールドがあれば、それぞれの名前を繰り返すのはいっそう煩わしくなります。幸い、便利な省略記法があります。
フィールド初期化省略記法を使う
リスト 5-4 ではパラメータ名と構造体フィールド名がまったく同じなので、_フィールド初期化省略記法_を使って build_user を書き換えられます。これにより、挙動はまったく同じまま、username と email の繰り返しがなくなります。これをリスト 5-5 に示します。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
}
ここでは、email という名前のフィールドを持つ User 構造体の新しいインスタンスを生成しています。email フィールドの値には、build_user 関数の email パラメータの値を設定したいのです。email フィールドと email パラメータは同名なので、email: email ではなく単に email と書くだけで済みます。
構造体更新記法でインスタンスを生成する
同じ型の別のインスタンスからほとんどの値を引き継ぎつつ、その一部だけを変更した新しい構造体インスタンスを作りたいことはよくあります。これは構造体更新記法を使うと実現できます。
まずリスト 5-6 では、更新記法を使わずに、通常の方法で user2 に新しい User インスタンスを作る方法を示します。email には新しい値を設定しますが、それ以外にはリスト 5-2 で作成した user1 と同じ値を使います。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
構造体更新記法を使うと、リスト 5-7 に示すように、より少ないコードで同じ効果を得られます。.. という構文は、明示的に設定していない残りのフィールドについて、与えられたインスタンス内の対応するフィールドと同じ値を使うことを指定します。
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
リスト 5-7 のコードでも、user2 に対して email には異なる値を持ちながら、username、active、sign_in_count フィールドには user1 と同じ値を持つインスタンスを作成しています。..user1 は、残りのすべてのフィールドが user1 の対応するフィールドから値を取得することを示すため、最後に書かなければなりません。ただし、構造体定義におけるフィールドの順序に関係なく、いくつのフィールドに対してでも、どの順番でも値を指定できます。
構造体更新構文では代入のように = を使うことに注意してください。これは、「変数とデータの相互作用:ムーブ」 節で見たのと同じように、データをムーブするためです。この例では、user1 の username フィールド内の String が user2 にムーブされたため、user2 を作成した後は user1 を使えなくなります。もし user2 に対して email と username の両方に新しい String 値を与え、user1 からは active と sign_in_count の値だけを使っていたなら、user2 を作成した後でも user1 は引き続き有効です。active と sign_in_count はどちらも Copy トレイトを実装する型なので、「スタックのみのデータ:Copy」 節で説明した動作が適用されます。この例では user1.email も引き続き使用できます。これは、その値が user1 からムーブされていないためです。
タプル構造体で異なる型を作る
Rust は、タプルに似た見た目の構造体である タプル構造体 もサポートしています。タプル構造体は、構造体名が与える追加の意味を持ちますが、各フィールドに関連付けられた名前はありません。代わりに、フィールドの型だけを持ちます。タプル構造体は、タプル全体に名前を付けて、そのタプルを他のタプルとは異なる型にしたい場合や、通常の構造体のように各フィールドに名前を付けると冗長または重複になる場合に便利です。
タプル構造体を定義するには、struct キーワードと構造体名を書き、その後にタプル内の型を続けます。たとえば、ここでは Color と Point という 2 つのタプル構造体を定義して使用しています。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
black と origin の値は、異なるタプル構造体のインスタンスであるため、別の型であることに注意してください。定義した各構造体は、それ自体が独自の型です。たとえ構造体内のフィールドが同じ型を持っていても同様です。たとえば、型 Color の引数を取る関数は、引数として Point を受け取ることはできません。両方の型が 3 つの i32 値で構成されていたとしても同じです。それ以外の点では、タプル構造体のインスタンスはタプルと似ており、分解して個々の要素を取り出せますし、. に続けてインデックスを書くことで個々の値にアクセスできます。タプルとは異なり、タプル構造体を分解するときには構造体の型名を書く必要があります。たとえば、origin ポイント内の値を x、y、z という変数に分解するには、let Point(x, y, z) = origin; と書きます。
ユニット様構造体を定義する
フィールドをまったく持たない構造体を定義することもできます。これらは ユニット様構造体 と呼ばれます。これは、「タプル型」 節で触れたユニット型 () と似た振る舞いをするためです。ユニット様構造体は、ある型に対してトレイトを実装する必要があるものの、その型自体に格納したいデータがない場合に便利です。トレイトについては第 10 章で説明します。以下は、AlwaysEqual という名前のユニット構造体を宣言してインスタンス化する例です。
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
AlwaysEqual を定義するには、struct キーワードと望む名前を書き、その後にセミコロンを付けます。波かっこも丸かっこも不要です。次に、subject 変数の中に AlwaysEqual のインスタンスを同様の方法で取得できます。つまり、定義した名前をそのまま使い、波かっこも丸かっこも付けません。後でこの型に対して、AlwaysEqual のあらゆるインスタンスが、場合によってはテスト目的で既知の結果を得るために、他のあらゆる型のインスタンスと常に等しくなるような振る舞いを実装すると想像してみてください。その振る舞いを実装するのに、データはまったく必要ありません。第 10 章では、ユニット様構造体を含むあらゆる型に対してトレイトを定義し、それを実装する方法を見ていきます。
構造体データの所有権
リスト 5-1 の
User構造体定義では、&str文字列スライス型ではなく、所有権を持つString型を使いました。これは意図的な選択です。なぜなら、この構造体の各インスタンスがそのすべてのデータを所有し、そのデータが構造体全体が有効である限り有効であってほしいからです。構造体に、他の何かが所有するデータへの参照を格納することも可能ですが、そのためには ライフタイム の使用が必要になります。ライフタイムは Rust の機能であり、第 10 章で説明します。ライフタイムは、構造体が参照しているデータが、その構造体と同じだけの期間有効であることを保証します。たとえば、以下の src/main.rs のように、ライフタイムを指定せずに構造体へ参照を格納しようとすると、これは動作しません。
struct User { active: bool, username: &str, email: &str, sign_in_count: u64, } fn main() { let user1 = User { active: true, username: "someusername123", email: "someone@example.com", sign_in_count: 1, }; }コンパイラは、ライフタイム指定子が必要だと報告します。
$ cargo run Compiling structs v0.1.0 (file:///projects/structs) error[E0106]: missing lifetime specifier --> src/main.rs:3:15 | 3 | username: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 ~ username: &'a str, | error[E0106]: missing lifetime specifier --> src/main.rs:4:12 | 4 | email: &str, | ^ expected named lifetime parameter | help: consider introducing a named lifetime parameter | 1 ~ struct User<'a> { 2 | active: bool, 3 | username: &str, 4 ~ email: &'a str, | For more information about this error, try `rustc --explain E0106`. error: could not compile `structs` (bin "structs") due to 2 previous errors第 10 章では、こうしたエラーを修正して構造体に参照を格納できるようにする方法を説明しますが、今のところは、
&strのような参照の代わりにStringのような所有権を持つ型を使うことで、このようなエラーを回避します。