前の記事
- 【0】 準備 ← 初回
- ...
- 【5】 バリデーション・モジュールの公開範囲 ~ → カプセル化!~ ← 前回
- 【6】 カプセル化の続きと所有権とセッター ~そして不変参照と可変参照!~ ← 今回
全記事一覧
- 【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 演習第6回になります!
今回の関連ページ
[03_ticket_v1/05_encapsulation] カプセル化
問題はこちらです。
pub mod ticket {
pub struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
pub fn new(title: String, description: String, status: String) -> Ticket {
// 省略
}
// TODO: Add three public methods to the `Ticket` struct:
// - `title` that returns the `title` field.
// - `description` that returns the `description` field.
// - `status` that returns the `status` field.
}
}
テストを含めた全体
pub mod ticket {
pub struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
pub 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: Add three public methods to the `Ticket` struct:
// - `title` that returns the `title` field.
// - `description` that returns the `description` field.
// - `status` that returns the `status` field.
}
}
#[cfg(test)]
mod tests {
use super::ticket::Ticket;
#[test]
fn description() {
let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());
assert_eq!(ticket.description(), "A description");
}
#[test]
fn title() {
let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());
assert_eq!(ticket.title(), "A title");
}
#[test]
fn status() {
let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());
assert_eq!(ticket.status(), "To-Do");
}
}
前回保留になっていたゲッターを設ける問題ですね...!
解説
impl Ticket {
pub fn new(title: String, description: String, status: String) -> Ticket {
// 省略
}
pub fn title(self) -> String {
self.title
}
pub fn description(self) -> String {
self.description
}
pub fn status(self) -> String {
self.status
}
}
手動実装の必要がありますが(deriveマクロとか探せばありそう...?まぁ要らないことが多い...)、同じモジュール内ならフィールドにアクセスできるわけですから、フィールドにアクセスするメソッドだけを公開してしまえばよいのです。
外部からフィールドに値をセットすることはできないので、これで目的は達成されます!
...まぁRust名物の 所有権的な問題が残っている のですが...それは次のエクササイズになります!
[03_ticket_v1/06_ownership] 所有権
問題はこちらです。
// TODO: based on what we just learned about ownership, it sounds like immutable references
// are a good fit for our accessor methods.
// Change the existing implementation of `Ticket`'s accessor methods take a reference
// to `self` as an argument, rather than taking ownership of it.
pub struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
pub fn new(title: String, description: String, status: String) -> Ticket {
// 省略
}
pub fn title(self) -> String {
self.title
}
pub fn description(self) -> String {
self.description
}
pub fn status(self) -> String {
self.status
}
}
テストを含めた全体
// TODO: based on what we just learned about ownership, it sounds like immutable references
// are a good fit for our accessor methods.
// Change the existing implementation of `Ticket`'s accessor methods take a reference
// to `self` as an argument, rather than taking ownership of it.
pub struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
pub 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,
}
}
pub fn title(self) -> String {
self.title
}
pub fn description(self) -> String {
self.description
}
pub fn status(self) -> String {
self.status
}
}
#[cfg(test)]
mod tests {
use super::Ticket;
#[test]
fn works() {
let ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());
// If you change the signatures as requested, this should compile:
// we can call these methods one after the other because they borrow `self`
// rather than taking ownership of it.
assert_eq!(ticket.title(), "A title");
assert_eq!(ticket.description(), "A description");
assert_eq!(ticket.status(), "To-Do");
}
}
「所有権を考慮すると不変参照を使って書いたほうが良さそうなので書き直しましょう!」という問題です。つまり先の回答はRustを多少知っている人なら恣意的なものであったことがわかるわけです。
所有権についてめっちゃ解説したい気持ちがありますが、ここはBookの方をぜひ読んでほしいなと思うのであえてしません。
解説
不変参照でアクセスするように変えましょう!
impl Ticket {
pub fn new(title: String, description: String, status: String) -> Ticket {
// 省略
}
- pub fn title(self) -> String {
+ pub fn title(&self) -> &String {
- self.title
+ &self.title
}
// 以降も同様の改変
pub fn description(&self) -> &String {
&self.description
}
pub fn status(&self) -> &String {
&self.status
}
}
ちなみに ticket.title()
なども本来なら (&ticket).title()
などに直さないといけないように感じますが、不変参照や可変参照で構造体にアクセスするのはRustではかなり頻出なパターンなので、忖度してもらえるため書き直す必要はなかったりします。便利
所有権及び不変参照への所感。よく「所有権が難しい」と聞きますが、Rustに慣れてくると他言語の仕様の方が(難しいというよりは)不安になってきます。え?だって 所有権がなかったら確保したリソースがどんなタイミングで解放されてNULLになるかコード「全部」を読まないとわからない じゃないですか...!?それぐらいなら所有権と参照を基軸にして確実にアクセスできることがコンパイル時点で確定してくれている方が数億倍わかりやすいです。Unityの並行処理みたいなソースコードでN敗した末に所有権をとてもありがたい存在だと思うようになったのでした1。
小規模コードでスタックしか使われないような数値計算しかしないなら、参照という概念は省いて全部クローンされるような言語の方が読みやすい気もしますが、所有権や不変参照は慣れてしまえば全然読むのが苦じゃなくなるので、結局好みに終始しそうです。
[03_ticket_v1/07_setters] 可変参照とセッター
問題はこちらです。
// TODO: Add &mut-setters to the `Ticket` struct for each of its fields.
// Make sure to enforce the same validation rules you have in `Ticket::new`!
// Even better, extract that logic and reuse it in both places. You can use
// private functions or private static methods for that.
pub struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
pub 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,
}
}
pub fn title(&self) -> &String {
&self.title
}
pub fn description(&self) -> &String {
&self.description
}
pub fn status(&self) -> &String {
&self.status
}
}
テストを含めた全体
// TODO: Add &mut-setters to the `Ticket` struct for each of its fields.
// Make sure to enforce the same validation rules you have in `Ticket::new`!
// Even better, extract that logic and reuse it in both places. You can use
// private functions or private static methods for that.
pub struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
pub 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,
}
}
pub fn title(&self) -> &String {
&self.title
}
pub fn description(&self) -> &String {
&self.description
}
pub fn status(&self) -> &String {
&self.status
}
}
#[cfg(test)]
mod tests {
use super::Ticket;
use common::{overly_long_description, overly_long_title, valid_description, valid_title};
#[test]
fn works() {
let mut ticket = Ticket::new("A title".into(), "A description".into(), "To-Do".into());
ticket.set_title("A new title".into());
ticket.set_description("A new description".into());
ticket.set_status("Done".into());
assert_eq!(ticket.title(), "A new title");
assert_eq!(ticket.description(), "A new description");
assert_eq!(ticket.status(), "Done");
}
#[test]
#[should_panic(expected = "Title cannot be empty")]
fn title_cannot_be_empty() {
Ticket::new(valid_title(), valid_description(), "To-Do".into()).set_title("".into());
}
#[test]
#[should_panic(expected = "Description cannot be empty")]
fn description_cannot_be_empty() {
Ticket::new(valid_title(), valid_description(), "To-Do".into()).set_description("".into());
}
#[test]
#[should_panic(expected = "Title cannot be longer than 50 bytes")]
fn title_cannot_be_longer_than_fifty_chars() {
Ticket::new(valid_title(), valid_description(), "To-Do".into())
.set_title(overly_long_title())
}
#[test]
#[should_panic(expected = "Description cannot be longer than 500 bytes")]
fn description_cannot_be_longer_than_500_chars() {
Ticket::new(valid_title(), valid_description(), "To-Do".into())
.set_description(overly_long_description())
}
#[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(), "To-Do".into()).set_status("Funny".into());
}
}
今度は 可変参照 を利用し「セッター」を書こうという問題です。セッターでもバリデーションをするようにしてほしいとのことです。
解説
impl Ticket {
// newやゲッターは省略
pub fn set_title(&mut self, title: String) {
if title.is_empty() {
panic!("Title cannot be empty");
}
if title.len() > 50 {
panic!("Title cannot be longer than 50 bytes");
}
self.title = title;
}
pub fn set_description(&mut self, description: String) {
if description.is_empty() {
panic!("Description cannot be empty");
}
if description.len() > 500 {
panic!("Description cannot be longer than 500 bytes");
}
self.description = description;
}
pub fn set_status(&mut self, status: String) {
if status != "To-Do" && status != "In Progress" && status != "Done" {
panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
}
self.status = status;
}
}
バリデーション部分は別なプライベート関数に抜き出しても良かったのですが、記事の見栄えを優先して全部展開してみました。共通化したほうが安牌でしょう(どうせ Result
等で書き直すだろうから丁寧に書かなかったのもある)
例によって可変参照周りで面白い話が書いてあるので、今回はBookの方も是非読んでほしいです。例えば変数のシャドーイングの話が書いていたりします。
fn main() {
let hoge = 10;
println!("{}", hoge);
let hoge = 20; // もう一回変数宣言できる! hoge2 とかにする必要はなし!
println!("{}", hoge);
let mut i = 0;
let mut j = i + 1;
for _ in 0..5 {
i += 1;
// シャドーイングはあくまでも改めて宣言しているだけなので、
// スコープを跨ぐことはできない
// (ので可変な変数とは全く別な意味)
let j = i + 1;
}
println!("{}", j); // 1
}
ここで話し足りない参照の話は、次の拙著に任せたいと思います。読んでいただけると幸いです!
では次の問題に行きましょう!
次の記事: 【7】 スタック・ヒープと参照のサイズ ~メモリの話~
-
あんまり他技術の悪口を書くのは読者の方をいたずらに不快にさせるだけなので良くないですが、所有権に関しては性質上他言語での失敗を引き合いに出したほうが説明しやすいというのがあり...まぁ何が言いたいかというとUnity好きな人ごめんなさい。 ↩