はじめに
構造体の定義とインスタンス化へようこそ。この実験は、Rust Bookの一部です。LabExでRustのスキルを練習することができます。
この実験では、Rustにおける構造体の定義とインスタンス化について学びます。構造体は複数の関連する値を保持し、名前付きのフィールドを持つことができ、データの利用とアクセスをより柔軟にすることができます。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
構造体の定義とインスタンス化へようこそ。この実験は、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("[email protected]"),
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("[email protected]"),
sign_in_count: 1,
};
user1.email = String::from("[email protected]");
}
リスト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("[email protected]"),
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("[email protected]"),
..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: "[email protected]", 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でさらに実験を行って練習してください。