前の記事
- 【0】 準備 ← 初回
- ...
- 【13】 トレイトまとめ・列挙型・match式 ~最強のトレイトの次は、最強の列挙型~ ← 前回
- 【14】 フィールド付き列挙型とOption型 ~チョクワガタ~ ← 今回
全記事一覧
- 【0】 準備
- 【1】 構文・整数・変数
- 【2】 if・パニック・演習
- 【3】 可変・ループ・オーバーフロー
- 【4】 キャスト・構造体 (たまにUFCS)
- 【5】 バリデーション・モジュールの公開範囲 ~ → カプセル化!~
- 【6】 カプセル化の続きと所有権とセッター ~そして不変参照と可変参照!~
- 【7】 スタック・ヒープと参照のサイズ ~メモリの話~
- 【8】 デストラクタ(変数の終わり)・トレイト ~終わりと始まり~
- 【9】 Orphan rule (孤児ルール)・演算子オーバーロード・derive ~Empowerment 💪 ~
- 【10】 トレイト境界・文字列・Derefトレイト ~トレイトのアレコレ~
- 【11】 Sized トレイト・From トレイト・関連型 ~おもしろトレイトと関連型~
- 【12】 Clone・Copy・Dropトレイト ~覚えるべき主要トレイトたち~
- 【13】 トレイトまとめ・列挙型・match式 ~最強のトレイトの次は、最強の列挙型~
- 【14】 フィールド付き列挙型とOption型 ~チョクワガタ~
- 【15】 Result型 ~Rust流エラーハンドリング術~
- 【16】 Errorトレイトと外部クレート ~依存はCargo.tomlに全部お任せ!~
- 【17】 thiserror・TryFrom ~トレイトもResultも自由自在!~
- 【18】 Errorのネスト・慣例的な書き方 ~Rustらしさの目醒め~
- 【19】 配列・動的配列 ~スタックが使われる配列と、ヒープに保存できる動的配列~
- 【20】 動的配列のリサイズ・イテレータ ~またまたトレイト登場!~
- 【21】 イテレータ・ライフタイム ~ライフタイム注釈ようやく登場!~
- 【22】 コンビネータ・RPIT ~ 「
Iterator
トレイトを実装してるやつ」~ - 【23】
impl Trait
・スライス ~配列の欠片~ - 【24】 可変スライス・下書き構造体 ~構造体で状態表現~
- 【25】 インデックス・可変インデックス ~インデックスもトレイト!~
- 【26】 HashMap・順序・BTreeMap ~Rustの辞書型~
- 【27】 スレッド・'staticライフタイム ~並列処理に見るRustの恩恵~
- 【28】 リーク・スコープ付きスレッド ~ライフタイムに技あり!~
- 【29】 チャネル・参照の内部可変性 ~Rustの虎の子、mpscと
Rc<RefCell<T>>
~ - 【30】 双方向通信・リファクタリング ~返信用封筒を入れよう!~
- 【31】 上限付きチャネル・PATCH機能 ~パンクしないように制御!~
- 【32】
Send
・排他的ロック(Mutex
)・非対称排他的ロック(RwLock
) ~真打Arc<Mutex<T>>
登場~ - 【33】 チャネルなしで実装・Syncの話 ~考察回です~
- 【34】
async fn
・非同期タスク生成 ~Rustの非同期入門~ - 【35】 非同期ランタイム・Futureトレイト ~非同期のお作法~
- 【36】 ブロッキング・非同期用の実装・キャンセル ~ラストスパート!~
- 【37】 Axumでクラサバ! ~最終回~
- 【おまけ1】 Rustで勘違いしていたこと3選 🏄🌴 【100 Exercises To Learn Rust 🦀 完走記事 🏃】
- 【おまけ2】 【🙇 懺悔 🙇】Qiitanグッズ欲しさに1日に33記事投稿した話またはQiita CLIとcargo scriptを布教する的な何か
100 Exercise To Learn Rust 演習第14回になります!
今回の関連ページ
[05_ticket_v2/03_variants_with_data] フィールド持ち列挙子
問題はこちらです。
// TODO: Implement `Ticket::assigned_to`.
// Return the name of the person assigned to the ticket, if the ticket is in progress.
// Panic otherwise.
#[derive(Debug, PartialEq)]
struct Ticket {
title: String,
description: String,
status: Status,
}
#[derive(Debug, PartialEq)]
enum Status {
ToDo,
InProgress { assigned_to: String },
Done,
}
impl Ticket {
pub fn new(title: String, description: String, status: Status) -> Ticket {
if title.is_empty() {
panic!("Title cannot be empty");
}
if title.len() > 50 {
panic!("Title cannot be longer than 50 bytes");
}
if description.is_empty() {
panic!("Description cannot be empty");
}
if description.len() > 500 {
panic!("Description cannot be longer than 500 bytes");
}
Ticket {
title,
description,
status,
}
}
pub fn assigned_to(&self) -> &str {
todo!()
}
}
Ticket
構造体の status: Status
フィールドについて、 InProgress
の時には担当者の名前を返し、それ以外の時はパニックするメソッド assigned_to
を定義してほしいという問題です!
テストを含めた全体
// TODO: Implement `Ticket::assigned_to`.
// Return the name of the person assigned to the ticket, if the ticket is in progress.
// Panic otherwise.
#[derive(Debug, PartialEq)]
struct Ticket {
title: String,
description: String,
status: Status,
}
#[derive(Debug, PartialEq)]
enum Status {
ToDo,
InProgress { assigned_to: String },
Done,
}
impl Ticket {
pub fn new(title: String, description: String, status: Status) -> Ticket {
if title.is_empty() {
panic!("Title cannot be empty");
}
if title.len() > 50 {
panic!("Title cannot be longer than 50 bytes");
}
if description.is_empty() {
panic!("Description cannot be empty");
}
if description.len() > 500 {
panic!("Description cannot be longer than 500 bytes");
}
Ticket {
title,
description,
status,
}
}
pub fn assigned_to(&self) -> &str {
todo!()
}
}
#[cfg(test)]
mod tests {
use super::*;
use common::{valid_description, valid_title};
#[test]
#[should_panic(expected = "Only `In-Progress` tickets can be assigned to someone")]
fn test_todo() {
let ticket = Ticket::new(valid_title(), valid_description(), Status::ToDo);
ticket.assigned_to();
}
#[test]
#[should_panic(expected = "Only `In-Progress` tickets can be assigned to someone")]
fn test_done() {
let ticket = Ticket::new(valid_title(), valid_description(), Status::Done);
ticket.assigned_to();
}
#[test]
fn test_in_progress() {
let ticket = Ticket::new(
valid_title(),
valid_description(),
Status::InProgress {
assigned_to: "Alice".to_string(),
},
);
assert_eq!(ticket.assigned_to(), "Alice");
}
}
解説
match
式を使って、 Status::InProgress { assigned_to }
にマッチするときだけ、 assigned_to
に人の名前を束縛して不変参照として返します。
pub fn assigned_to(&self) -> &str {
match &self.status {
Status::InProgress { assigned_to } => assigned_to,
_ => panic!("Only `In-Progress` tickets can be assigned to someone"),
}
}
今回、新しい要素が2つ出てきました!
- フィールドを持つ列挙体(!): Rustでは、他のプログラム言語ではあまり例を見ない、「別な値を内包している」列挙子を持つ列挙体を定義できます!
- パターンマッチで列挙子を分解し、値を変数に束縛する: 第9回等で地味に登場している機能ですが、列挙体がパターンにマッチする時、そのパターンを利用して分解しながらの束縛ができます!
- 例えばJavaScript等にある分割代入に当たる機能です、モダン!
このようなフィールド持ち列挙子を match
式のパターンマッチで分解する書き方は、今後頻出になります! match
式が本領を発揮してきたというところでしょうか。
継承は不要...?
フィールドを持つ列挙体はかなり強力で、Rustにオブジェクト指向の「継承を用意しなくて良い」理由の一端を担っているのではないかと筆者は考えています。
というのも、まずトレイトがあるのと、トレイトの存在のみでカバーできないシナリオ「似たような挙動をするけど、バリエーションを持たせて、それらの値を例えば同じ動的配列で管理したい(例)」、という場合でも、2, 3通り思いつく方法のうち1つはフィールド付きEnumで実装可能なためです!
- 方法1: トレイトオブジェクトを利用する
- 動的ディスパッチと呼ばれる方法で、詳細は省略します。多分継承がある言語も内部的にはコレ
- 方法2:
Deref
トレイトを活用し、参照の方を保持する- ニュータイプパターンの応用で擬似的に継承を実装可能ですが、かなり複雑になるので非現実的です。
- 方法3: まとめたい構造体をフィールドに持つ列挙型を定義し、その列挙型にメソッドを定義する
継承で表現したいものって、トレイトか、実は結構方法3で済むものがあったりするのです!例えばIPアドレスなら以下のような実装例が思いつきます(まだ紹介していない機能を用いていて申し訳ないです)。
use anyhow::Result;
trait Validate {
fn validation(&self) -> Result<()>;
}
struct IPv4(String);
impl Validate for IPv4 {
fn validation(&self) -> Result<()> {
// ...
Ok(())
}
}
struct IPv6(String);
impl Validate for IPv6 {
fn validation(&self) -> Result<()> {
// ...
Ok(())
}
}
enum IPAddress {
V4(IPv4),
V6(IPv6),
}
impl Validate for IPAddress {
fn validation(&self) -> Result<()> {
use IPAddress::*;
match self {
V4(adrs) => adrs.validation(),
V6(adrs) => adrs.validation(),
}
}
}
ちなみに標準ライブラリでも列挙体を使っています!(上記を書いてから答え合わせで見ましたが、結構一致してた...): https://doc.rust-lang.org/std/net/enum.IpAddr.html
こんな感じで「バリエーションを表現したい」という時は継承は使わずEnumを使うのがRustっぽい書き方じゃないかなぁと思います。
[05_ticket_v2/04_if_let] if let
式 / let else
式
問題はこちらです。
enum Shape {
Circle { radius: f64 },
Square { border: f64 },
Rectangle { width: f64, height: f64 },
}
impl Shape {
// TODO: Implement the `radius` method using
// either an `if let` or a `let/else`.
pub fn radius(&self) -> f64 {
todo!()
}
}
テストを含めた全体
enum Shape {
Circle { radius: f64 },
Square { border: f64 },
Rectangle { width: f64, height: f64 },
}
impl Shape {
// TODO: Implement the `radius` method using
// either an `if let` or a `let/else`.
pub fn radius(&self) -> f64 {
todo!()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_circle() {
let _ = Shape::Circle { radius: 1.0 }.radius();
}
#[test]
#[should_panic]
fn test_square() {
let _ = Shape::Square { border: 1.0 }.radius();
}
#[test]
#[should_panic]
fn test_rectangle() {
let _ = Shape::Rectangle {
width: 1.0,
height: 2.0,
}
.radius();
}
}
if let
式か、 let else
文を使って、 Shape::Circle
の時だけ半径 radius
を返し、それ以外はパニックさせてほしいという問題です。
解説
pub fn radius(&self) -> f64 {
let Shape::Circle { radius } = self else {
panic!("Not Circle!");
};
*radius
}
実は let else
文は筆者が一番好きな構文だったりします。ここで解説するより拙著を読んでほしいです if let
式の方も解説を入れています。
Rustのlet-else文気持ち良すぎだろ #Rust - Qiita
まぁBookでも言っている通り、 match
式と比べると乱用注意です。ここぞというところで書けるとカッコいいですね!
[05_ticket_v2/05_nullability] Option型 (Rust用NULL)
問題はこちらです。
// TODO: Implement `Ticket::assigned_to` using `Option` as the return type.
#[derive(Debug, PartialEq)]
struct Ticket {
title: String,
description: String,
status: Status,
}
#[derive(Debug, PartialEq)]
enum Status {
ToDo,
InProgress { assigned_to: String },
Done,
}
impl Ticket {
pub fn new(title: String, description: String, status: Status) -> Ticket {
if title.is_empty() {
panic!("Title cannot be empty");
}
if title.len() > 50 {
panic!("Title cannot be longer than 50 bytes");
}
if description.is_empty() {
panic!("Description cannot be empty");
}
if description.len() > 500 {
panic!("Description cannot be longer than 500 bytes");
}
Ticket {
title,
description,
status,
}
}
pub fn assigned_to(&self) -> Option<&String> {
todo!()
}
}
テストを含めた全体
// TODO: Implement `Ticket::assigned_to` using `Option` as the return type.
#[derive(Debug, PartialEq)]
struct Ticket {
title: String,
description: String,
status: Status,
}
#[derive(Debug, PartialEq)]
enum Status {
ToDo,
InProgress { assigned_to: String },
Done,
}
impl Ticket {
pub fn new(title: String, description: String, status: Status) -> Ticket {
if title.is_empty() {
panic!("Title cannot be empty");
}
if title.len() > 50 {
panic!("Title cannot be longer than 50 bytes");
}
if description.is_empty() {
panic!("Description cannot be empty");
}
if description.len() > 500 {
panic!("Description cannot be longer than 500 bytes");
}
Ticket {
title,
description,
status,
}
}
pub fn assigned_to(&self) -> Option<&String> {
todo!()
}
}
#[cfg(test)]
mod tests {
use super::*;
use common::{valid_description, valid_title};
#[test]
fn test_todo() {
let ticket = Ticket::new(valid_title(), valid_description(), Status::ToDo);
assert!(ticket.assigned_to().is_none());
}
#[test]
fn test_done() {
let ticket = Ticket::new(valid_title(), valid_description(), Status::Done);
assert!(ticket.assigned_to().is_none());
}
#[test]
fn test_in_progress() {
let ticket = Ticket::new(
valid_title(),
valid_description(),
Status::InProgress {
assigned_to: "Alice".to_string(),
},
);
assert_eq!(ticket.assigned_to(), Some(&"Alice".to_string()));
}
}
先程の問題では InProgress
ではない際にパニックさせてしまっていましたが、ここでRust用のNULLに相当する None
を返すようにしてほしい、という問題です!
解説
パニック部分は None
にし、有効な値になる時は Some(...)
にすれば良さそうです!
pub fn assigned_to(&self) -> Option<&String> {
match &self.status {
Status::InProgress { assigned_to } => Some(assigned_to),
_ => None,
}
}
オプショナル型 Option
は、Bookにも書いてあるように次のような列挙体です。
enum Option<T> {
Some(T),
None
}
それだけです。それ以上でもそれ以下でもありません。 Option
型という型であり、内包している型のメソッドを直接呼べなくなるので最初戸惑いますが、 Option
型に用意されているメソッドに慣れてくればめっちゃシンプルであることに気づくでしょう。
フィールド付き列挙体を使うことで値が「あるか」「ないか」を表現しているだけで、NULL
なんていう特殊な値でも特殊な型でも無いのです。わかりやすい!
直和型
構造体やタプルを数学用語で直積型、そして今回のようなフィールド付き列挙体を直和型と呼んだりするらしいです。
参考: https://zenn.dev/exyrias/articles/d8b56fc900900b4238a9
数学クラスタの人からマサカリが飛んでくるかもしれないですが...この直和型を扱えるという性質はモダンな言語(TSのユニオンとか?)に見られる傾向なんじゃないかなと思います。
似たような機能があったら直和型かも...?言語間で比較してみると面白そうです。
では次の問題に行きましょう!