3
0

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 🏃【12/37】 Clone・Copy・Dropトレイト ~覚えるべき主要トレイトたち~

Last updated at Posted at 2024-07-17

前の記事

全記事一覧

100 Exercise To Learn Rust 演習第12回になります!今回紹介するトレイト達はRustでプログラミングする際に常に重要になってくる者たちです。

今回の関連ページ

[04_traits/11_clone] Cloneトレイト

問題はこちらです。

lib.rs
// TODO: add the necessary `Clone` implementations (and invocations)
//  to get the code to compile.

pub fn summary(ticket: Ticket) -> (Ticket, Summary) {
    (ticket, ticket.summary())
}

pub struct Ticket {
    pub title: String,
    pub description: String,
    pub status: String,
}

impl Ticket {
    pub fn summary(self) -> Summary {
        Summary {
            title: self.title,
            status: self.status,
        }
    }
}

pub struct Summary {
    pub title: String,
    pub status: String,
}

Clone トレイトを実装して呼んでほしい、という問題です。

解説

Clone トレイトを実装して呼ぶだけですね。

lib.rs
// TODO: add the necessary `Clone` implementations (and invocations)
//  to get the code to compile.

pub fn summary(ticket: Ticket) -> (Ticket, Summary) {
-     (ticket, ticket.summary())
+     (ticket.clone(), ticket.summary())
}

+ #[derive(Clone)]
pub struct Ticket {
    pub title: String,
    pub description: String,
    pub status: String,
}

impl Ticket {
    pub fn summary(self) -> Summary {
        Summary {
            title: self.title,
            status: self.status,
        }
    }
}

pub struct Summary {
    pub title: String,
    pub status: String,
}

Clone トレイトは複製のトレイトです。所有権に厳しいRustで、変更を共有するつもりがない構造体の複製は、 Clone トレイトの .clone() メソッドを呼ぶことによって行われます。

かなり重要なトレイトではありますが、構造体に関しては幸いにして全フィールドに Clone トレイトが実装されていれば手動実装もとても楽なトレイトです(そうでない場合でも手動実装可能です)。書き出してみると .clone() メソッドが何を行うのかは一目瞭然でしょう。

Rust
struct Hoge {
    a: usize,
    b: String,
    c: Vec<bool>,
}

impl Clone for Hoge {
    // 元の変数の所有権は奪わず、新しい構造体インスタンスを返している
    fn clone(&self) -> Self {
        Self {
            // 複製されてそう
            a: self.a.clone(),
            b: self.b.clone(),
            c: self.c.clone(),
        }
    }
}

そしてこの処理を自動実装してくれるのが Clone deriveマクロです。 Clone マクロを使うには、全フィールドの型が Clone トレイトを実装している必要があります。 下手に手動実装するより、この制約を守りながらderiveマクロで実装するのが定石でしょう。

このマクロを使用して Clone を実装すると嬉しいこととして、 .clone() メソッドを呼び出すと複製が再帰的に呼び出されます。すなわちデフォルトで ディープコピーされる わけです!

所有権や参照という仕組み、そしてここまでの演習からもしかしたら察しがつくかもしれませんが、 Rustにはシャローコピーという考え方は基本的にありません 1。 もちろん工夫をすればシャローコピーに準じた実装をすることが可能ですが、その場合ちょっと大掛かりになる(第29回か、下記に示す拙著を参照してほしいです)ので普段は気にする必要がありません。他言語で散々プログラマを惑わすあの罠は、Rustには基本通用しないのです!

もっと詳しい話は拙著を読んでくださると恐悦至極です!:bow:

Rustにはシャローコピーがわからない - Qiita

[04_traits/12_copy] Copyトレイト

問題はこちらです。

lib.rs
// TODO: implement the necessary traits to make the test compile and pass.
//  You *can't* modify the test.

pub struct WrappingU32 {
    value: u32,
}

impl WrappingU32 {
    pub fn new(value: u32) -> Self {
        Self { value }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ops() {
        let x = WrappingU32::new(42);
        let y = WrappingU32::new(31);
        let z = WrappingU32::new(u32::MAX);
        assert_eq!(x + y + y + z, WrappingU32::new(103));
    }
}

「テストを変えずにコンパイルを通してね」問題になります。

解説

.clone() メソッドが呼び出されていないので Clone トレイトだけでは不十分です。 マーカートレイト Copy も実装することで、所有権を奪われそうになったら自動複製されるようにします。

lib.rs
use std::ops::Add;

+ #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct WrappingU32 {
    value: u32,
}

impl WrappingU32 {
    pub fn new(value: u32) -> Self {
        Self { value }
    }
}

impl Add for WrappingU32 {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        Self {
            value: self.value.wrapping_add(rhs.value),
        }
    }
}

Copy トレイトを実装するには、前提として Clone トレイトが実装されている必要があるため、回答には Clone トレイトも付けています。

Copy トレイトの付与にはもう一つ条件があって、内包するフィールド全てが Copy トレイトを実装している 必要があります。そして、 Copy を実装している型は大体 スタック上に値が保持されるプリミティブ型 です。ヒープアロケーションが必要な型( StringVec<T> など)を内包している時は Copy は実装できないと考えておきましょう。

Clone トレイトを付与していることから内部的に .clone() を呼んでいると勘違いしがちですが(実際筆者は勘違いしていました。)、 Copy トレイトが付与された型の複製は、スタックメモリ上の値がそのまま別な場所に複製されるだけで .clone() は呼ばれません!

Copy を実装している型で .clone() にて変わった処理を実装することは可能ですが、その場合望まない結果になることが容易に予想されるので、やめておいた方が良さそうです。

参考: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=de1113871471d8e7deb6aeae09c48dc7

Copy による自動複製では .clone() が呼ばれていないことが観測できます。

[04_traits/13_drop] Dropトレイト

問題はこちらです。

lib.rs
// TODO: implement a so-called "Drop bomb": a type that panics when dropped
//  unless a certain operation has been performed on it.
//  You can see the expected API in the tests below.

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    #[should_panic]
    fn test_drop_bomb() {
        let bomb = DropBomb::new();
        // The bomb should panic when dropped
    }

    #[test]
    fn test_defused_drop_bomb() {
        let mut bomb = DropBomb::new();
        bomb.defuse();
        // The bomb should not panic when dropped
        // since it has been defused
    }
}

「ドロップ前にリソースをしっかりと解放していないとパニックになる、『Drop爆弾』を実装してください」という問題です。

解説

Rustのリソース管理便利トレイト、 Drop の登場です!このトレイトにより提供される drop メソッドは、 変数のドロップ時に呼び出されます

lib.rs
struct DropBomb {
    defused: bool,
}

impl DropBomb {
    fn new() -> Self {
        Self { defused: false }
    }

    fn defuse(&mut self) {
        self.defused = true;
    }
}

impl Drop for DropBomb {
    fn drop(&mut self) {
        if !self.defused {
            panic!("Resources are not yet cleaned up! Please call `defuse` method before drop to clean up it.");
        }
    }
}

この性質を利用してテストを通します!DropBombの値のライフタイムが終了してドロップされる際に drop メソッドの中身が呼ばれるので、 defuse メソッドであらかじめ然るべきリソース解放処理をしていないと、パニックしてしまう、という寸法です。

Drop トレイトを実装すれば、Rustのライフタイムの仕組みを借りて、リソース管理ができるのです!Pythonの with 文やC#のuseブロックに近い感じでしょうか...?

これは例えば以前少し触れたようにファイルハンドラや、そしてDBのコネクションやトランザクション等にも応用できます。

実際の実装例: https://github.com/sfackler/rust-postgres/blob/a1bdd0bf15325cdc05fcfe3c0671953b2a8c7981/postgres/src/transaction.rs#L16

トランザクション等の close の呼び忘れを気にしなくて良いので、とても強力なトレイトですね。常に意識しておきたいです。

Bookに書かれていることですが、 Copy トレイトと Drop トレイトは共存できません! 。これも筆者知らなかったよ...

Copy トレイトが実装される型はスタック上でしか扱われない軽い型であるというコンセプトを鑑みると、リソース片付けを目的とする Drop トレイトが共存不可というのは納得できる話ではあります。

では次の問題に行きましょう!

次の記事: 【13】 トレイトまとめ・列挙型・match式 ~最強のトレイトの次は、最強の列挙型~

登場したPlayground

(実際に無効化したことはないですが、)Rust Playground上のデータが喪失する可能性を鑑みて、一応記事にもソースコードを掲載することとしました。

URL: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=de1113871471d8e7deb6aeae09c48dc7

Rust
#[derive(Debug)]
struct Hoge(u32);

impl Clone for Hoge {
    fn clone(&self) -> Self {
        println!("beep!: {}", self.0);    

        Hoge(self.0)
    }
}

impl Copy for Hoge {}

fn main() {
    let h = Hoge(10);
    let h2 = h;
    
    dbg!(h);
    dbg!(h2);
    dbg!(h);
    h.clone();
}
  1. 「参照が複製される様はシャローコピーでは?」というツッコミが来そうですが、「なんでわざわざそんな難しい言い方をするのですか?『参照が複製される』と表現すれば良くないですか?」と返したくなっちゃうんですよね。もとい、シャローコピーという考え方がない、というよりは、そういう言い回しをしない、という方が正確なのかもしれません。

3
0
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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?