[Rust 入門] #05 構造体・enum・match — 値のまとめ方と場合分けを動かして確かめる
はじめに
第3回と第4回で、次の形の型を使ってきました。
struct Noisy(&'static str);
&'static str を1つ持つだけの型です。今回は、この struct と、その仲間の enum、そして enum を場合分けする match を扱います。
| 道具 | 役割 |
|---|---|
struct | いくつかの値を、1つにまとめる |
impl | その型に、動作(メソッド)を付ける |
enum | 「いくつかの選択肢の、どれか1つ」を表す |
match | enum などを、場合分けする |
構造体(struct)
名前を付けた値(フィールド)を、まとめます。
struct Item {
name: String,
qty: u32,
}
fn main() {
let mut item = Item {
name: String::from("りんご"),
qty: 3,
};
println!("{} × {}", item.name, item.qty);
item.qty += 2;
println!("{} × {}", item.name, item.qty);
}
りんご × 3
りんご × 5
| 部分 | 意味 |
|---|---|
struct Item { ... } | 型の定義。名前: 型 を並べる |
Item { name: ..., qty: ... } | 値を作る。全部のフィールドを、書く |
item.qty | フィールドを読む・書く |
フィールドを書き換えるには、変数が mut である必要があります。mut なしで書き換えると、エラーです。
error[E0594]: cannot assign to `item.qty`, as `item` is not declared as mutable
--> a2.rs:11:5
|
11 | item.qty += 2;
| ^^^^^^^^^^^^^ cannot assign
|
help: consider changing this to be mutable
|
7 | let mut item = Item {
| +++
タプル構造体
フィールドに名前を付けない形もあります。冒頭の Noisy が、この形です。取り出すときは、.0、.1 と書きます。
この形は、同じ中身でも別の型にするときにも使えます。
struct Meters(f64);
struct Feet(f64);
fn show(m: Meters) {
println!("{} m", m.0);
}
fn main() {
let a = Meters(3.5);
show(a);
let b = Feet(3.5);
show(b);
}
error[E0308]: mismatched types
--> a6.rs:12:10
|
12 | show(b);
| ---- ^ expected `Meters`, found `Feet`
| |
| arguments to this function are incorrect
どちらも中身は f64 ですが、Meters を求める関数に Feet は渡せません。
{:?} で中身を表示する
println!("{:?}", item) と書くと、エラーになります。
error[E0277]: `Item` doesn't implement `Debug`
--> a3.rs:11:22
|
11 | println!("{:?}", item);
| ---- ^^^^ `Item` cannot be formatted using `{:?}` because it doesn't implement `Debug`
|
= note: add `#[derive(Debug)]` to `Item` or manually `impl Debug for Item`
help: consider annotating `Item` with `#[derive(Debug)]`
|
1 + #[derive(Debug)]
2 | struct Item {
|
help のとおり、struct の前に #[derive(Debug)] を付けます。
#[derive(Debug)]
struct Item {
name: String,
qty: u32,
}
Item { name: "りんご", qty: 3 }
Item {
name: "りんご",
qty: 3,
}
1つ目が {:?}、2つ目が {:#?}(整形して表示)です。#[derive(...)] は、「その機能を、自動で付けてください」という指定です。Debug が何なのか(トレイト)は、第7回で扱います。
impl: メソッドと関連関数
型に動作を付けるのが、impl です。
struct Item {
name: String,
qty: u32,
}
impl Item {
fn new(name: &str, qty: u32) -> Item {
Item {
name: String::from(name),
qty,
}
}
fn describe(&self) -> String {
format!("{} × {}", self.name, self.qty)
}
fn add(&mut self, n: u32) {
self.qty += n;
}
fn into_name(self) -> String {
self.name
}
}
fn main() {
let mut item = Item::new("りんご", 3);
println!("{}", item.describe());
item.add(2);
println!("{}", item.describe());
println!("{}", Item::describe(&item));
let name = item.into_name();
println!("{name}");
}
りんご × 3
りんご × 5
りんご × 5
りんご
| 書き方 | 種類 | 意味 |
|---|---|---|
fn new(name: &str, qty: u32) -> Item | 関連関数 | self を取らない。Item::new(...) で呼ぶ |
fn describe(&self) | メソッド | self を借りる(第4回の &)。読むだけ |
fn add(&mut self, n: u32) | メソッド | self を可変で借りる(&mut)。書き換える |
fn into_name(self) | メソッド | self の所有権を受け取る(第3回のムーブ) |
第3回・第4回の内容が、そのまま、self の書き方になっています。
qty,は、qty: qty,の省略です(同名の変数があるとき)item.describe()とItem::describe(&item)は、同じ結果になりましたnewは、ただの関数名です。fn make(...) -> Selfと書き換えても、動きました(型の名前の代わりにSelfとも書けます)
self を取るメソッドを呼ぶと、第3回と同じく、呼んだ後にその変数は使えません。
let item = Item::new("りんご", 3);
let name = item.into_name();
println!("{name} {}", item.qty);
error[E0382]: borrow of moved value: `item`
--> a8.rs:22:27
|
20 | let item = Item::new("りんご", 3);
| ---- move occurs because `item` has type `Item`, which does not implement the `Copy` trait
21 | let name = item.into_name();
| ----------- `item` moved due to this method call
22 | println!("{name} {}", item.qty);
| ^^^^^^^^ value borrowed here after move
|
note: `Item::into_name` takes ownership of the receiver `self`, which moves `item`
構造体のムーブとコピー
String を持たない、整数だけの構造体でも、そのままではムーブされます。
struct P {
x: i32,
y: i32,
}
fn main() {
let p = P { x: 1, y: 2 };
let q = p;
println!("{} {}", q.x, q.y);
println!("{}", p.x);
}
error[E0382]: borrow of moved value: `p`
--> a9.rs:10:20
|
7 | let p = P { x: 1, y: 2 };
| - move occurs because `p` has type `P`, which does not implement the `Copy` trait
8 | let q = p;
| - value moved here
第3回では、整数やタプルが Copy でした。自分で作った型は、そのままでは Copy になりません。 #[derive(Clone, Copy)] を付けると、コピーされるようになります。
#[derive(Clone, Copy)]
struct P {
x: i32,
y: i32,
}
fn main() {
let p = P { x: 1, y: 2 };
let q = p;
println!("{} {}", q.y, p.x);
}
2 1
Clone と Copy の中身も、第7回で扱います。
フィールドは、宣言した順に drop される
第3回の Noisy を、フィールドにして、drop の順番を見ます。
struct Pair {
first: Noisy,
second: Noisy,
}
fn main() {
let p = Pair {
second: Noisy("second"),
first: Noisy("first"),
};
println!("{} と {}", p.first.0, p.second.0);
println!("main の終わり");
}
(Noisy と impl Drop は、第3回と同じです)
first と second
main の終わり
drop: first
drop: second
値を作った順は second、first でしたが、drop されたのは、構造体の定義でフィールドを書いた順(first、second)でした。
Java と並べる(struct と class)
| 場面 | Java | Rust |
|---|---|---|
| 値をまとめる | class Item { String name; int qty; } | struct Item { name: String, qty: u32 } |
| 動作を付ける | クラスの中にメソッドを書く | impl Item { ... } に、別に書く |
b = a | 参照のコピー。a も b も同じ物を指す | ムーブ(Copy でない型の場合)。a は使えない |
Java で、Item b = a; b.qty += 2; を実行すると、a.qty も変わります(a == b は true)。
5 true
Rust の let b = a; は、第3回のとおり、a を使えなくします。
enum
「いくつかの選択肢の、どれか1つ」を表すのが、enum です。場合分けには、match を使います。
enum Direction {
North,
East,
South,
West,
}
fn turn_right(d: Direction) -> Direction {
match d {
Direction::North => Direction::East,
Direction::East => Direction::South,
Direction::South => Direction::West,
Direction::West => Direction::North,
}
}
fn name(d: &Direction) -> &'static str {
match d {
Direction::North => "北",
Direction::East => "東",
Direction::South => "南",
Direction::West => "西",
}
}
fn main() {
let mut d = Direction::North;
for _ in 0..5 {
print!("{} ", name(&d));
d = turn_right(d);
}
println!();
}
北 東 南 西 北
| 部分 | 意味 |
|---|---|
enum Direction { North, ... } | 選択肢(バリアント)を並べる |
Direction::North | 値の作り方。型名::バリアント名 |
match d { 模様 => 値, ... } | d を、上から順に模様と比べ、合ったものの右側の値になる |
match は、式です(第2回)。=> の右側の値が、match 全体の値になります。
全部の場合を書かないと、通らない
バリアントを1つ書き忘れると、コンパイルが止まります。
fn name(d: &Direction) -> &'static str {
match d {
Direction::North => "北",
Direction::East => "東",
Direction::South => "南",
}
}
error[E0004]: non-exhaustive patterns: `&Direction::West` not covered
--> b2.rs:9:11
|
9 | match d {
| ^ pattern `&Direction::West` not covered
|
note: `Direction` defined here
--> b2.rs:1:6
|
1 | enum Direction {
| ^^^^^^^^^
...
5 | West,
| ---- not covered
West が、書かれていない、と指摘しています。整数の match でも、同じです。
let n = 3;
let s = match n {
1 => "one",
2 => "two",
};
error[E0004]: non-exhaustive patterns: `i32::MIN..=0_i32` and `3_i32..=i32::MAX` not covered
この検査があるので、あとで enum にバリアントを足すと、match を直すべき場所が、コンパイルエラーとして出ます。
Java の switch と並べる
Java(21)で、同じ形を書いて比べます。
enum Direction { NORTH, EAST, SOUTH, WEST }
static String name(Direction d) {
String s = "?";
switch (d) {
case NORTH: s = "北"; break;
case EAST: s = "東"; break;
case SOUTH: s = "南"; break;
}
return s;
}
WEST を渡すと、コンパイルは通り、? が返ります。switch 文は、全部の場合を書いているか、検査されません。
switch を式にすると、検査が効きます。
static String name(Direction d) {
return switch (d) {
case NORTH -> "北";
case EAST -> "東";
case SOUTH -> "南";
};
}
J2.java:5: error: the switch expression does not cover all possible input values
return switch (d) {
^
Rust の match は、常に、この検査がかかります。
データを持つ enum
Rust の enum は、バリアントごとに、別の形のデータを持てます。
enum Shape {
Circle(f64),
Rect { w: f64, h: f64 },
Point,
}
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rect { w, h } => w * h,
Shape::Point => 0.0,
}
}
fn main() {
let shapes = [Shape::Circle(1.0), Shape::Rect { w: 2.0, h: 3.0 }, Shape::Point];
for s in &shapes {
println!("{:.3}", area(s));
}
println!("Shape = {} バイト", std::mem::size_of::<Shape>());
}
3.142
6.000
0.000
Shape = 24 バイト
| バリアント | 持つデータ |
|---|---|
Circle(f64) | 半径を1つ(タプル構造体と同じ形) |
Rect { w, h } | 幅と高さ(名前付きフィールド) |
Point | なし |
match の模様で、データを取り出して、名前を付けられます(Shape::Circle(r) の r)。match の対象が参照(&Shape)なので、取り出した r、w、h も、借りた値です。
同じ形を Java で書くと、sealed interface と record になります。
sealed interface Shape permits Circle, Rect, Point {}
record Circle(double r) implements Shape {}
record Rect(double w, double h) implements Shape {}
record Point() implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle c -> Math.PI * c.r() * c.r();
case Rect r -> r.w() * r.h();
case Point p -> 0.0;
};
}
3.142
6.000
0.000
Point の行を消すと、Java も、the switch expression does not cover all possible input values で止まりました。
| Java(21) | Rust | |
|---|---|---|
| 型の宣言 | sealed interface と record を、3つ並べる | enum 1つ |
| 場合分け | switch 式(パターンマッチ) | match |
| 書き忘れの検査 | 式のときだけ | 常に |
大きさも測りました。データを持たない Direction は 1 バイト、Shape は 24 バイトでした。Rect が、f64 を2つで 16 バイトです。
match の中で起きる所有権の移動
String を持つバリアントを、match で取り出すと、第3回のムーブが起きます。
enum Cmd {
Add { name: String, qty: u32 },
Clear,
}
fn main() {
let cmd = Cmd::Add { name: String::from("りんご"), qty: 2 };
match cmd {
Cmd::Add { name, qty } => println!("追加 {name} {qty}"),
Cmd::Clear => println!("クリア"),
}
match cmd {
Cmd::Add { .. } => println!("また追加"),
Cmd::Clear => println!("クリア"),
}
}
error[E0382]: use of partially moved value: `cmd`
--> b9.rs:12:11
|
9 | Cmd::Add { name, qty } => println!("追加 {name} {qty}"),
| ---- value partially moved here
...
12 | match cmd {
| ^^^ value used here after partial move
|
= note: partial move occurs because value has type `String`, which does not implement the `Copy` trait
1つ目の match で、name(String)が、cmd から取り出されてムーブしました。qty(u32)は Copy なので、問題になりません。
match &cmd と、参照にして借りれば、通ります。
match &cmd {
Cmd::Add { name, qty } => {
let n: &String = name;
println!("追加 {n} {qty}");
}
Cmd::Clear => println!("クリア"),
}
match &cmd {
Cmd::Add { .. } => println!("また追加"),
Cmd::Clear => println!("クリア"),
}
追加 りんご 2
また追加
name の型は、&String になります(let n: &String = name; が通ることで確認)。第4回の「借りるか、取るか」が、match でも同じように働きます。
match のいろいろ
整数にも、範囲や条件を付けて使えます。
fn grade(score: u32) -> &'static str {
match score {
90..=100 => "A",
70..=89 => "B",
0 | 1 | 2 => "ほぼゼロ",
n if n < 40 => "要注意",
_ => "C",
}
}
100 -> A
90 -> A
89 -> B
70 -> B
69 -> C
39 -> 要注意
1 -> ほぼゼロ
0 -> ほぼゼロ
| 模様 | 意味 |
|---|---|
90..=100 | 90 以上 100 以下(..= は、終わりを含む) |
0 | 1 | 2 | どれか |
n if n < 40 | 値を n と呼び、n < 40 のときだけ合う(ガード) |
_ | 何でも。残り全部 |
模様は、上から順に比べられます。1 は n if n < 40 にも合いますが、先に 0 | 1 | 2 に合うので、「ほぼゼロ」でした。
=> の右側は、全部同じ型でなければなりません。
let s = match n {
1 => "one",
2 => 2,
_ => "many",
};
error[E0308]: `match` arms have incompatible types
--> b6.rs:5:14
|
4 | 1 => "one",
| ----- this is found to be of type `&str`
5 | 2 => 2,
| ^ expected `&str`, found integer
1つだけ調べる: if let
1つのバリアントだけを調べたいときは、if let が短く書けます。
enum Cmd {
Add(u32),
Clear,
}
fn main() {
for cmd in [Cmd::Add(5), Cmd::Clear] {
if let Cmd::Add(n) = cmd {
println!("追加 {n}");
} else {
println!("追加ではない");
}
}
}
追加 5
追加ではない
if let 模様 = 値 { ... } は、値が模様に合ったときだけ、中を実行します。match と違って、残りの場合を書かなくても、通ります。
サンプル: 買い物リストを、struct と enum で書き直す
第4回の買い物リストを、品物(struct)、種類(enum)、操作(データを持つ enum)で書き直します。
cargo new shop3
cd shop3
src/main.rs を、次のようにします。
enum Category {
Fruit,
Drink,
Other,
}
struct Item {
name: String,
qty: u32,
category: Category,
bought: bool,
}
impl Item {
fn new(name: &str, qty: u32, category: Category) -> Item {
Item {
name: String::from(name),
qty,
category,
bought: false,
}
}
fn describe(&self) -> String {
let mark = if self.bought { "済" } else { "未" };
let kind = match self.category {
Category::Fruit => "果物",
Category::Drink => "飲み物",
Category::Other => "その他",
};
format!("[{mark}] {} × {}({kind})", self.name, self.qty)
}
}
enum Command {
Add {
name: String,
qty: u32,
category: Category,
},
Buy(usize),
Clear,
}
fn apply(list: &mut Vec<Item>, cmd: Command) {
match cmd {
Command::Add {
name,
qty,
category,
} => list.push(Item::new(&name, qty, category)),
Command::Buy(i) => list[i].bought = true,
Command::Clear => list.clear(),
}
}
fn print_list(list: &[Item]) {
if list.is_empty() {
println!("(空)");
}
for item in list {
println!("{}", item.describe());
}
println!("---");
}
fn main() {
let mut list = Vec::new();
let commands = [
Command::Add {
name: String::from("りんご"),
qty: 3,
category: Category::Fruit,
},
Command::Add {
name: String::from("お茶"),
qty: 2,
category: Category::Drink,
},
Command::Add {
name: String::from("電池"),
qty: 1,
category: Category::Other,
},
Command::Buy(1),
];
for cmd in commands {
apply(&mut list, cmd);
}
print_list(&list);
apply(&mut list, Command::Clear);
print_list(&list);
}
cargo run
[未] りんご × 3(果物)
[済] お茶 × 2(飲み物)
[未] 電池 × 1(その他)
---
(空)
---
第4回の版との違いは、次のとおりです。
| 第4回 | 今回 |
|---|---|
品物は String | 名前・個数・種類・買ったかを持つ struct Item |
| 品物を足す関数だけ | 「足す・買う・空にする」を、enum Command で表す |
| 操作ごとに、関数を呼び分ける | apply の中の match で、操作ごとに場合分けする |
apply の match cmd は、cmd を値で受けています。Command::Add の name は、ここでムーブされ、Item::new(&name, ...) には、借りて渡しています(第4回の &String から &str)。for cmd in commands で、配列の要素が1つずつ、apply にムーブされます(第3回の for s in v)。
Command::Buy(i) の list[i] は、範囲の外を指すと、実行時に止まります。
thread 'main' panicked at c1.rs:4:9:
index out of bounds: the len is 3 but the index is 5
(終了コード 101。要素3つのリストに、list[5] を指定した場合)失敗を、止めずに扱う方法は、第6回です。
cargo clippy と cargo fmt --check は、指摘なしで通りました。
歴史
| 年 | できごと |
|---|---|
| 2006〜2011年 | Rust の最初のコンパイラは、OCaml で書かれた(約38,000行) |
| 2012年〜 | コンパイラは、Rust 自身で書かれるようになった(自己ホスト)。切り替えの正確な日付は、不明とされている |
OCaml は、関数型言語の1つです。Wikipedia の Rust の記事は、enum を、「関数型言語にある代数的データ型に似た機能を持つ」ものと説明しています。また、Rust の機能の多くが、OCaml などの関数型言語から影響を受けた、とも書いています。
動作確認
確認できたこと
- 上のコードと出力は、すべて、Linux(Rust 1.97.0、Cargo 1.97.0、edition 2024)で、実際に実行した結果です
- Java の
switch、sealed interface、recordのコードは、同じ環境の Java(OpenJDK 21)で、実際に実行した結果です - 9つのエラー(
E0594、E0308を2種類の形、E0277、E0382を3種類の形、E0004を2種類の形)のメッセージ #[derive(Debug)]を付けると{:?}と{:#?}で表示できること- 自作の
structは、整数だけでも、そのままではムーブされ、#[derive(Clone, Copy)]でコピーになること - フィールドの
dropは、定義で書いた順になること(作った順と逆にして確認) newは、ただの名前であること(makeに変えても動く)- Java の
switch文は、WESTを書き忘れても、コンパイルが通り?を返すこと。switch式は、コンパイルエラーになること enumにバリアントを足すと、既存のmatchがE0004になること(Upを足して確認)match cmdはStringをムーブし、match &cmdは借りること- 買い物リストのサンプルが、
cargo runで動くこと。cargo clippyとcargo fmt --checkが、指摘なしで通ること
歴史の出典
- 最初のコンパイラが OCaml で書かれたこと(約38,000行、2006〜2011年)、自己ホストへの切り替え日が不明とされていること、
enumが代数的データ型に似ていること、OCaml などから影響を受けたことは、Wikipedia の「Rust (programming language)」の記述です。1つの出典で、ページの全文ではなく、取得ツールが返した要約を通して確認しました。他の資料とは、突き合わせていません
確認できていないこと
Shapeが 24 バイトになる内訳(バリアントを区別する印がどう置かれるか)。Rectが 16 バイトで、全体が 24 バイトという測定結果だけを書いていますDirectionが 1 バイトになる理由- Rust の
enumの設計が、OCaml など、どの言語から、どの程度引き継がれたか。Wikipedia の記述以上のことは、確かめていません - 自作の型を、フィールドごとに
mutにできるかどうか。この記事では、変数全体がmutでないと書き換えられない場合のエラーだけを確認しました - Windows と macOS での実行結果。出力の内容は、同じになるはずですが、実行はしていません
次回
次回は、Result / Option とエラー処理です。今回の match の例にあった「範囲外を指すと止まる」問題を、止めずに扱う方法を見ます。Option は、Rust の標準ライブラリにある enum で、今回の match と同じ書き方で場合分けできます(Some(3) と None の match で、None を書き忘れると、今回と同じ E0004 になることを確認しました)。
連載の目次(予定)
入門編(8回)
- Rust とは / 環境構築 / Hello World
- 変数・型・関数(Java との対比)
- 所有権(ムーブ)
- 借用と参照
- 構造体・enum・match(今回)
- Result / Option とエラー処理
- トレイトとジェネリクス(Java の interface との違い)
- ライフタイム入門と、総まとめの CLI(ファイル一覧ツール)