はじめに
構造体の定義とインスタンス化へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。
この実験では、Rust における構造体の定義とインスタンス化について学びます。構造体は複数の関連する値を保持し、名前付きのフィールドを持つことができ、データの利用とアクセスをより柔軟にすることができます。
構造体の定義とインスタンス化
構造体は、「タプル型」で議論されているタプルと似ており、複数の関連する値を保持しています。タプルと同様に、構造体の要素は異なる型であることができます。タプルとは異なり、構造体では各データの要素に名前を付けるので、値が何を意味するのかが明確になります。これらの名前を追加することで、構造体はタプルよりも柔軟になります。インスタンスの値を指定またはアクセスする際に、データの順序に依存する必要はありません。
構造体を定義するには、キーワードstructを入力し、構造体全体に名前を付けます。構造体の名前は、まとめられているデータの要素の重要性を表すものでなければなりません。その後、波括弧の中で、データの要素の名前と型を定義します。これらを「フィールド」と呼びます。たとえば、リスト 5-1 は、ユーザーアカウントに関する情報を格納する構造体を示しています。
ファイル名:src/main.rs
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
リスト 5-1: User構造体の定義
構造体を定義した後に使用するには、各フィールドに具体的な値を指定することで、その構造体の「インスタンス」を作成します。構造体の名前を記述し、その後にキー:値のペアを含む波括弧を追加することでインスタンスを作成します。ここで、キーはフィールドの名前で、値はそれらのフィールドに格納したいデータです。構造体で宣言した順序と同じ順序でフィールドを指定する必要はありません。言い換えると、構造体の定義は型の一般的なテンプレートのようなものであり、インスタンスはそのテンプレートに特定のデータを埋め込んで、その型の値を作成します。たとえば、リスト 5-2 に示すように、特定のユーザーを宣言することができます。
ファイル名:src/main.rs
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
sign_in_count: 1,
};
}
リスト 5-2: User構造体のインスタンスの作成
構造体から特定の値を取得するには、ドット表記を使用します。たとえば、このユーザーのメールアドレスにアクセスするには、user1.emailを使用します。インスタンスがミュータブルな場合、ドット表記を使用して特定のフィールドに代入することで値を変更することができます。リスト 5-3 は、ミュータブルなUserインスタンスのemailフィールドの値を変更する方法を示しています。
ファイル名:src/main.rs
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");
}
リスト 5-3: Userインスタンスのemailフィールドの値の変更
インスタンス全体がミュータブルであることに注意してください。Rust は、特定のフィールドのみをミュータブルとしてマークすることは許可していません。関数の最後の式として構造体の新しいインスタンスを構築することで、その新しいインスタンスを暗黙的に返すことができます。
リスト 5-4 は、与えられたメールアドレスとユーザー名を持つUserインスタンスを返すbuild_user関数を示しています。activeフィールドはtrueの値を取得し、sign_in_countは1の値を取得します。
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
リスト 5-4: メールアドレスとユーザー名を受け取り、Userインスタンスを返すbuild_user関数
関数のパラメータを構造体のフィールドと同じ名前で命名するのは理にかなっていますが、emailとusernameのフィールド名と変数を繰り返すのは少々面倒です。構造体により多くのフィールドがある場合、各名前を繰り返すのはさらに面倒くさくなります。幸い、便利な省略記法があります!
フィールド初期化省略記法の使用
リスト 5-4 では、パラメータ名と構造体のフィールド名がまったく同じであるため、フィールド初期化省略記法の構文を使用してbuild_userを書き換えることができます。これにより、同じように動作するが、usernameとemailの繰り返しがなくなります。これはリスト 5-5 に示されています。
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
リスト 5-5: usernameとemailのパラメータが構造体のフィールドと同じ名前であるため、フィールド初期化省略記法を使用するbuild_user関数
ここでは、emailという名前のフィールドを持つUser構造体の新しいインスタンスを作成しています。build_user関数のemailパラメータの値に、emailフィールドの値を設定したいと考えています。emailフィールドとemailパラメータが同じ名前であるため、email: emailではなく、単にemailと書くだけです。
構造体更新構文を使って他のインスタンスからインスタンスを作成する
既存のインスタンスの値のほとんどを含み、一部を変更した新しい構造体のインスタンスを作成することは、頻繁に役立ちます。これは、構造体更新構文を使って行うことができます。
まず、リスト 5-6 では、更新構文を使わずに通常どおり新しいUserインスタンスuser2を作成する方法を示しています。emailに新しい値を設定しますが、それ以外はリスト 5-2 で作成したuser1の同じ値を使用します。
ファイル名:src/main.rs
fn main() {
--snip--
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}
リスト 5-6: user1の値の 1 つを使って新しいUserインスタンスを作成する
構造体更新構文を使うと、より少ないコードで同じ効果を得ることができます。これはリスト 5-7 に示されています。構文..は、明示的に設定されていない残りのフィールドが、与えられたインスタンスのフィールドと同じ値を持つことを指定します。
ファイル名:src/main.rs
fn main() {
--snip--
let user2 = User {
email: String::from("another@example.com"),
..user1
};
}
リスト 5-7: 構造体更新構文を使ってUserインスタンスの新しいemail値を設定し、user1の残りの値を使用する
リスト 5-7 のコードも、user2にインスタンスを作成します。このインスタンスは、emailの値が異なりますが、username、active、およびsign_in_countのフィールドはuser1と同じ値を持ちます。..user1は、残りのすべてのフィールドがuser1の対応するフィールドから値を取得することを指定するために最後に来る必要がありますが、構造体の定義におけるフィールドの順序に関係なく、任意の順序で任意の数のフィールドに値を指定することができます。
構造体更新構文は代入のように=を使うことに注意してください。これは、「変数とデータのムーブによる相互作用」で見たように、データを移動させるためです。この例では、user2を作成した後はもはやuser1を使用することができません。なぜなら、user1のusernameフィールドのStringがuser2に移動したからです。もしuser2にemailとusernameの両方に新しいString値を与え、そのためuser1のactiveとsign_in_countの値のみを使用した場合、user2を作成した後もuser1は有効なままです。activeとsign_in_countの両方はCopyトレイトを実装する型なので、「スタックのみのデータ:Copy」で議論した動作が適用されます。
名前付きフィールドのないタプル構造体を使って異なる型を作成する
Rust は、タプルに似た構造体であるタプル構造体もサポートしています。タプル構造体は、構造体名が持つ追加の意味を持ちますが、フィールドには名前が関連付けられていません。それどころか、それらはただフィールドの型を持っています。タプル構造体は、タプル全体に名前を付けて、他のタプルとは異なる型にする場合や、通常の構造体のように各フィールドに名前を付けるのが煩雑または冗長になる場合に便利です。
タプル構造体を定義するには、structキーワードと構造体名を先頭に付け、その後にタプル内の型を続けます。たとえば、ここではColorとPointという名前の 2 つのタプル構造体を定義して使用しています。
ファイル名:src/main.rs
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値で構成されているとしてもです。それ以外は、タプル構造体のインスタンスはタプルと同様に、個々の要素に分解することができ、個々の値にアクセスするにはインデックスの後に.を使うことができます。
フィールドのないユニット型の構造体
フィールドがまったくない構造体も定義できます!これらはユニット型に似た構造体と呼ばれます。なぜなら、「タプル型」で言及したユニット型()と同じように振る舞うからです。ユニット型に似た構造体は、特定の型に対してトレイトを実装する必要があるが、型自体に格納したいデータがない場合に便利です。第 10 章でトレイトについて説明します。以下は、AlwaysEqualという名前のユニット構造体を宣言してインスタンス化する例です。
ファイル名:src/main.rs
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
AlwaysEqualを定義するには、structキーワードと望む名前、そしてセミコロンを使います。波括弧や丸括弧は必要ありません!その後、同じようにsubject変数にAlwaysEqualのインスタンスを取得できます。定義した名前を使って、波括弧や丸括弧は不要です。後でこの型に対して、AlwaysEqualのすべてのインスタンスが他の任意の型のすべてのインスタンスと常に等しくなるような動作を実装すると想像してみてください。たとえば、テスト目的で既知の結果を得るためです。その動作を実装するにはデータは必要ありません!第 10 章では、ユニット型に似た構造体を含む任意の型に対してトレイトを定義して実装する方法を説明します。
構造体データの所有権
リスト 5-1 の
User構造体の定義では、所有権のあるString型を&str文字列スライス型の代わりに使用しました。これは意図的な選択です。なぜなら、この構造体の各インスタンスがすべてのデータを所有し、そのデータが構造体全体が有効な限り有効であることを望むからです。構造体が他のものが所有するデータへの参照を格納することも可能ですが、そのためには寿命期間指定子を使用する必要があります。これは 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, |第 10 章では、これらのエラーを修正して構造体に参照を格納できるようにする方法について説明しますが、今のところ、このようなエラーを修正するために、
&strのような参照の代わりにStringのような所有権のある型を使用します。
まとめ
おめでとうございます!「構造体の定義とインスタンス化」の実験を完了しました。技術力を向上させるために、LabEx でさらに実験を行って練習してください。