スーパー忙しい人向け
- Rustの
trait
はabstract 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 { ... }
}
なんらかの書き込み/読み込み手段を持つことを宣言できます。
それぞれ単一の目的で実装されるため、必要な方もしくは両方を選択して利用できます。
Write
はwrite
マクロなどに利用されます。(Display
の実装など)
最後に
この記事は特にこれといった根拠なく執筆しているので、間違っている部分が含まれている可能性があります。
rust-by-exampleを読んで不安になりました。