14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustのtraitは抽象クラスではない

Posted at

スーパー忙しい人向け

  • Rustのtraitabstract classではない。
  • Is XXXではない!Can XXXだ!

抽象クラスについて

この記事で扱う抽象クラスとは、Java等に存在するabstract classのことを指し示しています。
抽象的なクラスを定義する概念であり、〇〇はXXである。はXXである。の部分が抽象クラスという体でこの記事を執筆しています。
この記事はJavaやabstract class、およびその利用者を中傷する意図はありません。

こんなtrait宣言していませんか?

// 例
trait Person {
    fn walk(&self);
    fn run(&self) -> Result<(), PersonError>;
    fn speak(&self, content);
    fn sleep(&mut self) -> Result<bool, PersonError>;
}

これ、間違っています。

それ、traitを抽象クラスと勘違いしていませんか?
trait何が出来るかを示すためのものです。


// 例
trait IoStream {
    type Output;

    fn input(&mut self) -> Result<(), IoStreamError>;
    fn output(&self) -> Result<Self::Output, IoStreamError>;
}

#[derive(Default)]
struct FileStream {
    buffer: Vec<u8>,
};

impl IoStream for FileStream {
    type Output = Vec<u8>;
    
    fn input(&mut self) -> Result<(), IoStreamError> {
        fsys::read_file(&mut self.buffer).map_err(|e| error_handle!(e))
    }

    fn output(&self) -> Result<Self::Output, IoStreamError> {
        fsys::write_file(&self.buffer).map_err(|e| error_handle!(e))
    }
}

#[derive(Default)]
struct ShellStream {
    attach_shell: Shell,
};

impl IoStream for ShellStream {
    type Output = String;

    fn input(&mut self) -> Result<(), IoStreamError> {
        let handler = self.attach_shell.blocking();
        handler.pipe().map_err(|e| error_handle!(e))
    }

    fn output(&self) -> Result<Self::Output, IoStreamError> {
        self.attach_shell.out().map_err(|e| error_handle!(e))
    }
}

これ、間違っています。

ioとくくられているだけでInputとOutputは別の目的で利用します。
1つのtraitの利用目的は1つに定めるべきです。

どうすればいいのか?

1つのtraitは1種類の機能のみを持たせるべきです。

なぜなら、traitとは〇〇が出来るということを宣言するための機能であるためです。
決して〇〇であるということではありません

〇〇であるということ

trait Person { ... }

struct John;

// John is Person
impl Person for John { ... }

これは主に保守性に影響します。
特に根本的な違いがある処理をtraitでまとめて共通化などをすると起こりやすく、大本のtraitに依存した構造になるため規模が大きくなると改変がとても難しくなります。

難しくなっちゃった例(ChatGPTで生成しました)

trait Entity {
    fn name(&self) -> &str;
    fn move_to(&mut self, x: i32, y: i32);
    fn attack(&mut self, target: &mut dyn Entity) -> Result<(), String>;
    fn take_damage(&mut self, amount: i32);
    fn speak(&self, message: &str);
    fn save_state(&self) -> String;
    fn load_state(&mut self, data: &str);
}

struct Player {
    name: String,
    x: i32,
    y: i32,
    hp: i32,
}

// Player is Entity
// 実装は考えたくもない
impl Entity for Player { ... }

要するに抽象クラスと同じ使い方をすると危険ということです。

より素晴らしい方法に修正するのは簡単です。
対象の持つすべてのメソッドを1つずつ分解し、既存のtraitやクレート内の共通化出来る機能でくくって実装するだけです。

traitは所詮機能なので、抽象クラスのように実装を前提としたロジックにする必要はありません!
保守作業で共通化出来そうにないと思えば、実装を外して独自実装に切り替えることが出来ます!

具体例

上記の間違いを修正してみます。

trait Person {
    fn walk(&self);
    fn run(&self) -> Result<(), PersonError>;
    fn speak(&self, content);
    fn sleep(&mut self) -> Result<bool, PersonError>;
}

Personは見る限り人の定義であり、traitとしてはふさわしくありません。
機能ごとに分割したtraitとして定義しましょう。

trait MoveFoot {
    fn walk(&self);
    fn run(&self) -> Result<(), PersonError>;
}

trait Speak {
    fn speak(&self, content);
}

trait Sleep {
    fn sleep(&mut self) -> Result<bool, PersonError>;
}

struct Person;

impl MoveFoot for Person { ... }
impl Speak for Person { ... }
impl Sleep for Person { ... }

trait IoStream {
    type Output;

    fn input(&mut self) -> Result<(), IoStreamError>;
    fn output(&self) -> Result<Self::Output, IoStreamError>;
}

#[derive(Default)]
struct FileStream {
    buffer: Vec<u8>,
};

impl IoStream for FileStream {
    type Output = Vec<u8>;
    
    fn input(&mut self) -> Result<(), IoStreamError> {
        fsys::read_file(&mut self.buffer).map_err(|e| error_handle!(e))
    }

    fn output(&self) -> Result<Self::Output, IoStreamError> {
        fsys::write_file(&self.buffer).map_err(|e| error_handle!(e))
    }
}

#[derive(Default)]
struct ShellStream {
    attach_shell: Shell,
};

impl IoStream for ShellStream {
    type Output = String;

    fn input(&mut self) -> Result<(), IoStreamError> {
        let handler = self.attach_shell.blocking();
        handler.pipe().map_err(|e| error_handle!(e))
    }

    fn output(&self) -> Result<Self::Output, IoStreamError> {
        self.attach_shell.out().map_err(|e| error_handle!(e))
    }
}

I/Oが両方必要な場面は限られます。
I/Oにまとめるのはモジュール単位で十分です。

trait Input {
    fn input(&mut self) -> Result<(), IoStreamError>;
}

trait Output {
    type Output;

    fn output(&self) -> Result<Self::Output, IoStreamError>;
}

impl Input for T { ... }
impl Output for T { ... }

impl Input for S { ... }
impl Output for S { ... }

ChatGPTに生成してもらったやつです。

trait HasName {
    fn name(&self) -> &str;
}

trait Movable {
    fn move_to(&mut self, x: i32, y: i32);
}

trait Combat {
    fn attack(&mut self, target: &mut dyn TakeDamage) -> Result<(), String>;
}

trait TakeDamage {
    fn take_damage(&mut self, amount: i32);
}

trait Speak {
    fn speak(&self, message: &str);
}

trait SaveLoad {
    fn save_state(&self) -> String;
    fn load_state(&mut self, data: &str);
}

struct Player {
    name: String,
    x: i32,
    y: i32,
    hp: i32,
}

impl HasName for Player {
    fn name(&self) -> &str {
        &self.name
    }
}

impl Movable for Player {
    fn move_to(&mut self, x: i32, y: i32) {
        self.x = x;
        self.y = y;
    }
}

impl Combat for Player {
    fn attack(&mut self, target: &mut dyn TakeDamage) -> Result<(), String> {
        target.take_damage(10);
        Ok(())
    }
}

impl TakeDamage for Player {
    fn take_damage(&mut self, amount: i32) {
        self.hp -= amount;
    }
}

impl Speak for Player {
    fn speak(&self, message: &str) {
        println!("{} says: {}", self.name, message);
    }
}

impl SaveLoad for Player {
    fn save_state(&self) -> String {
        format!("{},{},{},{}", self.name, self.x, self.y, self.hp)
    }

    fn load_state(&mut self, data: &str) {
        let parts: Vec<&str> = data.split(',').collect();
        self.name = parts[0].to_string();
        self.x = parts[1].parse().unwrap();
        self.y = parts[2].parse().unwrap();
        self.hp = parts[3].parse().unwrap();
    }
}

私が間違っているかもしれません。

stdのtrait

以下、stdで宣言されている一部のtraitの簡単な紹介です。

Display

pub trait Display {
    // Required method
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}

Displayを実装すると、フォーマット可能であることを宣言できます。
printlnマクロやformatマクロで直接利用するにはDisplayが実装されている必要があります。

ToString

pub trait ToString {
    // Required method
    fn to_string(&self) -> String;
}

ToStringを実装すると、Stringに変換可能であることを宣言できます。
また、Displayを実装すると自動で実装されるので、必要な場合のみToStringを実装してください。

Write / Read

pub trait Write {
    // Required methods
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;

    // Provided methods
    fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result<usize> { ... }
    fn is_write_vectored(&self) -> bool { ... }
    fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... }
    fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<()> { ... }
    fn write_fmt(&mut self, fmt: Arguments<'_>) -> Result<()> { ... }
    fn by_ref(&mut self) -> &mut Self
       where Self: Sized { ... }
}

pub trait Read {
    // Required method
    fn read(&mut self, buf: &mut [u8]) -> Result<usize>;

    // Provided methods
    fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> Result<usize> { ... }
    fn is_read_vectored(&self) -> bool { ... }
    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize> { ... }
    fn read_to_string(&mut self, buf: &mut String) -> Result<usize> { ... }
    fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> { ... }
    fn read_buf(&mut self, buf: BorrowedCursor<'_>) -> Result<()> { ... }
    fn read_buf_exact(&mut self, cursor: BorrowedCursor<'_>) -> Result<()> { ... }
    fn by_ref(&mut self) -> &mut Self
       where Self: Sized { ... }
    fn bytes(self) -> Bytes<Self>
       where Self: Sized { ... }
    fn chain<R: Read>(self, next: R) -> Chain<Self, R>
       where Self: Sized { ... }
    fn take(self, limit: u64) -> Take<Self>
       where Self: Sized { ... }
}

なんらかの書き込み/読み込み手段を持つことを宣言できます。
それぞれ単一の目的で実装されるため、必要な方もしくは両方を選択して利用できます。
Writewriteマクロなどに利用されます。(Displayの実装など)

最後に

この記事は特にこれといった根拠なく執筆しているので、間違っている部分が含まれている可能性があります。
rust-by-exampleを読んで不安になりました。

14
5
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
14
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?