Help us understand the problem. What is going on with this article?

Rustにおけるオブジェクト指向の考え方

More than 5 years have passed since last update.

トレイト

Rustにクラスやプロトタイプチェーンは存在しない。そのかわり、トレイトを使ってオブジェクト指向プログラミングを実現する。トレイトはメソッドを集めたものだ。トレイトを型に実装することによって、その型の値がメソッドを提供するようになる。

トレイトの定義

say_helloメソッドを提供するSayHelloトレイトを作ってみよう。

trait SayHello : std::fmt::Display {
    fn say_hello(&self) {
        println!("Hello. This is {}.", self);
    }
}

std::fmt::Displayとは何かというと、これもトレイトだ。SayHello : std::fmt::Displayというのは、SayHelloトレイトを実装するには、まずstd::fmt::Displayトレイトを実装する必要があるということを表している。これは、整形表示するのに必要なトレイトだ。

say_helloメソッドの第一引数は&selfとなっている。これは、レシーバーへのリファレンスを受け取ることを表している。say_helloメソッドにはデフォルトの実装が定義されている。デフォルトの実装は必ずしも定義されなくても良いが、その場合は、型にトレイトを実装する際に定義する必要がある。

トレイトの実装

implを使って、型にトレイトを実装できる。

impl SayHello for i32 {}
impl SayHello for str {
    fn say_hello(&self) {
        println!("Hi. I'm {}.", self);
    }
}

str型については、デフォルトと異なる実装を定義している。

使ってみる。

trait SayHello : std::fmt::Display {
    fn say_hello(&self) {
        println!("Hello. This is {}.", self);
    }
}

impl SayHello for i32 {}
impl SayHello for str {
    fn say_hello(&self) {
        println!("Hi. I'm {}.", self);
    }
}

fn main() {
    42.say_hello();
    "Alice".say_hello();
}
Hello. This is 42.
Hi. I'm Alice.

トレイト制約

ジェネリックの型変数がトレイトを実装していることを表したいときは、トレイト制約を使う。

fn greeting<T: SayHello>(x: T) {
    x.say_hello();
}

<T: SayHello>の部分が、型変数TSayHelloを実装していることを表す。

+で、型が複数のトレイトを実装していることを表すことができる。

fn greeting<T: SayHello + SayGoodbye>(x: T) {
    x.say_hello();
    x.say_goodbye();
}

where節を使った書き方もできる。

fn greeting<T>(x: T) where T: SayHello + SayGoodbye {
    x.say_hello();
    x.say_goodbye();
}

Uniform Function Call Syntax

メソッドは関数として使うこともできる。

fn greeting<T>(x: T) where T: SayHello + SayGoodbye {
    <T as SayHello>::say_hello(&x);
    <T as SayGoodbye>::say_goodbye(&x);
}

<T as R>::method(args)という書き方は、型Tが実装しているトレイトRのメソッドmethodを関数として書いたものだ。このような記法(UFCS)は、複数のトレイトが同名のメソッドを提供していた場合に、曖昧さを無くすために使うことが出来る。

Sizedトレイト

Sizedは特別なトレイトで、strや後述のオブジェクト型のような、型からサイズが決定できない型以外が実装しており、暗黙のうちに型変数の型制約として加わっている。型変数がstrやトレイト型を取ることができるようにするためには、この型制約を取り除かなければならない。そのための記法が?Sizedだ。

リファレンス型に対してSayHelloメソッドを実装する。

impl<'a, T: ?Sized + SayHello> SayHello for &'a T {
    fn say_hello(&self) {
        <T as SayHello>::say_hello(self);
    }
}

このように定義することで、&str型もSayHelloトレイトを実装していることになる。

トレイトオブジェクト

トレイトオブジェクトは、仮想メソッドテーブル(vtable)を使った動的ディスパッチができる型だ。トレイトRを実装した型Tへのリファレンス&TBox<T>&RあるいはBox<R>にキャストすることで、トレイトオブジェクトに変換できる。

Box::new(42) as Box<SayHello>

異なる型のデータも、トレイトオブジェクトに変換することで同じコンテナーに入れられるようになる。

trait SayHello : std::fmt::Display {
    fn say_hello(&self) {
        println!("Hello. This is {}.", self);
    }
}

impl SayHello for i32 {}
impl SayHello for str {
    fn say_hello(&self) {
        println!("Hi. I'm {}.", self);
    }
}
impl<'a, T: ?Sized + SayHello> SayHello for &'a T {
    fn say_hello(&self) {
        <T as SayHello>::say_hello(self);
    }
}

fn main() {
    let v = vec![
        Box::new(42) as Box<SayHello>,
        Box::new("Alice") as Box<SayHello>,
    ];
    for x in &v {
        x.say_hello();
    }
}

オブジェクト安全なトレイトについてのみトレイトオブジェクトに変換できる。バイナリメソッド(レシーバー以外の引数にSelf型の値をとるメソッド)を持つトレイトなどは、オブジェクト安全でないので、トレイトオブジェクトに変換できない。

fn main() {
    let _ = Box::new(42) as Box<std::cmp::Eq>; // ERROR
}

オブジェクト安全の詳しい定義はオブジェクト安全#Detailed designを参照すること。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away