はじめに
The Rust Programming Language 日本語版の勉強のために、自分が大切だと思った箇所に装飾を付けて記録を残しました。
この記事の内容は、以下記事の引用です。
引用元 :
https://doc.rust-jp.rs/book-ja/ch01-00-getting-started.html
構造体を使用して関係のあるデータを構造化する
structまたは構造体は、意味のあるグループを形成する複数の関連した値をまとめ、名前つけできる独自のデータ型です。
あなたがオブジェクト指向言語に造形が深いなら、structはオブジェクトのデータ属性みたいなものです。
この章では、タプルと構造体を対照的に比較し、構造体の使用法をデモし、メソッドや関連関数を定義して、構造体のデータに紐づく振る舞いを指定する方法について議論します。
構造体とenumは、自分のプログラム領域で新しい型を定義し、Rustのコンパイル型精査機能をフル活用する構成要素になります。
構造体を定義し、インスタンス化する
構造体は第3章で議論したタプルと似ています。
タプル同様、構造体の一部を異なる型にできます。
一方、タプルとは違って、各データ型には名前をつけるので、値の意味が明確になります。
この名前のお陰で、構造体はタプルに比して、より柔軟になるわけです。
データの順番に頼って、インスタンスの値を指定したり、アクセスしたりする必要がないのです。
構造体の定義は、structキーワードに入れ、構造体全体に名前をつけます。
構造体名は、1つにグループ化されるデータ型の意義を表すものであるべきです。
そして、波括弧内にデータ型の名前と型を定義し、これはフィールドと呼ばれます。
例えば、以下ではユーザーアカウントに関する情報を保持する構造体を示しています。
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
構造体から特定の値を得るには、ドット記法が使えます。
このユーザのEメールアドレスだけが欲しいなら、この値を使いたかった場所全部で「user1.email」が使えます。
インスタンスが可変であれば、ドット記法を使い特定のフィールドに代入することで値を変更できます。
以下では、可変なUserインスタンスのemailフィールドの値を変更する方法を示しています。
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");
インスタンス全体が可変でなければならないことに注意してください。
Rustでは、一部のフィールドのみを可変にすることは出来ないのです。
また、あらゆる式同様、構造体の新規インスタンスを関数本体の最後の式として生成して、そのインスタンスを返すことを暗示できます。
以下では、与えられたemailとusernameでUserインスタンスを生成するbuild_user関数を示しています。
activeフィールドにはtrue値が入り、sign_in_countには値1が入ります。
▼Emailとユーザー名を受け取り、Userインスタンスを返すbuild_user関数
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
構造体のフィールドと同じ名前を関数の引数にも付けることは筋が通っていますが、emailとusernameというフィールド名と変数を繰り返さないといけないのは面倒です。
構造体にもっとフィールドがあれば、名前を繰り返すことは更にまず煩わしくなります。
幸運なことに、便利な省略記法があります。
フィールドと変数が同盟のときにフィールド初期化省略記法を使う
仮引数名と構造体のフィールド名が全く一緒なので、フィールド初期化省略記法を使ってbuild_userを書き換えても、振る舞いは全く同じにしつつ、書きで示したようにemailとusernameを繰り返さなくても良くなります。
▼emailとusername引数が構造体のフィールドと同名なので、フィールド初期化省略法を使用するbuild_user関数
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
ここでemailというフィールドを持つUser構造体の新規インスタンスを生成しています。
emailフィールドをbuild_user関数のemail引数の値にセットしたいわけです。
emailフィールドとemail引数は同じ名前なので、email:emailと書くのではなく、emailと書くだけで済むのです。
構造体更新記法で他のインスタンスからインスタンスを生成する
多くは前のインスタンスの値を使用しつつ、変更する箇所もある形で新しいインスタンスを生成できると有用です。
構造体更新記法で叶えられます。
まず、以下は更新記法なしでuser2に新しいUserインスタンスを生成する方法を示しています。
emailとusernameには新しい値をセットしていますが、それ以外には上図のuser1の値を使用しています。
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
active: user1.active,
sign_in_count: user1.sign_in_count,
};
構造体更新記法を使用すると、書きで示すようにコード量を減らしつつ、同じ効果を達成できます。
「..」という記法により、明示的にセットされていない残りのフィールドが与えられたインスタンスのフィールドと同じ値になるように指定します。
▼構造体更新記法を使用して、新しいUserインスタンス用の値に新しいemailとusernameをセットしつつ、残りの値はuser1変数のフィールド値を使う
let user2 = User {
email: String::from("another@example.com"),
username: String::from("anotherusername567"),
..user1
};
emailとusernameについては異なる値、activeとsign_in_countフィールドについては、user1と同じ値になるインスタンスをuser2に生成します。
異なる型を生成する名前付きフィールドのないタプル構造体を使用する
構造体名により追加の意味を含むものの、フィールドに紐付けられた名前がなく、むしろフィールドの型だけのタプル構造体と呼ばれる、タプルに似た構造体を定義する事もできます。
タプル構造体は、構造体名が提供する追加の意味は含むものの、フィールドに紐付けられた名前はありません。
むしろ、フィールドの型だけが存在します。
タプル構造体は、タプル全体に名前をつけ、そのタプルを他のタプルとは異なる型にしたい場合に有用ですが、普通の構造体のように各フィールド名を与えるのは冗長、または余計になるでしょう。
タプル構造体を定義するには、structキーワードのあとに構造体名、さらにタプルに含まれる型を続けます。
例えば、以下はColorとPointという2種類のタプル構造体の定義と使用法です。
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
blackとoriginの値は違う型であることに注目してください。
これらは、異なるタプル構造体のインスタンスですからね。
定義された各構造体は、構造体内のフィールドが同じ型であっても、それ自身が独自の型になります。
例えば、Color型を引数にとる関数は、Pointを引数に取ることは出来ません。
たとえ、両者の型が3つのi32値からできていてもです。
それ以外については、タプル構造体のインスタンスは、タプルと同じように振る舞います。
分配して個々の部品にしたり、「.」と添字を使用して個々の値にアクセスするなどです。
フィールドのないユニット様構造体
また、一切フィールドのない構造体を定義することもできます。
これらは、()、ユニット型と似たような振る舞いをすることから、ユニット様構造体と呼ばれます。
ユニット様構造体は、ある型にトレイトを実装するけれども、型自体に保持させるデータは一切ない場面に有効になります。
構造体データの所有権
User構造体定義において、&str文字列スライス型でなく、所有権のあるString型を使用しました。
これは意図的な選択です。
というのも、この構造体のインスタンスには全データを所有してもらう必要があり、このデータは構造体全体が有効な間はずっと有効である必要があるのです。
構造体に、他のなにかに所有されたデータへの参照を保持させることもできますが、そうするにはライフタイムという第10章で議論するRustの機能を使用しなければいけません。
ライフタイムのおかげで構造体に参照されたデータが、構造体自体が有効な間、ずっと有効であることを保証してくれるのです。
ライフタイムを指定せずに構造体に参照を保持させようとしましょう。これは動きません。
struct User {
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: "someone@example.com",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}
コンパイラはライフタイム指定しが必要だと起こるでしょう。
第10章で、これらのエラーを解消して構造体に参照を保持する方法について議論しますが、当面今回のようなエラーは&strのような参照の代わりにStringのような所有された型を使うことで修正します。
構造体を使ったプログラム例
構造体を使用したくなる可能性のあるケースを理解するために、長方形の面積を求めるプログラムを書きましょう。
単一の変数からはじめ、代わりに構造体を使うようにプログラムをリファクタリングします。
Cargoでrectanglesという新規バイナリプロジェクトを作成しましょう。
このプロジェクトは、長方形の幅と高さをピクセルで指定し、その面積を求めます。
以下にプロジェクトのsrcx/main.rsでまさにそうする一例を短いプログラムとして示しました。
▼個別の幅と高さ変数を指定して長方形の面積を求める
fn main() {
let width1 = 30;
let height1 = 50;
println!(
// 長方形の面積は、{}平方ピクセルです
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
では、cargo runでこのプログラムを走らせてください。
The area of the rectangle is 1500 square pixels.
(長方形の面積は、1500平方ピクセルです)
タプルでリファクタリングする
幅と高さを組み合わせると一つの長方形を表すので、相互に関係があります。
このコードの問題点は、areaのシグニチャから明らかです。
fn area(width: u32, height: u32) -> u32 {
area関数は、1長方形の面積を求めるものと考えられますが、今書いた関数には、引数が2つあります。
引数は関連性があるのに、このプログラム内のどこにもそのことは表現されていません。
幅と高さを一緒のグループ化するほうが、より読みやすく扱いやすくなるでしょう。
それをする1つの方法として、第三章の「タプル型」ですでに議論したタプルを使うのです。
タプルでリファクタリングする
以下は、タプルを使う別バージョンのプログラムを示しています。
▼タプルで長方形の幅と高さを指定する
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
ある意味では、このプログラムはましです。
タプルのおかげで少し構造的になり、1引数を渡すだけになりました。
しかし別の意味では、このバージョンは明確性を失っています。
タプルは要素に名前をつけないので、計算が不明瞭になったのです。
なぜなら、タプルの一部に添字アクセスをする必要があるからです。
面積計算で幅と高さを混在させるのなら問題はないのですが、長方形を画面に描画したいとなると問題になるのです。
タプルの添字0が幅で、添字1が高さであることを肝に命じて置かなければなりません。
他人がこのコードをいじることになったら、
このことを割り出し、同様に肝に命じなければならないでしょう。
たやすくこのことを忘れたり、これらの値をまぜこぜにしたりしてエラーを発生させてしまうでしょう。
データの意味をコードに載せていないからです。
構造体でリファクタリングする:より意味付けする
データのラベル付けで意味を付与するために構造体を使います。
現在使用しているタプルを全体と一部に名前のあるデータ型に変形することが出来ます。
▼Rectangle構造体を定義する
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
ここでは、構造体を定義し、Rectangleという名前にしています。
波括弧の中でwidthとheightというフィールドを定義し、u32という型にしました。
それからmain内でRectangleの特定のインスタンスを生成し、幅を30、高さを50にしました。
これでarea関数は引数が1つになり、この引数は名前がrectangle、型はRectangle構造体インスタンスへの不変借用になりました。
第4章で触れたように、構造体の所有権を奪うよりも借用する必要があります。
こうすることでmainは所有権を保って、rect1を使用し続けることができ、そのために関数シグニチャと関数呼び出し時に&を使っているわけです。
area関数は、Rectangleインスタンスのwidthとheightフィールドにアクセスしています。
これで、area関数シグニチャは我々の意図をズバリ示すようになりました。
widthとheightフィールドを使って、Rectangleの面積を計算します。
これにより、幅と高さが相互に関係していることが伝わり、タプルの0や1という添字を使うよりも、これらの値に説明的な名前を与えられるのです。
プログラムの意図が明確になりました。
トレイトの導出(どうしゅつ)で有用な機能を追加する
プログラムのデバックをしている間に、Rectangleのインスタンスを出力し、フィールドの値を確認できると、素晴らしいわけです。
以前の章のようにprintln!マクロを試しに使用しようとしても動きません。
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
// rect1は{}です
println!("rect1 is {}", rect1);
}
このコードを走らせると、以下のようなエラーが出ます。
error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied
(エラー: トレイト境界`Rectangle: std::fmt::Display`が満たされていません)
println!マクロには様々な整形があり、標準では波括弧はDisplayとして知られる整形をするよう、println!に指示するのです。
直接エンドユーザ向けの出力です。
これまでに見てきた基本形は、標準でDisplayを実装しています。
というのも、1や他の基本形をユーザーに見せる方法は1つしかないからです。
しかし構造体では、println!が出力を整形する方法は自明ではなくなります。
出力方法がいくつもあるからです。
カンマは必要なの?波括弧を出力する必要はある?全フィールドが見えるべき?この曖昧性のため、Rustは必要なものを推測しようとせず、構造体にはDisplay実装が提供されていないのです。
エラーを読み返すと、このような有益な注意書きがあります。
`Rectangle` cannot be formatted with the default formatter; try using
`:?` instead if you are using a format string
(注釈: `Rectangle`は、デフォルト整形機では、整形できません; フォーマット文字列を使うのなら
代わりに`:?`を試してみてください)
試してみましょう!
println!マクロ呼び出しは、**println!("rect1 is {:?}", rect1);**という見た目になるでしょう。
波カッコ内に「:?」という指定師を書くと、println!にDebugと呼ばれる出力整形を使いたいと指示するのです。
Debugトレイトは、開発者にとって有用な方法で構造体を出力させてくれるので、コードをデバッグしている最中に値を確認することができます。
変更してコードを走らせてください。
なに!まだエラーが出ます。
error[E0277]: the trait bound `Rectangle: std::fmt::Debug` is not satisfied
(エラー: トレイト境界`Rectangle: std::fmt::Debug`が満たされていません)
しかも、今回のコンパイラは有益な注意書きを残してくれています。
`Rectangle` cannot be formatted using `:?`; if it is defined in your
crate, add `#[derive(Debug)]` or manually implement it
(注釈: `Rectangle`は`:?`を使って整形できません; 自分のクレートで定義しているのなら
`#[derive(Debug)]`を追加するか、手動で実装してください)
確かにRustにはデバッグ用の情報を出力する機能が備わっていますが、この機能を構造体で使えるようにするには、明示的な選択をしなければならないのです。
そうするには、構造体定義の直前に「#[derive(Debug)]という注釈を追加します。
▼Debugトレイトを導出する注釈を追加し、Rectangleインスタンスをデバッグ用整形機で出力する
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1 is {:?}", rect1);
}
これでプログラムを実行すれば、エラーは出ず、以下のような出力が得られるでしょう。
rect1 is Rectangle { width: 30, height: 50 }
素晴らしい!最善の出力ではないものの、このインスタンスの全フィールドの値を出力しているので、デバッグ中には間違いなく役立つでしょう。
より大きな構造体があるなら、もう少し読みやすい出力のほうが有用です。
そのような場合には、、println!文字列中の{:?}の代わりに{:#?}を使う事ができます。
この例で、{:#?}というスタイルを使用したら、出力は以下のようになるでしょう。
rect1 is Rectangle {
width: 30,
height: 50
}
Rustには、derive(和訳:導き出す)注釈で使えるトレイトが多く提供されており、独自の型に有用な振る舞いを追加することができます。
area関数は、非常に特殊です。
長方形の面積を算出するだけです。
Rectangle構造体とこの動作をより緊密に結び付けられると役立つでしょう。
なぜなら、他のどんな型でもうまく動作しなくなるからです。
area関数をRectangle型に定義されたareaメソッドに変形することで、このコードをリファクタリングし続けられる方法について見ていきます。
メソッド記法
メソッドは関数に似ています。
fnキーワードと名前で宣言されるし、引数と返り値があるし、どこか別の場所で呼び出された時に実行されるコードを含みます。
ところが、メソッドは構造体の文脈(あるいはenumかトレイとオブジェクトの)で定義されるという点で、関数とは異なり、最初の引数は必ずselfになり、selfはメソッドが呼び出されている構造体インスタンスを表します。
メソッドを定義する
Rectangleインスタンスを引数に取るarea関数を変え、代わりにRevtangle構造体上にareaメソッドを作りましょう。
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
Rectangleの文脈内で関数を定義するには、**impl(implementation:実装)**ブロックを始めます。
それからarea関数をimplの波括弧内に移動させ、最初の(今回は唯一の)引数をシグニチャ内と本体内全てでselfに変えます。
area関数を呼び出し、rect1を引数として渡すmainでは、だいたいとしてメソッド記法を使用して、Rectangleインスタンスのareaメソッドを呼び出せます。
メソッド記法は、インスタンスのあとに続きます。
ドット、メソッド名、カッコ、そして引数と続くわけです。
areaのシグニチャでは、rectangle: &Rectangleの代わりに&selfを使用します。
というのも、コンパイラはこのメソッドがimpl Rectangleという文脈内に存在するために、selfの型がRectangleであると把握しているからです。
&Rectangleと同様に、selfの直前に&を使用していることにも注意してください。
メソッドは、selfの所有権を奪ったり、ここでしているように不変でselfを借用したり、可変でselfを借用したりできるのです。
他の引数と全く同じですね。
ここで**&selfを選んでいるのは、関数バージョンで&Rectangle**を使用していたのと同様の理由です。
所有権はいらず、構造体のデータを読み込みたいだけで、書き込む必要なはいわけです。
メソッドの一部でメソッドを呼び出したインスタンスを変更したかったら、第一引数に**&mut self**を使用するといいでしょう。
selfだけを第一引数にしてインスタンスの所有権を奪うメソッドを定義することはまれです。
このテクニックは通常、メソッドがselfを何か別のものに変形し、変形後に呼び出し元が元のインスタンスを使用できないようにしたい場合に使用されます。
関数の代替としてメソッドを使う主な利点は、メソッド記法を使用して全メソッドのシグニチャでselfの型を繰り返す必要がなくなる以外だと、体系化です。
コードの将来的な利用者にRectangleの機能を提供しているライブラリ内の各所でその機能を探せるのではなく、この型のインスタンスでできることを1つのimplブロックにまとめあげています。
メソッドの引数
- **読み込み専用(&self)
- 読み込みと書き込み(&mut self)
- 所有権を奪う(self)**
より引数の多いメソッド
Rectangle構造体に2番目のメソッドを実装して、メソッドを使う鍛錬をしましょう。
今回は、Rectangleのインスタンスに別のRectangleのインスタンスを取らせ、2番目のRectangleがselfに完全にはめ込まれたら、trueを返すようにしたいのです。
そうでなければ、falseを返すべきです。
つまり、一旦can_holdメソッドを定義したら、以下のようなプログラムを書けるようになりたいのです。
▼まだ書いていないcan_holdメソッドを使用する
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
let rect2 = Rectangle { width: 10, height: 40 };
let rect3 = Rectangle { width: 60, height: 45 };
// rect1にrect2ははまり込む?
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
そして、予期される出力配下のようになります。
なぜなら、rect2の各寸法はrect1よりも小さいものの、rect3はrect1よりも幅が広いからです。
Can rect1 hold rect2? true
Can rect1 hold rect3? false
メソッドを定義したいことはわかっているので、impl Rectangleブロック内での話しになります。
メソッド名はcan_holdになり、引数として別のRectangleを不変借用で取るでしょう。
メソッドを呼び出すコードを見れば、引数の型が何になるかわかります。
rect1.can_hold(&rect2)は、&rect2(読み込み専用)、Rectangleのインスタンスであるrect2への不変借用を渡しています。
これは道理が通っています。
なぜなら、rect2を読み込む(書き込みではなく。この場合、可変借用が必要になります)だけでよく、can_holdメソッドを呼び出した後にもrect2が使えるよう、所有権をmainに残したままにしたいからです。
can_holdの返り値は、booleanになり、メソッドの中身はselfの幅と高さがもつ1つのRectangleの幅と高さよりも、それぞれ大きいことを確認します。
implブロックに新しいcan_holdメソッドを追記しましょう。
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
メソッドはself引数のあとにシグニチャに追加した引数を複数取ることができ、その引数は、関数の引数と同様に動作するのです。
関連関数(staticメソッド:クラス関数と同じ。クラスインスタンスを介しては呼び出せない)
implブロックの別の有益な機能は、implブロック内にselfを引数に取らない関数を定義できることです。
これは、構造体に関連付けられているので、関連関数と呼ばれます。
それでも、関連関数は関数であり、メソッドではありません。
というのも、対象となる構造体のインスタンスが存在しないからです。
もうString::fromという関連関数を使用したことがありますね。
関連関数は、構造体の新規インスタンスを返すコンストラクタによく使用されます。
例えば、一つの寸歩を引数に取り、長さと幅両方に使用する関連関数を提供することができ、その結果、同じ値を2回指定する必要なく、正方形のRectangleを生成しやすくすることができます。
impl Rectangle {
fn square(size: u32) -> Rectangle(このRectangleは型) {
Rectangle { width: size, height: size }
}
}
この関連関数を呼び出すために、構造体と一緒に「::」記法を使用します。
「::」という記法は、関連関数とモジュールによって作り出される名前空間両方に使用されます。
※staticメソッドとインスタンスメソッドの違い
・staticメソッド
クラスに紐付いた関数のこと。
・インスタンスメソッド
インスタンスに紐付いたメソッドのこと。
それぞれの違いとしては、
staticはthisを使えないけど、インスタンスはthisを使える。
インスタンスメソッドがthisを使えるのは、言語が特別に用意しているから。
staticメソッド(クラスに紐付いた関数)は、クラスをインスタンス化せずに呼び出され、クラスインスタンスを介して呼び出すことはできません!
複数のimplブロック
各構造体には、複数のimplブロックを存在させることができます。
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
ここでこれらのメソッドを個々のimplブロックに分ける理由はないのですが、合法な書き方です。
複数のimplブロックが有用になるケースは第10章で見ますが、そこではジェネリック型とトレイトについて議論します。
まとめ
構造体により、自分の領域で意味のある独自の型を作成することができます。
構造体を使用することで、関連のあるデータ片を相互に結合させたままにし、各部品似名前をつけ、コードを明確にすることができます。
メソッドにより、構造体のインスタンスが行う動作を指定することができ、関連関数により、構造体に特有の機能をインスタンスを利用することなく、名前空間わけすることができます。
しかし、構造体だけが独自の型を作成する手段ではありません。
Rustのenum機能に目を向けて、別の道具を道具箱に追加しましょう。