前の記事
- 【0】 準備 ← 初回
- ...
- 【7】 スタック・ヒープと参照のサイズ ~メモリの話~ ← 前回
- 【8】 デストラクタ(変数の終わり)・トレイト ~終わりと始まり~ ← 今回
全記事一覧
- 【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 演習第8回になります!3章の終わりと4章の最初が混在しています。
今回の関連ページ
[03_ticket_v1/11_destructor] デストラクタ (ちょっとライフタイム要素)
このエクササイズには問題はありません。「 Drop
トレイト等を説明するまでは適切なエクササイズはできない」とメモ書きがしてあり、よって解く要素はないです。トレイトに関しては本記事より新しく入る4章にて取り上げられています。
一応どんな内容が書いてるか
// We need some more machinery to write a proper exercise for destructors.
// We'll pick the concept up again in a later chapter after covering traits and
// interior mutability.
fn outro() -> &'static str {
"I have a basic understanding of __!"
}
#[cfg(test)]
mod tests {
use crate::outro;
#[test]
fn test_outro() {
assert_eq!(outro(), "I have a basic understanding of destructors!");
}
}
というわけで解説項では本エクササイズのBookを補足する形として、C言語のソースコードでも載せてみたいと思います。
Rustのドロップ( drop
)は、(ヒープにあるリソースに対しては)ずばり free
関数に相当します(マサカリ飛んできそう)。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p;
p = (int*)malloc(sizeof(int) * 16);
p[0] = 10;
printf("%d\n", *p);
free(p); // リソースの片付け // drop相当!
// free(p); <- 二重解放!
// printf("%d\n", *p); <- 無効なポインタの参照!
return 0;
}
C言語で言う malloc
関数でヒープに確保したメモリは、ポインタさえあればいつでもアクセスを試みることができてしまいます(そしてランタイムでアクセスできないことが発覚します)。そして要らなくなったら領域を解放する責任があります。
わざわざ言うまでもないかもしれませんが、言語によってこのヒープメモリ管理方法は2つの派閥があるわけです。
- C/C++: 自分で管理する1。言わずがな二重freeやダングリングポインタ等様々なバグを生んできた
- ガベコレ言語(Javaとか): ガベコレランタイム等が使わなくなったメモリであるかを判断して自動的に解放してくれる
Rustは第3の選択肢 「基本的2にはスコープが外れたら片付ける」 を提供してくれています。(Rc
もガベコレと呼ばれる辺りの関係で、)個人的にはRustは広義のガベコレ言語に含まれる気がするのですが、メモリやリソースが解放されるタイミングを所有権・ライフタイムやスコープという形でプログラマが意識しなければならない という点が他言語と特に異なるので、別なパラダイムとして扱われている気がします。しかし所有権やライフタイムのお陰で コンパイル時に 二重解放やダングリングポインタに相当するバグを潰すことができるわけです。Rustが堅牢な言語と呼ばれる所以があります。
ところで「スコープを抜け出したらDrop」という挙動はメモリだけの話にとどまらないリソース管理の考え RAII を理解しやすいものにしてくれていることの恩恵の方が大きいと筆者は思っています。例えば、 std::fs::File::open
でオープンしたファイルは、Rustの場合 close
のようなメソッドでクローズしなくても、ファイルハンドラがドロップされた時点で自動的に閉じてくれます。この辺の話は、冒頭でも話した通り Drop
トレイトが関係する話なので、 第12回 でもう一度触れたいです!
[03_ticket_v1/12_outro] 3章まとめ
本章のまとめとなる問題ですね。問題指示は次のとおりです。
- TODO:
Order
(注文)構造体を定義してください-
product_name
,quantity
,unit_price
フィールドを持ちます -
product_name
は1文字以上300文字以下 -
quantity
は正の数 -
unit_price
も正の数 - 注文の合計金額を算出する
total
メソッドを実装してください - 注文のフィールドにはゲッターとセッターを設けてください
-
- テストは
tests
フォルダの方にまとめています(後述) - テストから参照できるように然るべきメソッドは
pub
を付けて公開する必要があります
規模が大きいからかテストは tests/integration.rs
にまとめられています。 tests
は特殊なディレクトリで、テスト用のファイルを置いておける場所になっています。(他にも特殊なものとして、 bin
ディレクトリはエントリポイントになるプログラムを置いておけたりする)
テスト
use outro_02::Order;
// Files inside the `tests` directory are only compiled when you run tests.
// As a consequence, we don't need the `#[cfg(test)]` attribute for conditional compilation—it's
// implied.
#[test]
fn test_order() {
let mut order = Order::new("Rusty Book".to_string(), 3, 2999);
assert_eq!(order.product_name(), "Rusty Book");
assert_eq!(order.quantity(), &3);
assert_eq!(order.unit_price(), &2999);
assert_eq!(order.total(), 8997);
order.set_product_name("Rust Book".to_string());
order.set_quantity(2);
order.set_unit_price(3999);
assert_eq!(order.product_name(), "Rust Book");
assert_eq!(order.quantity(), &2);
assert_eq!(order.unit_price(), &3999);
assert_eq!(order.total(), 7998);
}
// Validation tests
#[test]
#[should_panic]
fn test_empty_product_name() {
Order::new("".to_string(), 3, 2999);
}
#[test]
#[should_panic]
fn test_long_product_name() {
Order::new("a".repeat(301), 3, 2999);
}
#[test]
#[should_panic]
fn test_zero_quantity() {
Order::new("Rust Book".to_string(), 0, 2999);
}
#[test]
#[should_panic]
fn test_zero_unit_price() {
Order::new("Rust Book".to_string(), 3, 0);
}
解説
pub struct Order {
product_name: String,
quantity: u32,
unit_price: u32,
}
impl Order {
pub fn new(product_name: String, quantity: u32, unit_price: u32) -> Self {
validate_product_name(&product_name);
validate_quantity(quantity);
validate_unit_price(unit_price);
Self {
product_name,
quantity,
unit_price,
}
}
pub fn product_name(&self) -> &str {
&self.product_name
}
pub fn set_product_name(&mut self, product_name: String) {
validate_product_name(&product_name);
self.product_name = product_name;
}
pub fn quantity(&self) -> &u32 {
&self.quantity
}
pub fn set_quantity(&mut self, quantity: u32) {
validate_quantity(quantity);
self.quantity = quantity;
}
pub fn unit_price(&self) -> &u32 {
&self.unit_price
}
pub fn set_unit_price(&mut self, unit_price: u32) {
validate_unit_price(unit_price);
self.unit_price = unit_price;
}
pub fn total(&self) -> u32 {
self.quantity * self.unit_price
}
}
fn validate_product_name(product_name: &str) {
assert!(!product_name.is_empty());
assert!(product_name.len() <= 300);
}
fn validate_quantity(quantity: u32) {
assert!(quantity > 0);
}
fn validate_unit_price(unit_price: u32) {
assert!(unit_price > 0);
}
復習問題だからあまり解説することがない...Rustでプログラミングするとして上記のコードはほぼ違和感がない仕上がりになっていると自負していますが、やはりバリデーションがパニックするのが違和感ですね...早く Result
型を使用したいです。
[04_traits/01_trait] トレイト
問題はこちらです。短いのでテストも載せてしまいます。
// Define a trait named `IsEven` that has a method `is_even` that returns a `true` if `self` is
// even, otherwise `false`.
//
// Then implement the trait for `u32` and `i32`.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_u32_is_even() {
assert!(42u32.is_even());
assert!(!43u32.is_even());
}
#[test]
fn test_i32_is_even() {
assert!(42i32.is_even());
assert!(!43i32.is_even());
assert!(0i32.is_even());
assert!(!(-1i32).is_even());
}
}
トレイト IsEven
を定義して u32
型の変数と i32
型の変数に実装してくださいという問題です。
トレイトは他言語でいうインターフェースのようなもので、今後様々な場所で登場します。Rustを理解する上で避けては通れず、あるいはRustのヘンテコな機能なんかは大体型とこのトレイトで説明されていることが多いです。
解説
複数通り解法が思いつきますが、とりあえず一番シンプルそうな素直に実装する方法を取りました。
trait IsEven {
fn is_even(&self) -> bool;
}
impl IsEven for u32 {
fn is_even(&self) -> bool {
*self % 2 == 0
}
}
impl IsEven for i32 {
fn is_even(&self) -> bool {
*self % 2 == 0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_u32_is_even() {
assert!(42u32.is_even());
assert!(!43u32.is_even());
}
#[test]
fn test_i32_is_even() {
assert!(42i32.is_even());
assert!(!43i32.is_even());
assert!(0i32.is_even());
assert!(!(-1i32).is_even());
}
}
特に解説できることがない... trait IsEven {...}
でトレイトを定義して、 impl IsEven for u32 {...}
等で具体的な実装を与えています。
トレイトに出会った時に特徴的に感じるのは、メソッドの引数に self
を使えることでしょうか? self
の型はトレイト定義時点では決まっていないのに、メソッドの引数に取れるというのはなんとも奇妙な感じがしますが、この後出てくるジェネリクスや関連型の存在を鑑みると別におかしくないのかもしれません。
ちなみに別な解法としては、ボイラーテンプレート削減のために num::Num
トレイトを使うか、 マクロを使う方法がありますが、後々登場しそうだったので今回は控えました。
では次の問題に行きましょう!
次の記事: 【9】 Orphan rule (孤児ルール)・演算子オーバーロード・derive ~Empowerment \U0001F4AA ~
-
よく「
free
関数は絶対呼ばなければダメか?」という議論が出ます。100 Exercisesでも後ほど似たようなシチュエーションになり言及があった気がしますが(確か 第28回 ?要はRust語で言えば「リークさせて良いか?」とほぼ同じ意味ということ)、筆者は「メモリリークが降り積もりリソースを無限に食べてしまうような 常駐 プログラム、ではないならば律儀に呼ぶ必要はない」というスタンスです。最も、(C言語をこの先書くことはほとんど無いとは思いますが)書いたコードがいつ常駐プログラムに組み込まれるかわかりませんから、やっぱり書いたほうが行儀が良いという結論には変わりませんね。Rustは自動的にこの行儀を守っていると見ることもできそうです。 ↩ -
例外が
Rc
等です。しかし例外的にすぐに解放されるわけではないことが型を通してソースコードからわかる点が、他言語より有利に働いている点と見れます。 ↩