LoginSignup
8
9

More than 3 years have passed since last update.

Rust勉強中 - その19 -> ユーティリティトレイト

Last updated at Posted at 2019-10-27

自己紹介

出田 守と申します。
しがないPythonプログラマです。
情報セキュリティに興味があり現在勉強中です。CTFやバグバウンティなどで腕を磨いています。主に低レイヤの技術が好きで、そっちばかり目が行きがちです。

Rustを勉強していくうえで、読んで学び、手を動かし、記録し、楽しく学んでいけたらと思います。

環境

新しい言語を学ぶということで、普段使わないWindowsとVimという新しい開発環境で行っています。
OS: Windows10 Home 64bit 1903
CPU: Intel Core i5-3470 @ 3.20GHz
Rust: 1.38.0
RAM: 8.00GB
Editor: Vim 8.1.1
Terminal: PowerShell

前回

前回は演算子オーバーロードについて学びました。
Rust勉強中 - その18

ユーティリティトレイト

Rustには標準ライブラリで提供されているトレイトがあります。それらを順番に学習していきます。

std::ops::Drop

Dropトレイトは値がドロップされる際の動作を定義できます。

pub trait Drop {
    fn drop(&mut self);
}

以下に実装例を示します。

struct MyMemory {
    id:      usize,
    address: String,
    friends: Vec<String>
}
impl MyMemory {
    fn new() -> Self {
        MyMemory {id: 0, address: String::from("0x0"), friends: vec![]}
    }
}
impl Drop for MyMemory {
    fn drop(&mut self) {
        println!("{}: Dropping your memory from {}...", self.id, self.address);
        if self.friends.is_empty(){
            println!("    No friends!");
        } else {
            println!("    Number of friends: {}", self.friends.len());
        }
        println!("Bye...");
    }
}
fn main() {
    let mem1 = MyMemory::new();
    // drop(mem1); // drop here
    {
        let mut mem2 = MyMemory::new();
        mem2.id      = 1;
        mem2.address = String::from("0x1");
        mem2.friends = vec![String::from("masaru")];
        // mem2.drop(); // Error
    } // first drop here
} // second drop here
1: Dropping your memory from 0x1...
    Number of friends: 1
Bye...
0: Dropping your memory from 0x0...
    No friends!
Bye...

ドロップした際にメッセージを表示させています。
main関数内に新たなスコープを設けて、その中で変数mem2を定義しています。そのため先にmem2がドロップされます。次に変数mem1がドロップされます。
dropメソッドは暗黙的にしか呼び出せず、直接呼び出すことはできません。

Dropトレイトを実装した場合、その型に対してCopyトレイトを実装することはできません。コピーした同じ値をドロップすることはないからだそうです。

標準のpleludeにはstd::mem::drop関数が定義されています。

pub fn drop<T>(_x: T)

引数_xに値の所有権を渡して、何もせずドロップさせています。(なるほど)
先ほどの例でdrop関数を使ってみます。

...
fn main() {
    let mem1 = MyMemory::new();
    drop(mem1); // drop here
    {
        let mut mem2 = MyMemory::new();
        mem2.id      = 1;
        mem2.address = String::from("0x1");
        mem2.friends = vec![String::from("masaru")];
        // mem2.drop(); // Error
    } // first drop here
} // second drop here
0: Dropping your memory from 0x0...
    No friends!
Bye...
1: Dropping your memory from 0x1...
    Number of friends: 1
Bye...

mem1を先にドロップさせるようにしました。そして、確かに先にmem1がドロップされているようです。

std::marker::Sized

Sizedトレイトを実装するsized型の値は、メモリ上でのサイズが決まっていることを表します。このようなトレイトをマーカトレイトと言います。u8や(u8, u8, u8)やVecなどはsized型です。
一方で、unsized型の値が、str型や[T]や、トレイトオブジェクトの参照先になります。unsized型の値は、変数の格納や引数に渡すことができません。
sized型は通常のポインタで、unsized型はファットポインタを持ちます。
Sized型の定義は以下のようになっています。

pub trait Sized { }

Sizedトレイトは型パラメータの制約でしか使用できません。
型パラメータの制約には、暗黙的にSizedトレイトを指定しています。
ただし、構造体の最後のフィールドのみunsized型の値を許容します。
もし、Sizedトレイトの制約を外したい場合は?Sizedを指定します。

以下に簡単な例を示します。

struct SizedData<T> {
    data: T
}
// impl SizedData<[i32]> {} // Error

struct UnsizedData<T: ?Sized> {
    data: T
}
impl UnsizedData<[i32]> {}  // Ok

fn main() {
}

std::clone::Clone

Cloneトレイトを実装すれば、自分自身のコピーを作ることができます。ただし、時間とメモリを食う可能性があります。

pub trait Clone {
    fn clone(&self) -> Self;

    fn clone_from(&mut self, source: &Self) { ... }
}

cloneメソッドは引数から新しい自分自身を作り出します。clone_fromメソッドはデフォルトメソッドで、sourceからselfにコピーします。デフォルトではsourceをcloneメソッドで呼び出すので、時間とメモリを食う可能性があります。

#[derive(Debug)]
struct S {
    data: Vec<i32>
}
impl Clone for S {
    fn clone(&self) -> Self {
        println!("Cloning... {:?}", self);
        S {data: self.data.clone()}
    }
    fn clone_from(&mut self, source: &Self) {
        println!("Cloning from {:?} to {:?}", source, self);
        *self = source.clone();
    }
}

fn main() {
    let mut s1 = S {data: vec![1, 2, 3]};
    let s2 = s1.clone();
    let s3 = S {data: vec![4, 5, 6]};
    println!("s1 = {:?}, s2 = {:?}", s1, s2);
    println!("s1 = {:?}", {s1.clone_from(&s3); s1});
}
Cloning... S { data: [1, 2, 3] }
s1 = S { data: [1, 2, 3] }, s2 = S { data: [1, 2, 3] }
Cloning from S { data: [4, 5, 6] } to S { data: [1, 2, 3] }
Cloning... S { data: [4, 5, 6] }
s1 = S { data: [4, 5, 6] }

std::marker::Copy

Copyは、その値がコピーできることを表すマーカトレイトです。

pub trait Copy: Clone { }

CopyトレイトはCloneトレイトを継承しているようです。なので、Copyトレイトを実装する場合は、Cloneトレイトも実装する必要があります。

#[derive(Clone, Debug)]
struct S {
    // v: Vec<i32>,  // Error: this field does not implement `Copy`
    data: i32
}
impl Copy for S {}

fn main() {
    let mut s = S {data: 0};
    let s2 = s;
    println!("{:?}", s.clone());
    println!("{:?}", {s.clone_from(&S{data: 1}); s});
}
S { data: 0 }
S { data: 1 }

Copyトレイトは、バイト単位のコピーができる型のみ実装できます。ですので、例で示すようにVecなどはエラーを吐きます。
上記例で構造体SはCopyトレイトを実装しているので、変数sを変数s2へ代入してもコピーされるので、その後のsに関する処理もエラーを吐きません。
CopyもClone同様、時間とメモリを食う可能性があるので慎重に実装するか考えるべきです。

std::ops::Derefとstd::ops::DerefMut

DerefトレイトとDerefMutトレイトは、ある型の参照解決時の動作を定義できます。Derefトレイトは、共有参照のみ必要な型に実装し、DerefMutは可変参照が必要な型に実装します。

pub trait Deref {
    type Target: ?Sized;
    fn deref(&self) -> &Self::Target;
}
pub trait DerefMut: Deref {
    fn deref_mut(&mut self) -> &mut Self::Target;
}

DerefMutはDerefを継承しています。Target関連型を持ち、メソッドで出力されます。
これらのトレイトはある参照型から別の参照型へ自動変換されます。これを型強制や参照解決型変換と呼ばれるそうです。型強制はメソッド呼び出し時や引数に渡したときに、必要とされる型に対応していればその型に変換されます。

struct S<T> {
    data: T
}
use std::ops::{Deref, DerefMut};
impl<T> Deref for S<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        println!("dereference");
        &self.data
    }
}
impl<T> DerefMut for S<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        println!("dereference mutable");
        &mut self.data
    }
}


fn print_S<T:std::fmt::Debug>(s: T) {
    println!("{:?}", s);
}

fn main() {
    let mut s = S {data: [5]};
    println!("{:?}", *s);
    *s = [10];
    println!("{:?}", *s);
    let s = S {data: String::from("aaa")};
    // print_S(&s);      // Error: `S<std::string::String>` doesn't implement `std::fmt::Debug`
    print_S(&s as &str); // ok
}
dereference
[5]
dereference mutable
dereference
[10]
dereference
"aaa"

型強制を目的にDerefやDerefMutを実装すると混乱の原因になるそうです。上記のprint_S関数はDebugトレイトの制約があります。そこに、Debugトレイトを実装していない構造体Sの参照を渡した場合、型強制を期待しますが、実際は制約チェック時には型強制されないのでエラーを吐きます。この場合は明示的に変換してから引数に渡すと解決するようです。

std::default::Default

Defaultトレイトは、ある型にデフォルト値を設けたい場合に実装します。

pub trait Default: Sized {
    fn default() -> Self;
}

以下に例を示します。

#[derive(Debug)]
struct S {
    data: (i32, i32, i32),
    tmp1:  i32,
    tmp2:  i32
}
impl Default for S {
    fn default() -> Self {
        S {data: (0, 0, 0), tmp1: 0, tmp2: 0}
    }
}

fn main() {
    let s = S::default();
    println!("{:?}", s);
}
S { data: (0, 0, 0), tmp1: 0, tmp2: 0 }

ほとんど、構造体Sのスタティックメソッドnew()を定義した場合と同じ動作をします。#[derive(Default)]を使えば同じことを簡単に実装できます。

#[derive(Default, Debug)]
// #[derive(Debug)]
struct S {
    data: (i32, i32, i32),
    tmp1:  i32,
    tmp2:  i32
}
// impl Default for S {
//     fn default() -> Self {
//         S {data: (0, 0, 0), tmp1: 0, tmp2: 0}
//     }
// }

fn main() {
    let s = S::default();
    println!("{:?}", s);
}

defaultメソッドはフィールドが多すぎて、一部の値だけを設定して他はデフォルト値を使いたい場合などのときに便利です。

...
fn main() {
    ...
    let s = S {data: (5, 10, 15), ..Default::default()};
    println!("{:?}", s);
}
S { data: (5, 10, 15), tmp1: 0, tmp2: 0 }

std::convert::AsRefとstd::convert::AsMut

AsRefトレイトとAsMutトレイトは、ある型から&Tを効率的に借用するために実装します。

pub trait AsRef<T: ?Sized> {
    fn as_ref(&self) -> &T;
}
pub trait AsMut<T: ?Sized> {
    fn as_mut(&mut self) -> &mut T;
}

以下に例を示します。

fn get_type<T>(_: T) -> &'static str {
    std::any::type_name::<T>()
}

#[derive(Debug)]
struct S {
    data: i32
}
impl AsRef<i32> for S {
    fn as_ref(&self) -> &i32 {
        &self.data
    }
}
impl AsMut<i32> for S {
    fn as_mut(&mut self) -> &mut i32 {
        &mut self.data
    }
}

fn main() {
    println!("{:?}", get_type(S {data: Default::default()}.as_ref()));
    println!("{:?}", get_type(S {data: Default::default()}.as_mut()));
}
"&i32"
"&mut i32"

std::borrow::Borrowとstd::borrow::BorrowMut

AsRefやAsMutと似ているのですが違うようです。
ドキュメントでも例として出されているHashMapのgetメソッドをみてみます。

impl<K, V, S> HashMap<K, V, S>
where
    K: Eq + Hash,
    S: BuildHasher,
{
    ...
    pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V>
    where
        K: Borrow<Q>,
        Q: Hash + Eq,
    {
        self.base.get(k)
    }
    ...
}

型パラメータKはgetメソッドにおいてEq+Hash(impl宣言時)+Borrow(get宣言時)を制約しています。他方型パラメータQはHash+Eqを制約しています。つまり、KはHash+EqでQのBorrowができれば許容されるということです。例えば、Stringと&strはハッシュが同じだと保証されているので、標準でimpl Borrow(str) for Stringimpl Borrow(String) for Stringを実装しているそうです。これによって、getメソッドは&String型でも&str型でも引数として受け付けるようにできるというわけです。
このように、複数の型を受け付けたい場合やハッシュ値を計算する場合、比較する場合にBorrowは用いられるそうです。
私はまだHashMapなどに行きつけていないので、フワフワしていますが経験を積めば使いどころが分かってくると思っています。

std::convert::Fromとstd::convert::Into

FromトレイトとIntoトレイトは、ある型から別の型への変換を実装します。

pub trait From<T>: Sized {
    fn from(_: T) -> Self;
}
pub trait Into<T>: Sized {
    fn into(self) -> T;
}

Fromはある型から別の型へのインスタンスを返します。Intoは、例えば関数の引数で複数の型を許容するために指定します。

AsRefと違って、FromとIntoへ値を渡す際には所有権を移動させます。

Fromをある型に実装した場合、自動的にIntoトレイトが実装されます。

以下に例を示します。

#[derive(Debug)]
struct S {
    id:   i32,
    data: String
}

impl<T> From<(i32, T)> for S 
    where T: Into<String>
{
    fn from(t: (i32, T)) -> Self {
        S {id: t.0, data: t.1.into()}
    }
}

fn print_data<T>(s: T)
    where T: Into<S>
{
    println!("data = {}", s.into().data);
}

fn main() {
    println!("{:?}", S::from((0, "DATA_FROM_String".to_string())));
    println!("{:?}", S::from((1, "DATA_FROM_str")));
    print_data(S::from((2, "call with S")));
    print_data((3, "call with tuple"));
}
S { id: 0, data: "DATA_FROM_String" }
S { id: 1, data: "DATA_FROM_str" }
data = call with S
data = call with tuple

構造体SにFromトレイトを実装する際に、dataをstr型でもString型でも受け取れるようにしたかったので、TにInto<String>として制約を付けています。そしてdataを設定する際にintoメソッドで変換を行います。
print_data関数も同じように、TにInto<S>を付けることで、構造体Sの引数だけではなく、タプルからの引数も許容します。

std::borrow::ToOwned

ToOwnedトレイトは、Cloneできない値(例えば&strや&[T]など)をクローンするために実装します。

pub trait ToOwned {
    type Owned: Borrow<Self>;

    fn to_owned(&self) -> Self::Owned;

    fn clone_into(&self, target: &mut Self::Owned) {
        *target = self.to_owned();
    }
}

ToOwnedドキュメントの例を以下に掲載します。

fn main() {
    let s: &str = "a";
    let ss: String = s.to_owned();

    let v: &[i32] = &[1, 2];
    let vv: Vec<i32> = v.to_owned();
}

これは、StringはBorrowを実装しており、strはToOwnedを実装しているのでクローンが許されます。&[T]についても同様です。

std::borrow::Cow

値を借用するか所有するかを判断できない場合にCow列挙型を使うと便利だそうです。

pub enum Cow<'a, B: ?Sized + 'a>
    where B: ToOwned
{
    /// Borrowed data.
    Borrowed(&'a B),

    /// Owned data.
    Owned(<B as ToOwned>::Owned),
}

まだ経験が浅いので使いどころの実感がわかないですが、以下の記事が非常に参考になりました。
Rustでfizzbuzzした話 あるいはstd::borrow::Cowについて

このような状況になったとき改めて勉強しようと思います。今は、値を借用するか所有するか迷っているときは列挙型Cowが使えるかもしれないとだけ憶えておきます。

今回はここまで~!

8
9
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
8
9