Rust: オブジェクト指向からの移行
派生と継承
Rust の構造体には派生/継承という機能がないが、いくつかの手法を使って同等のことができる。
trait
を使う
Rust の trait
は interface
に相当する機能で、構造体に対するメソッドの実装を強制することができる。
// Java
interface X {
public String hello();
}
class A implements X {
public String hello() { return "Good morning"; }
}
class B implements X {
public String hello() { return "Good afternoon"; }
}
// Rust
trait X {
fn hello(&self) -> &'static str;
}
struct A {}
impl X for A {
fn hello(&self) -> &'static str { "Good morning" }
}
struct B {}
impl X for B {
fn hello(&self) -> &'static str { "Good afternoon" }
}
trait
は一見して interface
のように高い抽象度を持つ最良の方法に見えるかもしれないが、Rust においては borrowing や lifetime、Sized
といった制限により、オブジェクト指向言語と同じ感覚では少し煩雑さや扱いづらさを感じるかもしれない。
// 関数に渡すときは dyn 付きの参照として渡す必要がある。
fn foo(x: & dyn X) { unimplemented!() }
fn x_factory() -> Box<dyn X> { unimplemented!() }
enum
付きの構造体を使う
オブジェクト指向言語でなくても異なる型のオブジェクトに共通の情報をもたせ統一的に操作したいということはよくあることである。
- あるクラス X から派生した A と B というクラスがある。
- クラス X には A, B 共通のメンバーが定義されている。
- A と B の抽象表現 X のみに注目して統一的に扱いたい。
struct
と enum
を使用する。
構造体
A
とB
が部分的に同じメンバーとメソッドを持ち、一部が異なっているような構造を表す。
// Java
abstract class X {
private int field1;
private String field2;
public abstract String hello();
}
class A extends X {
private int fieldA;
public String hello() { return "Good morning"; }
}
class B extends X {
private long fieldB;
public String hello() { return "Good afternoon"; }
}
// Rust
struct X {
field1: u32,
field2: String,
field3: Variant,
}
impl X {
pub fn hello(&self) -> &str {
match self.field3 {
Variant::A { .. } => "Good morning",
Variant::B { .. } => "Good afternoon",
}
}
}
enum Variant {
A { field_a: u32 },
B { field_b: u64 }
}
同じメソッドでも継承先によって実装 (振る舞い) を変える多態化は、enum
型に対する match
で分岐する方法で代用する。