前の記事
- 【0】 準備 ← 初回
- ...
- 【4】 キャスト・構造体 (たまにUFCS) ← 前回
- 【5】 バリデーション・モジュールの公開範囲 → カプセル化! ← 今回
全記事一覧
- 【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 演習第5回になります!
記事投稿を溜めすぎてしまったため、本回から解説を軽くできるところは軽くしていく所存です、ご了承ください
今回の関連ページ
では早速参りましょう!
[03_ticket_v1/02_validation] バリデーション
問題はこちらです。
struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
// TODO: implement the `new` function.
// The following requirements should be met:
// - Only `To-Do`, `In Progress`, and `Done` statuses are allowed.
// - The `title` and `description` fields should not be empty.
// - the `title` should be at most 50 bytes long.
// - the `description` should be at most 500 bytes long.
// The method should panic if any of the requirements are not met.
//
// You'll have to use what you learned in the previous exercises,
// as well as some `String` methods. Use the documentation of Rust's standard library
// to find the most appropriate options -> https://doc.rust-lang.org/std/string/struct.String.html
fn new(title: String, description: String, status: String) -> Self {
todo!();
Self {
title,
description,
status,
}
}
}
テストを含めた全体
struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
// TODO: implement the `new` function.
// The following requirements should be met:
// - Only `To-Do`, `In Progress`, and `Done` statuses are allowed.
// - The `title` and `description` fields should not be empty.
// - the `title` should be at most 50 bytes long.
// - the `description` should be at most 500 bytes long.
// The method should panic if any of the requirements are not met.
//
// You'll have to use what you learned in the previous exercises,
// as well as some `String` methods. Use the documentation of Rust's standard library
// to find the most appropriate options -> https://doc.rust-lang.org/std/string/struct.String.html
fn new(title: String, description: String, status: String) -> Self {
todo!();
Self {
title,
description,
status,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use common::{overly_long_description, overly_long_title, valid_description, valid_title};
#[test]
#[should_panic(expected = "Title cannot be empty")]
fn title_cannot_be_empty() {
Ticket::new("".into(), valid_description(), "To-Do".into());
}
#[test]
#[should_panic(expected = "Description cannot be empty")]
fn description_cannot_be_empty() {
Ticket::new(valid_title(), "".into(), "To-Do".into());
}
#[test]
#[should_panic(expected = "Title cannot be longer than 50 bytes")]
fn title_cannot_be_longer_than_fifty_chars() {
Ticket::new(overly_long_title(), valid_description(), "To-Do".into());
}
#[test]
#[should_panic(expected = "Description cannot be longer than 500 bytes")]
fn description_cannot_be_longer_than_500_chars() {
Ticket::new(valid_title(), overly_long_description(), "To-Do".into());
}
#[test]
#[should_panic(expected = "Only `To-Do`, `In Progress`, and `Done` statuses are allowed")]
fn status_must_be_valid() {
Ticket::new(valid_title(), valid_description(), "Funny".into());
}
#[test]
fn done_is_allowed() {
Ticket::new(valid_title(), valid_description(), "Done".into());
}
#[test]
fn in_progress_is_allowed() {
Ticket::new(valid_title(), valid_description(), "In Progress".into());
}
}
不正なデータがプログラムに混入しないようにする、いわゆる「バリデーション」の問題ですね。問題指示を和訳要約するとこんな感じです。
- TODO:
new
する関数を作成してください! - 以下を守ってね:
- ステータスは
To-Do
,In Progress
,Done
のみが許されます! -
title
とdescription
について空文字は許されません。 -
title
は50文字以下 -
description
は500文字以下
- ステータスは
-
new
は上記を満たしていない時パニックさせてください。 -
String
型に関するドキュメントを読むと良いことあるよ! -> https://doc.rust-lang.org/std/string/struct.String.html
解説
struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
fn new(title: String, description: String, status: String) -> Self {
// Result型使いたい...!!
// Only `To-Do`, `In Progress`, and `Done` statuses are allowed.
// Enum 使いたい...!!
if status != "To-Do" && status != "In Progress" && status != "Done" {
panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
}
// The `title` and `description` fields should not be empty.
if title.is_empty() {
panic!("Title cannot be empty");
}
if description.is_empty() {
panic!("Description cannot be empty");
}
// the `title` should be at most 50 bytes long.
if title.len() > 50 {
panic!("Title cannot be longer than 50 bytes");
}
// the `description` should be at most 500 bytes long.
if description.len() > 500 {
panic!("Description cannot be longer than 500 bytes");
}
Self {
title,
description,
status,
}
}
}
コメントにところどころお気持ちが漏れていますが、この問題はRustに慣れている人からすると改善したい点が2つあります...!しかし100 Exercisesでは後ほど紹介されるため今は我慢...ちなみに同じチケット管理システムのプログラムでちゃんと改善します。
- Result型を使いたい! -> 第15回 5.6. Fallibility
- パニックは「発生箇所がわかりにくくなる」「一度発生してしまうとコントロールしにくい」という理由よりなるべく避けたいものです...!
- Enumを使いたい! -> 第13回 5.1. Enums
- 「特定の値しか取らないフィールド」はRustでは列挙型(Enum)の枕詞です。こちらも後ほど登場します!
どの回であったかは覚えていませんが、エクササイズ本編の方でも「後でもっと良い方法に改善します」と書かれており、「とりあえず手を動かして覚える」という方針で設計されているのだということがわかりますね。
今回は if
式により単に期待する条件を満たしていない時にパニックさせるようにしています。 is_empty
という専用メソッドを使ったりしていますが、この手のメソッドはセマンティックな感じがして好きです!(セマンティックって言いたいだけ) ちなみにここで title.len() == 0
みたいに書くとclippy辺りが「 is_empty
使ったほうが良いですよ!」と親切に教えてくれたりします。
new
は普通のメソッド!
Rustでも構造体のコンストラクタは今回のように new
メソッドとして定義しますが、あくまでも慣例であり new
はキーワードになっていません! コンストラクタという厄介な要素ではなく、ただのメソッドだから気軽に呼べる良さがあります!
ちなみにただのメソッドなので、 new
があろうがなかろうが今まで通り直接フィールドを指定する形で構造体のいわゆるインスタンスを作成できます。え?じゃあどうやって不正な値の生成を防ぐかって?それは次のエクササイズです!
[03_ticket_v1/03_modules] モジュール
問題はこちらです。
mod helpers {
// TODO: Make this code compile, either by adding a `use` statement or by using
// the appropriate path to refer to the `Ticket` struct.
fn create_todo_ticket(title: String, description: String) -> Ticket {
Ticket::new(title, description, "To-Do".into())
}
}
struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
fn new(title: String, description: String, status: String) -> 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");
}
if status != "To-Do" && status != "In Progress" && status != "Done" {
panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
}
Ticket {
title,
description,
status,
}
}
}
「 use
ステートメントを使用するか、適切なパスを設定するを設定することでコンパイルを通してね!」という問題です。100 Exercisesでは「コンパイルを通してね!」問題がしばしば出るみたいですね。
解説
mod helpers {
// TODO: Make this code compile, either by adding a `use` statement or by using
// the appropriate path to refer to the `Ticket` struct.
+ use super::Ticket;
fn create_todo_ticket(title: String, description: String) -> Ticket {
Ticket::new(title, description, "To-Do".into())
}
}
本問題は先のエクササイズの最後に言及した不正な値の生成を防ぐための書き方、すなわち「 new
メソッドで検査せずにそれ以外の方法で構造体インスタンスを生成することを防ぐ方法」の布石になります。
プログラムを適切な「モジュール」単位に分割することで、モジュール外からのアクセスについて制限をかけようというわけです。他言語でいう「 名前空間 」に該当するものなのでそこまで複雑ではないかと思います。
use
文は他のモジュールで定義されている要素を"短く"書ける機能と考えておくとわかりやすいです。つまり、上記の場合使うたびに super::Ticket
と書いても良いところを、 Ticket
と書いても通じるようにする以上の意味(つまり import
みたいな意味)は持ちません。ちなみに as
を使うことでエイリアスをつけることも可能です。
mod helpers {
use super::Ticket as MyTicket;
fn create_todo_ticket(title: String, description: String) -> MyTicket {
MyTicket::new(title, description, "To-Do".into())
}
}
また、 use
文は、関数の中など結構意外な場所に書けます。どこに書けるか探ってみるのも面白いかもしれません。
[03_ticket_v1/04_visibility] use
の有効範囲
問題はこちらです。
mod ticket {
struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
fn new(title: String, description: String, status: String) -> 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");
}
if status != "To-Do" && status != "In Progress" && status != "Done" {
panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
}
Ticket {
title,
description,
status,
}
}
}
}
// TODO: **Exceptionally**, you'll be modifying both the `ticket` module and the `tests` module
// in this exercise.
#[cfg(test)]
mod tests {
// TODO: Add the necessary `pub` modifiers in the parent module to remove the compiler
// errors about the use statement below.
use super::ticket::Ticket;
// Be careful though! We don't want this function to compile after you have changed
// visibility to make the use statement compile!
// Once you have verified that it indeed doesn't compile, comment it out.
fn should_not_be_possible() {
let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());
// You should be seeing this error when trying to run this exercise:
//
// error[E0616]: field `description` of struct `Ticket` is private
// |
// | assert_eq!(ticket.description, "A description");
// | ^^^^^^^^^^^^^^^^^^
//
// TODO: Once you have verified that the below does not compile,
// comment the line out to move on to the next exercise!
assert_eq!(ticket.description, "A description");
}
fn encapsulation_cannot_be_violated() {
// This should be impossible as well, with a similar error as the one encountered above.
// (It will throw a compilation error only after you have commented the faulty line
// in the previous test - next compilation stage!)
//
// This proves that `Ticket::new` is now the only way to get a `Ticket` instance.
// It's impossible to create a ticket with an illegal title or description!
//
// TODO: Once you have verified that the below does not compile,
// comment the lines out to move on to the next exercise!
let ticket = Ticket {
title: "A title".into(),
description: "A description".into(),
status: "To-Do".into(),
};
}
}
テストの方に色々書いていますが、要約するとモジュール分割によって
-
new
を使う方法以外での構造体インスタンス生成はできなくなったよ! - もうフィールドに直接アクセスすることはできなくなったよ!
という旨が書かれています。
解説
先程の問題で普通に new
メソッドにアクセスできていたのはアクセス先が「親」モジュールだったからでした。子モジュールや親子関係にないモジュールの要素(構造体のフィールドも含みます!)にアクセスするには pub
をつける必要があります!
mod ticket {
pub struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
- fn new(title: String, description: String, status: String) -> Ticket {
+ pub fn new(title: String, description: String, status: String) -> Ticket {
// ...
}
}
}
これで let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());
の部分はコンパイルが通るようになります。他の行は構造体のプライベートフィールドにアクセスするせいでそもそもコンパイルが通らなくなるので、コメントアウトしましょう。
このように書くと他のモジュールはチケットのフィールドに直接アクセスできなくなってしまいますが、ではどうするかというと他のOOP言語同様、ゲッター を設けます!それは後のエクササイズのようです。
では次の問題に行きましょう!