25
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rust勉強中 - その17 -> トレイト

Last updated at Posted at 2019-10-20

自己紹介

出田 守と申します。
しがない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勉強中 - その16

トレイト

トレイトは、ある型の機能を表現します。トレイトを使うことで同じ名前の機能をあらゆる型に対して実装したり、追加することができます。トレイトはインタフェースや抽象基底クラスといった概念に似ており、ポリモーフィズム(多様性)を実現します。

トレイトの定義と実装

まず、トレイトの定義方法と型への実装方法を説明します。
トレイトの定義はtraitブロックで行います。

trait Trait {
    fn method(...);
    ...
}

traitブロックにはメソッドを列挙していきます。このとき、メソッドの機能はまだ、定義しません(後述するデフォルトメソッドは例外)。
例えば以下のように定義します。

mod mod_traits {
    pub trait Out {
        fn print_twice(&self);
        fn print_add_doller(&self);
    }
}

上記はOutというトレイトをパブリックで定義しています。Outはメソッドを2つ持ちます。なお、modブロックで囲っているのはモジュールとして別ファイルで定義した想定で進めたかったからです。
ある型にトレイトを使って実装するにはimplブロックを使います。

impl Trait for Type {
    fn method(...) {
        ...
    }
}

この時に型Typeに対してTraitのmethodを実装していきます。
先ほどのOutトレイトを実装してみます。

use mod_traits::Out;

#[derive(Debug)]
struct Number {
    num: i32
}

impl Out for Number {
    fn print_twice(&self) {
        println!("{}, {}", self.num, self.num);
    }

    fn print_add_doller(&self) {
        println!("$ {}", self.num);
    }
}

impl Out for String {
    fn print_twice(&self) {
        println!("{}, {}", self, self);
    }

    fn print_add_doller(&self) {
        println!("$ {}", self);
    }
}

まず、重要なのはそのトレイトがスコープ内に存在しないと使えないということです。なので、最初にuseを使ってインポートしています。また、そのトレイトのメソッドを使う際にもトレイトがスコープ内にいなければ使えません。
次に型となるNumber構造体を定義します。このNumber構造体にOutトレイトを実装します。そして、Outトレイトのメソッドの動作も定義していきます。
ついでに、Stringに対してもOutトレイトを実装しました。こうすれば、既存の型に対してメソッドを追加することができます。

使い方は単純にメソッドを呼ぶように使えます。

fn main() {
    let num = Number {num: 5};
    num.print_twice();      // 5, 5
    num.print_add_doller(); // $ 5
    String::from("JUMP!").print_twice();          // JUMP!, JUMP!
    String::from("cargo run").print_add_doller(); // $ cargo run
}

トレイトには、多様性を実現するために2種類の方法があります。

  • トレイトオブジェクト
  • ジェネリクス

トレイトオブジェクト

トレイトオブジェクトは、あるトレイトを実装したある型のポインタです。
トレイトオブジェクトを作るには以下のようにします。

let trait_object: Trait = type;           // Error
let trait_object: &dyn Trait = &type;         // ok
let trait_object: &mut dyn Trait = &mut type; // ok

変数のサイズはコンパイル時に決まっている必要があります。トレイトは、あらゆる型に実装できるため、サイズが決まりません。1行目のように渡してしまうとエラーを吐きます。
なので、何らかの値を指しているというアドレスを指す参照にすることでトレイトオブジェクトを作成します。
では、実際にトレイトオブジェクトを作成してみます。

fn trait_object_out(o: &dyn Out) {
    o.print_twice();
    o.print_add_doller();
}

fn main() {
    ...
    // trait object
    let out: &dyn Out = #
    out.print_twice();      // 5, 5
    out.print_add_doller(); // $ 5
    trait_object_out(&out); // 5, 5, $ 5
}

dynはトレイトオブジェクトのための識別子のようなものです。トレイトとトレイトオブジェクトを見分けるために付けます。これを付けないと警告されました。

トレイトオブジェクトは2wordのファットポインタで、値へのポインタと参照先の値の型に関するテーブルを持ちます。これらは、Rustの内部で参照されます。

ジェネリクス

次にジェネリクスをみていきます。
ジェネリック関数を定義するには以下のようにします。

fn function<T: Trait>(t: T) {
    ...
}

これは関数functionはTraitを実装した引数を受け取るという意味です。このTraitを実装したあらゆる型を受け取ります。このような書き方を制約といいます。
では、実際に定義して使ってみます。

use std::fmt::Debug;
fn generic_out<O: Out+Debug>(o: &O) {
    o.print_twice();
    o.print_add_doller();
    println!("{:?}", o);
}

fn main() {
    ...
    // generics
    generic_out(&num);
}

ある型に複数のTraitで制約を付けたい場合は+を付けてつなげます。この場合関数generic_out内でOutのメソッドを呼び出しかつ、{:?}によるデバッグ出力を行っています。つまり、引数oはOutトレイトとDebugトレイトを実装していなければいけないということです。
もし、制約を多く書かなければいけない場合、<>内に書くと見づらくなってしまうため、そのような場合はwhereを使います。

fn generic_where_out<'a, O, O2>(o: &'a O, o2: &'a O2)
    where O: Out+Debug,
          O2: Out2+Debug
{
    o.print_twice();
    o.print_add_doller();
    println!("{:?}", o);
    o2.print_triple();
    o2.print_add_asterisk();
    println!("{:?}", o2);
}

fn main() {
    ...
    generic_where_out(&num, &num);
}

また、上記のようにライフタイムパラメータがある場合、トレイトよりも先に書きます。

ジェネリック関数とトレイトオブジェクトの特徴

以上の二つの使い方は非常に似ています。どちらを使うかは、マシン環境やスピードなどを考慮して決めます。
トレイトオブジェクトは、関数をインライン化できません。なぜなら、実行時まで型が分からないためです。一方、ジェネリック関数は型が分かっているためインライン化できます。インライン化されることによって、実行速度が速くなります。
ジェネリック関数のバイナリサイズはトレイトオブジェクトより大きくなります。なぜなら、使用するジェネリック関数の型ごとにコンパイルして出力するためです。
トレイトオブジェクトは、すべてのトレイトで作成できるとは限りません。トレイトのメソッドでSelfを定義している場合などは、トレイトオブジェクトを作成できません。
以上を踏まえて、基本的にはジェネリック関数を使えばよいですが、ある環境や状況ではトレイトオブジェクトを使った方が良い場合があるということだと思います。このへんは経験も必要になってきますね。

デフォルトメソッド

もし、そのトレイトのメソッドが既知の動作であれば、あらかじめデフォルトメソッドとしてメソッドの動作を実装できます。

mod mod_traits {
    ...
    pub trait Out2 {
        fn print_triple(&self);
        fn print_add_asterisk(&self);
        fn print_default(&self) {
            println!("call default method");
        }
    }
}

impl Out2 for Number {
    fn print_triple(&self) {
        println!("{}, {}, {}", self.num, self.num, self.num);
    }

    fn print_add_asterisk(&self) {
        println!("* {}", self.num);
    }
}

fn main() {
    ...
    // default method
    num.print_default();
 }

デフォルトメソッドを定義した場合、implブロックでメソッドを実装する必要はありません。

ジェネリックimplブロック

ジェネリックなimplブロックを定義することで、複数の型にトレイトを実装できます。
以下の例は、Out2トレイトの実装を、Outトレイトを実装した型に対して、一気に定義しています。

//impl Out2 for Number {
//    fn print_triple(&self) {
//        println!("{}, {}, {}", self.num, self.num, self.num);
//    }
//
//    fn print_add_asterisk(&self) {
//        println!("* {}", self.num);
//    }
//}

impl<O: Out+Debug> Out2 for O {
    fn print_triple(&self) {
        println!("{:?}, {:?}, {:?}", self, self, self);
    }

    fn print_add_asterisk(&self) {
        println!("* {:?}", self);
    }
}

先ほどOut2に関するimplブロックはコメントアウトしておく必要があります。なぜなら、NumberはすでにOutトレイトを実装しているため、重複してしまうためです。

Self

トレイトでも自分自身を表すためのSelfという特別な値があります。

mod mod_traits {
    ...
    pub trait Out3 {
        fn out_twice(&self) -> &Self;
    }
}

impl<O: Out> Out3 for O {
    fn out_twice(&self) -> &Self {
        self
    }
}

fn main() {
    ...
    // generic imple block
    num.print_triple();
    String::from("JUMP!").print_triple();
    println!("{}", get_type(num.out_twice()));
    println!("{}", get_type(String::from("JUMP!").out_twice()));
}

これは、関数out_twiceは引数と同じ型を返すことを表しています。

継承(サブトレイト)

あるトレイトが別のトレイトを継承することができます。トレイトの継承は以下のように書きます。

trait SubTrait: SuperTrait {
    fn method();
    ...
}

impl SuperTrait for Type {
    ...
}

impl SubTrait for Type {
    ...
}

このとき、implでは継承されるトレイトと継承するトレイトどちらのメソッドも定義する必要があります。
例を以下に示します。

mod mod_traits {
    ...
    pub trait Super {
        fn print_super(&self) {
            println!("SUPER trait");
        }
    }
    pub trait Sub: Super {
        fn print_sub(&self) {
            println!("SUB trait");
        }
    }
}

struct Unit;
impl Super for Unit {
}
impl Sub for Unit {
}

fn main() {
    ...
    // Inheritance
    let unit = Unit;
    unit.print_super();
    unit.print_sub();
}

継承は既存のトレイトにメソッドを追加したい場合などに便利です。

トレイトのスタティックメソッド

トレイトにもスタティックメソッドが定義できます。

mod mod_traits {
    ...
    pub trait Static {
        fn new() -> Self;
    }
}

impl Static for Number {
    fn new() -> Self {
        println!("Create new Number");
        Number {num: 0}
    }
}
impl Static for Vec<i32> {
    fn new() -> Self {
        println!("Create new Vec<i32>");
        vec![]
    }
}
impl Static for String {
    fn new() -> Self {
        println!("Create new String");
        String::new()
    }
}

fn main() {
    ...
    // staticmethod
    println!("{}", get_type(Number::new()));
    // call method
    println!("{}", get_type(<Vec<i32> as Static>::new()));
    println!("{}", get_type(<String as Static>::new()));
}

スタティックメソッドを用いればあらゆる型に対して同じ名前で、コンストラクタなどを実装できます。
main関数の下2行の呼び出しで、<Type as Trait>のような呼び出し方をしています。これは完全修飾メソッドと言います。VecやStringにはすでにnewメソッドが定義されているため、Numberの時と同じようにVec<i32>::new()String::new()と呼ぶと、既存のnewメソッドが呼ばれます。なので、Staticトレイトのnewメソッドを読んでほしいために完全修飾メソッドで書く必要がありました。

関連型

関連型は、あるトレイトを実装している型の中で別の型を使用したい場合に定義します。関連型の定義は以下のようになります。

trait Trait {
    type Associate;
}

このtype 関連型名が関連型です。
以下に例を示します。

mod mod_traits {
    ...
    pub trait Associate {
        type Item;

        fn get_zero(&self) -> Option<Self::Item>;
    }
}

これは、Associateトレイトの中のget_zeroメソッドは出力に引数の中のItemに関連付けられた型のOptionを返すという意味です。
このimplブロックは例えば以下のようになります。

impl Associate for Vec<i32> {
    type Item = i32;
    fn get_zero(&self) -> Option<Self::Item> {
        if self.is_empty() {
            None
        } else {
            Some(self[0])
        }
    }
}

fn generic_get_zero<V: Associate<Item=i32>>(v: &V) -> Option<V::Item> {
    println!("call generic function of Associate");
    v.get_zero()
}
fn trait_object_get_zero(v: &dyn Associate<Item=i32>) -> Option<i32> {
    println!("call trait object function of Associate");
    v.get_zero()
}

fn main() {
    ...
    // associated type
    println!("{:?}", vec![111].get_zero());
    println!("{:?}", generic_get_zero(&vec![111]));
    let v = vec![222];
    let asc: &dyn Associate<Item=i32> = &v;
    println!("{:?}", trait_object_get_zero(asc));
}

関連型Itemにはi32型が関連付けされています。
また、ジェネリック関数では関数定義時に関連付けします。
トレイトオブジェクトでは関数定義時とトレイトオブジェクト作成時にも関連付けしてあげる必要がありました。直感的にトレイトオブジェクトの場合は関数定義時は関連付けが必要ないのかなーと思っていたのですがエラーを吐きました。

ジェネリックトレイト

ジェネリックトレイトは、ある型に対して複数のトレイトを割り当てたい場合に便利です。
ジェネリックトレイトは以下のように定義します。

trait Trait<T> {
    ...
}

以下に例を示します。

mod mod_traits {
    ...
    pub trait GenericTrait<T=Self> {
        fn generic_trait(&self, other: &T);
    }
}

impl GenericTrait for Number {
    fn generic_trait(&self, other: &Self) {
        println!("call generic_trait self={:?}, other={:?}", self, other);
    }
}
impl GenericTrait<String> for Number {
    fn generic_trait(&self, other: &String) {
        println!("call generic_trait self={:?}, other={:?}", self, other);
    }
}

fn main() {
    ...
    // generic trait
    Number::new().generic_trait(&num);
    Number::new().generic_trait(&String::from("JUMP!"));
    // Number::new().generic_trait(&5); // Error: not implemented
}

まず、トレイトの定義でGenericTrait<T=Self>としています。これは、もしTの指定がない場合は、Self(つまり実装した型)をTに指定するという意味です。
implブロックではトレイトのTに指定しない場合と指定した場合を定義しています。
main関数でそれぞれの型を呼び出します。もし、実装されていない型、上記の場合i32型(5)を指定した場合はエラーとなります。

ソース

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

mod mod_traits {
    pub trait Out {
        fn print_twice(&self);
        fn print_add_doller(&self);
    }
    pub trait Out2 {
        fn print_triple(&self);
        fn print_add_asterisk(&self);
        fn print_default(&self) {
            println!("call default method");
        }
    }
    pub trait Out3 {
        fn out_twice(&self) -> &Self;
    }
    pub trait Super {
        fn print_super(&self) {
            println!("SUPER trait");
        }
    }
    pub trait Sub: Super {
        fn print_sub(&self) {
            println!("SUB trait");
        }
    }
    pub trait Static {
        fn new() -> Self;
    }
    pub trait Associate {
        type Item;

        fn get_zero(&self) -> Option<Self::Item>;
    }
    pub trait GenericTrait<T=Self> {
        fn generic_trait(&self, other: &T);
    }
}

use mod_traits::{Out, Out2, Out3, Super, Sub, Static, Associate, GenericTrait};

#[derive(Debug)]
struct Number {
    num: i32
}

impl Out for Number {
    fn print_twice(&self) {
        println!("{}, {}", self.num, self.num);
    }

    fn print_add_doller(&self) {
        println!("$ {}", self.num);
    }
}

impl Out for String {
    fn print_twice(&self) {
        println!("{}, {}", self, self);
    }

    fn print_add_doller(&self) {
        println!("$ {}", self);
    }
}

//impl Out2 for Number {
//    fn print_triple(&self) {
//        println!("{}, {}, {}", self.num, self.num, self.num);
//    }
//
//    fn print_add_asterisk(&self) {
//        println!("* {}", self.num);
//    }
//}

impl<O: Out+Debug> Out2 for O {
    fn print_triple(&self) {
        println!("{:?}, {:?}, {:?}", self, self, self);
    }

    fn print_add_asterisk(&self) {
        println!("* {:?}", self);
    }
}

impl<O: Out> Out3 for O {
    fn out_twice(&self) -> &Self {
        self
    }
}

struct Unit;
impl Super for Unit {
}
impl Sub for Unit {
}

impl Static for Number {
    fn new() -> Self {
        println!("Create new Number");
        Number {num: 0}
    }
}
impl Static for Vec<i32> {
    fn new() -> Self {
        println!("Create new Vec<i32>");
        vec![]
    }
}
impl Static for String {
    fn new() -> Self {
        println!("Create new String");
        String::new()
    }
}

impl Associate for Vec<i32> {
    type Item = i32;
    fn get_zero(&self) -> Option<Self::Item> {
        if self.is_empty() {
            None
        } else {
            Some(self[0])
        }
    }
}
fn generic_get_zero<V: Associate<Item=i32>>(v: &V) -> Option<V::Item> {
    println!("call generic function of Associate");
    v.get_zero()
}
fn trait_object_get_zero(v: &dyn Associate<Item=i32>) -> Option<i32> {
    println!("call trait object function of Associate");
    v.get_zero()
}

impl GenericTrait for Number {
    fn generic_trait(&self, other: &Self) {
        println!("call generic_trait self={:?}, other={:?}", self, other);
    }
}
impl GenericTrait<String> for Number {
    fn generic_trait(&self, other: &String) {
        println!("call generic_trait self={:?}, other={:?}", self, other);
    }
}

fn trait_object_out(o: &dyn Out) {
    o.print_twice();
    o.print_add_doller();
}

use std::fmt::Debug;
fn generic_out<O: Out+Debug>(o: &O) {
    o.print_twice();
    o.print_add_doller();
    println!("{:?}", o);
}
fn generic_where_out<'a, O, O2>(o: &'a O, o2: &'a O2)
    where O: Out+Debug,
          O2: Out2+Debug
{
    o.print_twice();
    o.print_add_doller();
    println!("{:?}", o);
    o2.print_triple();
    o2.print_add_asterisk();
    println!("{:?}", o2);
}

fn main() {
    let num = Number {num: 5};
    num.print_twice();      // 5, 5
    num.print_add_doller(); // $ 5
    String::from("JUMP!").print_twice();          // JUMP!, JUMP!
    String::from("cargo run").print_add_doller(); // $ cargo run
     // trait object
    let out: &dyn Out = &num;
    out.print_twice();      // 5, 5
    out.print_add_doller(); // $ 5
    trait_object_out(out);
    println!("size = {}", std::mem::size_of::<&dyn Out>()); // size = 16(data=8, table=8)
    // generics
    generic_out(&num);
    generic_where_out(&num, &num);
    // default method
    num.print_default();
    // generic imple block
    num.print_triple();
    String::from("JUMP!").print_triple();
    println!("{}", get_type(num.out_twice()));
    println!("{}", get_type(String::from("JUMP!").out_twice()));
    // Inheritance
    let unit = Unit;
    unit.print_super();
    unit.print_sub();
    // staticmethod
    println!("{}", get_type(Number::new()));
    // call method
    println!("{}", get_type(<Vec<i32> as Static>::new()));
    println!("{}", get_type(<String as Static>::new()));
    // associated type
    println!("{:?}", vec![111].get_zero());
    println!("{:?}", generic_get_zero(&vec![111]));
    let v = vec![222];
    let asc: &dyn Associate<Item=i32> = &v;
    println!("{:?}", trait_object_get_zero(asc));
    // generic trait
    Number::new().generic_trait(&num);
    Number::new().generic_trait(&String::from("JUMP!"));
    // Number::new().generic_trait(&5); // Error: not implemented
}
5, 5
$ 5
JUMP!, JUMP!
$ cargo run
5, 5
$ 5
5, 5
$ 5
size = 16
5, 5
$ 5
Number { num: 5 }
5, 5
$ 5
Number { num: 5 }
Number { num: 5 }, Number { num: 5 }, Number { num: 5 }
* Number { num: 5 }
Number { num: 5 }
call default method
Number { num: 5 }, Number { num: 5 }, Number { num: 5 }
"JUMP!", "JUMP!", "JUMP!"
&main::Number
&alloc::string::String
SUPER trait
SUB trait
Create new Number
main::Number
Create new Vec<i32>
alloc::vec::Vec<i32>
Create new String
alloc::string::String
Some(111)
call generic function of Associate
Some(111)
call trait object function of Associate
Some(222)
Create new Number
call generic_trait self=Number { num: 0 }, other=Number { num: 5 }
Create new Number
call generic_trait self=Number { num: 0 }, other="JUMP!"

今回はここまでー。
パニックですよ!トレイトむずー。特にトレイトのジェネリック関数にジェネリックimplブロックにジェネリックトレイトが、それぞれどのような役割をはたすんやったっけ?ってなります。これも使って慣れるしかないです。頑張りましょう!

25
20
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
25
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?