前回の記事
https://qiita.com/revioness/items/c00ca4258095f08263e7
5. 構造体を使用して関係のあるデータを構造化する
- 構造体はほかの言語でいうクラスに近い役割を果たすもの
5.1 構造体を定義してインスタンス化する
-
公式にはいろいろ書いてあるが、見てもらったほうが早い
struct User { username: String, email: String, sign_in_count: u64, active: bool, }
structを定義してその中に変数と型を定義する
-
インスタンス化
let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, };
インスタンス化する際は各構造体の変数に実際の値を入れてインスタンス化する
参照方法は以下println!("email:{}", user1.email);
この辺はほかの言語と大差ない
-
値の変更
let mut user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, };
今まで何度も言っているように、
let 変数名
で定義したものはイミュータブルになるので、値の変更ができない
なのでmut
をつけてミュータブルにする
構造体のこの変数だけミュータブルに!といったことはできないuser1.username = String::from("test123");
フィールドと変数が同名の時にフィールド初期化省略記法を使う
-
例えば以下のようなコードがあったとする
let mut user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let mut user2 = User { email: String::from("someone2@example.com"), username: String::from("someusername1234"), active: true, sign_in_count: 1, };
activeとsign_in_countは必ずtrueと1が入るとすると、毎回下の2行を入れるのは面倒である
そういう時に使える省略記法が存在するlet mut user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let mut user2 = User { email: String::from("someone2@example.com"), username: String::from("someusername1234"), ..user1 };
このように書くことでactive以降の値をuser1と同じものを入れることができる
異なる型を生成する名前付きフィールドのないタプル構造体を使用する
構造体名により追加の意味を含むものの、フィールドに紐づけられた名前がなく、むしろフィールドの型だけのタプル構造体と呼ばれる、 タプルに似た構造体を定義することもできます。タプル構造体は、構造体名が提供する追加の意味は含むものの、 フィールドに紐付けられた名前はありません; むしろ、フィールドの型だけが存在します。タプル構造体は、タプル全体に名前をつけ、 そのタプルを他のタプルとは異なる型にしたい場合に有用ですが、普通の構造体のように各フィールド名を与えるのは、 冗長、または余計になるでしょう。
この説明だけではわからないかもしれないのでサンプルを元に解説
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
上の二つは構造体としては入っている型はまったく同じものになる
しかし型はColor型とPoint型というものに区別され、まったく別の型として扱うことができる
struct Color {
r: i32,
g: i32,
b: i32,
}
struct Point {
x: i32,
y: i32,
z: i32,
}
let black = Color{
r: 0,
g: 0,
b: 0
};
let origin = Point{
x: 0,
y: 0,
z: 0
};
このように書くことで同じような動作をさせることができるが、これだといちいち構造体を定義してインスタンス化していくことになるので非常に冗長だ
5.2 構造体を使ったプログラム例
以下のようなコードがあるとする
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
}
幅と高さから面積を出力する簡単なプログラムだ
これをタプル
と構造体
で書き換えていく
タプルで書き換える
-
まず上のコードでよくないところはどこか
- それは幅と高さが相互に作用するものなのにまったく関係のない値として扱われていること
- これをタプルで簡単にリファクタリングすると
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 }
このようになるだろう
rect1
として構造体を持ち、最初の要素が幅、2つ目の要素が高さといったように相互に作用する二つの値をグループ化することができる
構造体で書き換える
-
上でタプルで書き換えるということを行ったが、それでもまだ悪いところはある。それは、パッと見で30と50が何を表しているかわかりづらいところだ
- 正直このサンプルコードに関しては最初のパターンかもしくは構造体で書いたほうが冗長にはあるが後から見たときにわかりやすいと思う。では構造体ではどのように書くか
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 }
このように書くと幅と高さを同じ構造体に保存でき、さらにはどれがwidthでどれがheightかすぐにわかり、プログラムの意図がより明瞭になった
トレイトの導出で有用な機能を追加する
-
トレイトに関しては10章で解説されるらしいが、簡単にいうと共通の振る舞いを抽象的に定義できるものらしい
-
先ほどのコードで
Rectangle
クラスの中身をみたいとしよう。ここまでの知識であれば以下のように書くはずだstruct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; // rect1は{}です println!("rect1 is {}", rect1); }
これを実行すると
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display` --> src\main.rs:94:29 | 94 | println!("rect1 is {}", rect1); | ^^^^^ `Rectangle` cannot be formatted with the default formatter | = help: the trait `std::fmt::Display` is not implemented for `Rectangle` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0277`. error: could not compile `own` due to previous error
このようなエラーが出る
- なぜこうなるか?
- println!マクロにはあらかじめ様々なフォーマットが用意されており、
{}
はDisplayとして自動で見やすいように成形されるようになっている。しかし自分で定義した構造体はどうだろうか。 - 構造体は中に様々な値があり、どのように出力するのが適切かが自明でない。そのために
{}
には構造体を整形する機能はない
- println!マクロにはあらかじめ様々なフォーマットが用意されており、
- どうすればいいか
-
以上を踏まえたうえで以下のように書いていく
struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; // rect1は{}です println!("rect1 is {:?}", rect1); }
これだけではエラーになる
error[E0277]: `Rectangle` doesn't implement `Debug` --> src\main.rs:95:35 | 95 | println!("rect1 is {:?}", rect1); | ^^^^^ `Rectangle` cannot be formatted using `{:?}` | = help: the trait `Debug` is not implemented for `Rectangle` = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle` = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) help: consider annotating `Rectangle` with `#[derive(Debug)]` | 86 | #[derive(Debug)] | For more information about this error, try `rustc --explain E0277`. error: could not compile `own` due to previous error
#[derive(Debug)]
これが必要とのこと
{}
の中に:?
という指定子を書くとprintln!
にDebug
という出力整形を使いたい!とお願いすることになる
RustはこれだけではNGで、実際にはその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 }
今はメンバ変数が2つしかないのでこれでもいいが、複雑な構造体になった場合は
:
と?
の間に#
を入れる#[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, }
そのほかのフォーマットについては以下を読むとわかりやすい
-
- なぜこうなるか?
5.3 メソッド記法
- 公式ドキュメントには少し難しく書いてあるが、これは簡単に言うとクラス内メソッドのようなものなので難しく考える必要はない
メソッドを定義する
#[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()
);
}
-
5.2ではarea関数が外出しされていた。普通にプログラミングをやっている人間からすると、当然面積を計算するメソッドはクラスの内部に入れたいものだ。それを実現させるのが上のコードになる
impl Rectangle { fn area(&self) -> u32 { self.width * self.height } }
構造体を定義した上で
impl
で構造体を指して、その中にメソッドを書くことで構造体の中に関数を定義できる(厳密には違うのかもしれないが)
implというのはimplementation; 実装
の意味
構造体で内部変数を定義 → implで内部処理を実装ということになる
第一引数はかならずself
になる
サンプルでは&self
になっているが、これは普通の関数と同じで、面積の計算をするには幅と高さの参照があればいいからだ。
当然構造体の内部変数を変更したい場合はmut self
という引数を取ることになる
より引数の多いメソッド
- 構造体内のメソッドは複数の引数を受け取ることもできる
受け取り方は関数とまったく同じ
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 Rectangle { fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size } } } //呼び出し let sq = Rectangle::square(3);