2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

Rust 100 Ex 🏃【8/37】 デストラクタ(変数の終わり)・トレイト ~終わりと始まり~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第8回になります!3章の終わりと4章の最初が混在しています。

今回の関連ページ

[03_ticket_v1/11_destructor] デストラクタ (ちょっとライフタイム要素)

このエクササイズには問題はありません。「 Drop トレイト等を説明するまでは適切なエクササイズはできない」とメモ書きがしてあり、よって解く要素はないです。トレイトに関しては本記事より新しく入る4章にて取り上げられています。

一応どんな内容が書いてるか
lib.rs
// 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 関数に相当します(マサカリ飛んできそう)。

main.c
#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 ディレクトリはエントリポイントになるプログラムを置いておけたりする)

テスト
tests/integration.rs
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);
}

解説

lib.rs
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] トレイト

問題はこちらです。短いのでテストも載せてしまいます。

lib.rs
// 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のヘンテコな機能なんかは大体型とこのトレイトで説明されていることが多いです。

解説

複数通り解法が思いつきますが、とりあえず一番シンプルそうな素直に実装する方法を取りました。

lib.rs
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 ~

  1. よく「 free 関数は絶対呼ばなければダメか?」という議論が出ます。100 Exercisesでも後ほど似たようなシチュエーションになり言及があった気がしますが(確か 第28回 ?要はRust語で言えば「リークさせて良いか?」とほぼ同じ意味ということ)、筆者は「メモリリークが降り積もりリソースを無限に食べてしまうような 常駐 プログラム、ではないならば律儀に呼ぶ必要はない」というスタンスです。最も、(C言語をこの先書くことはほとんど無いとは思いますが)書いたコードがいつ常駐プログラムに組み込まれるかわかりませんから、やっぱり書いたほうが行儀が良いという結論には変わりませんね。Rustは自動的にこの行儀を守っていると見ることもできそうです。

  2. 例外が Rc 等です。しかし例外的にすぐに解放されるわけではないことが型を通してソースコードからわかる点が、他言語より有利に働いている点と見れます。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?