Rustに興味が沸いてちょっと触ってみようと思ってまずはドキュメントを読んでみたら、classがないことに驚かれると思います。
Rustはstructによってオブジェクトを作成していくので最初に抵抗を感じるかもしれません。
この記事では、オブジェクト指向で開発してきたけどいきなりRustの書き方に慣れていくのはきつそう・・と感じる方を対象に紹介して行きたいと思います。
目標にしたいこと
オブジェクト指向経験者と見出しにつけていますが、Rustではオブジェクト指向をしないというわけではなくその他の言語において広く使われているオブジェクト指向プログラミングに慣れている方がRustの機能によってやりたい事を実現できるようにすると言った所を目指しています。
そのため、記事中にはオブジェクト指向に関する応用的なRustにおける実装例は含まれておらず、置き換え例やパッケージ管理など広い範囲での紹介になっていきます。
Rustにはclassがない
Rustにはclassがないため、代わりに struct, impl, trait を使っていきます。
struct Apple {
size: u64
}
structは、
let apple = Apple { size: 1 };
とすることで、オブジェクトを得ることが出来ます。
しかし、structは実装を持つことが出来ないため中に関数を書くことは出来ません。これではオブジェクトは作れても、そのオブジェクトの中に関数がないためオブジェクト指向に慣れてると関数ポインタでなんとか・・・なんて悩んでしまうところだとおもいますが、こういった場合は impl を使います。
impl
implは、struct上で動作する関数を定義することが出来ます。
struct Apple {
size: u64
}
impl Apple {
fn get_size(&self) -> u64 {
self.size
}
}
fn はfunctionの略で、関数の定義を行っています。implは、structと同名にすることで structのAppleに対して実装(Implementation)を行っています。
これにより、
let apple = Apple { size: 1 };
let size = apple.get_size();
データと関数の部分が一緒となったオブジェクトとして扱えるようになります。
抽象化
struct から 実装を持ったobjectを作成することが出来るようになったので、条件を満たさなければobjectの生成をすることができないようにする仕組みを作っていきます。
trait Fruit {
fn get_size(&self) -> u64;
}
struct Apple {
size: u64
}
impl Fruit for Apple {
fn get_size(&self) -> u64 {
self.size
}
}
この時にtraitを使います。traitは構造体のようにobjectを作ることが出来ません。implも少し変更して、 traitを用いてAppleに対して実装を行っています。これによりget_sizeという関数を定義しなければエラーになります。
しかし問題がありまして、abstruct class や interface, virtual による抽象クラスなどで実現できていたメンバ変数はtraitには持たせることが出来ません。あくまで指定の関数の実装を強制させることしか出来ず、しかもその実装はstruct毎のimplの実装によって変えることが可能なので注意が必要です。
trait object
traitだけでは、オブジェクト指向にあるような抽象クラスとの関係性を作るには難しく感じます。そこで、trait objectによってポリモーフィズムを実現する方法をご紹介します。traitではディスパッチという仕組みによってどの関数が呼ばれるか挙動を変える事ができます。
use std::io;
use std::io::prelude::*;
trait Fruit {
fn get_size(&self) -> u64;
}
struct Apple {
size: u64,
}
struct Pine {
size: u64,
}
impl Fruit for Apple {
fn get_size(&self) -> u64 {
println!("apple");
self.size
}
}
impl Fruit for Pine {
fn get_size(&self) -> u64 {
println!("pine");
self.size
}
}
fn main() {
println!("1:Apple? 2:Pine?");
let stdin = io::stdin();
let mut input = String::new();
stdin.read_line(&mut input);
let input = input.trim();
let apple = Apple { size: 1 };
let pine = Pine { size: 2 };
let fruit: &Fruit = match input.as_ref() {
"1" => &apple as &Fruit,
"2" => &pine as &Fruit,
_ => panic!("error"),
};
println!("size: {}", fruit.get_size());
}
このコードはキーボード入力の待機状態になり1が入力された場合はApple、2が入力された場合はPineが返るコードです。
as &Fruit
という部分に注目して頂きたいのですが、これがTrait Objectになります。このように動的な処理でもAppleとPineを同じFruitとして扱い共通の処理を行いポリモーフィズムを実現させています。
継承に近いことがやりたい時
struct Apple {
size: u64,
}
struct PineApple {
size: u64,
}
trait AppleTrait {
fn get_size(&self) -> u64;
}
trait PineAppleTrait: AppleTrait { }
impl AppleTrait for Apple {
fn get_size(&self) -> u64 {
println!("apple");
self.size
}
}
impl AppleTrait for PineApple {
fn get_size(&self) -> u64 {
println!("pine apple");
self.size
}
}
impl PineAppleTrait for PineApple {}
fn main() {
let apple = Apple { size: 1 };
let pine_apple = PineApple { size: 2 };
println!("size: {}", apple.get_size());
println!("size: {}", pine_apple.get_size());
}
このコードでは、オブジェクトが作成可能な2つのstructを用意しました。りんごとパイナップルにどういった関係があるのかは気にしないで下さい。
注目する部分は trait PineAppleTrait: AppleTrait { }
の部分です。このようにすることでPineAppleTraitは、AppleTraitも実装しなければなりません。
そのために、
impl AppleTrait for PineApple {
fn get_size(&self) -> u64 {
println!("pine apple");
self.size
}
}
がないと、コンパイルエラーになります。impl PineAppleTrait for PineApple {}
の部分に関しては特にPineApple側で実装しなければいけないものがないため削除してもエラーにはなりません。
依存関係は作れましたが、実装をそれぞれで行わないといけないため本当にPineAppleはAppleの性質を持っているのかという部分がちょっと怪しいなと感じると思います。
番外編: traitにデフォルトの実装を持たせ処理を共通化させる
struct Fruit {
size: u64,
}
struct Apple {
size: u64,
}
struct PineApple {
size: u64,
}
trait FruitTrait {}
trait AppleTrait: FruitTrait {
fn get_size(&self) -> u64 {
let a: Fruit = self.as_fruit();
a.size
}
fn as_fruit(&self) -> Fruit;
}
trait PineAppleTrait: AppleTrait { }
impl FruitTrait for Apple {}
impl AppleTrait for Apple {
fn as_fruit(&self) -> Fruit {
Fruit { size: self.size }
}
}
impl FruitTrait for PineApple {}
impl AppleTrait for PineApple {
fn as_fruit(&self) -> Fruit {
Fruit { size: self.size }
}
}
impl PineAppleTrait for PineApple {}
fn main() {
let apple = Apple { size: 1 };
let pine_apple = PineApple { size: 2 };
println!("size: {}", apple.get_size());
println!("size: {}", pine_apple.get_size());
}
ちょっと強引な方法なのでおすすめできませんが、traitにデフォルトの実装を持たせることができる仕様があります。この仕様を応用して、traitに共通の処理を書くことが出来ます。
get_size
が一箇所にしか定義されていないのがポイントになります。そして、trait側では&self
が何の型なのかわからないため(一応matchを使うことで判断することが出来ます)、as_fruit
によって、traitの依存関係の最上位にあたる
Fruitに変換しFruitに定義されたsizeを取得しています。
番外編その2: Trait境界を利用した実装の共通化
struct Object {
size: u64,
}
struct Apple {
size: u64,
}
struct Pen {
size: u64,
}
trait ObjectTrait {
fn as_object(&self) -> Object;
}
impl ObjectTrait for Apple {
fn as_object(&self) -> Object {
Object { size: self.size }
}
}
impl ObjectTrait for Pen {
fn as_object(&self) -> Object {
Object { size: self.size }
}
}
trait ObjectLogic {
fn get_size(&self) -> u64;
}
impl<T: ObjectTrait> ObjectLogic for T {
fn get_size(&self) -> u64 {
let a = self.as_object();
a.size
}
}
fn main() {
let apple = Apple { size: 1 };
println!("size: {}", apple.get_size());
let pen = Pen { size: 2 };
println!("size: {}", pen.get_size());
}
こちらは、implにてジェネリクスを用いてObjectTraitが実装されているstructのみに対してObjectLogicを実装する流れになっています。
このようにTraitの境界で実装を型ごとに絞り込んでいくことができます。
処理を隠蔽したい
Rustのstructのメンバは、モジュールにすることでprivateにすることが出来ます。Rustのモジュールシステムは
pub mod hoge {
pub struct Hoge {
pub a: i64,
}
}
fn main() {
let hoge = hoge::Hoge { a: 1 };
println!("a: {}", hoge.a);
}
このように書くことが出来ます。pub は publicの略で、 mod単位、struct単位、メンバ単位など細かく公開範囲を分けることが可能で、デフォルトはprivateです。
ただ実際はディレクトリで分ける場合が多いです。
.
├── hoge
│ ├── hoge.rs
│ └── mod.rs
└── main.rs
pub mod hoge;
pub fn a() -> i64 {
1
}
mod hoge;
fn main() {
let a = hoge::hoge::a();
println!("a: {}", a);
}
ディレクトリに置かれたmod.rs
が、モジュールのインクルードを行います。
moduleはちょうど名前空間のように働きますが、公開範囲があるのが特徴です。そして、mod hoge;
のようにmainで読み込まれた時に初めて実行データとして組み込まれる仕組みになっています。
このモジュールの仕組みにより隠蔽が可能になります。
crateの作成について
モジュールシステムがありますが、crateという仕組みがあります。これはライブラリやパッケージのようなものです。
Rustでの開発は、一つ一つのクレートを小さく作りCargo.tomlに依存関係を書いていくことで作りあげていくパターンが多いようなので小さい機能だけを持ったクレートがcrates.ioに数多く登録されています。クレートを作ると聞くと抵抗がありますが、アップロードしなくてもローカルで完結させることも出来ます。
.
├── a
│ ├── Cargo.lock
│ ├── Cargo.toml
│ ├── src
│ └── target
└── b
├── Cargo.toml
└── src
このような構成だとして、a
側でb
を読み込んでみます。
[package]
name = "a"
version = "0.1.0"
authors = ["authors"]
[dependencies]
b = { path = "../b/" }
これだけで実行してみても、それぞれコンパイルされていることがわかります。
あとは a/src/main.rs
側で extern crate b;
とするとモジュールと同じように使うことが出来ます。
依存関係の整理にもなり、コンパイル時間の短縮の効果もあるのでおすすめです。
extern crate b;
fn main() {
println!("Hello, world!");
}
[追記]番外編その3: 内包関係を表す
これまで説明した内容はis-a関係を表す物でした。この他にもRustではhas-a関係を表現する方法があります。
#[derive(Debug)]
struct Body;
#[derive(Debug)]
struct Chassis;
#[derive(Debug)]
struct Engine;
#[derive(Debug)]
struct WagonBody;
#[derive(Debug)]
struct WagonChassis;
#[derive(Debug)]
struct WagonEngine;
#[derive(Debug)]
struct Car<BodyT, ChassisT, EngineT> {
body: BodyT,
chassis: ChassisT,
engine: EngineT,
}
trait CarTrait {
fn get_info(&self) -> &str;
}
impl CarTrait for Car<Body, Chassis, Engine> {
fn get_info(&self) -> &str {
"simple car!"
}
}
impl CarTrait for Car<WagonBody, WagonChassis, WagonEngine> {
fn get_info(&self) -> &str {
"wagon car!"
}
}
fn main() {
let car: Car<Body, Chassis, Engine> = Car {
body: Body {},
chassis: Chassis {},
engine: Engine {},
};
println!("{:?}", car);
println!("{}", car.get_info());
let wagon_car: Car<WagonBody, WagonChassis, WagonEngine> = Car {
body: WagonBody {},
chassis: WagonChassis {},
engine: WagonEngine {},
};
println!("{:?}", wagon_car);
println!("{}", wagon_car.get_info());
let broken_car: Car<Body, WagonChassis, WagonEngine> = Car {
body: Body {},
chassis: WagonChassis {},
engine: WagonEngine {},
};
println!("{:?}", broken_car);
// println!("{}", broken_car.get_info());
//=> no method named `get_info`
}
Rustにおける内包は、
Car<Body, Chassis, Engine>
のような型のものになります。例えば通信を行うライブラリでは Request<Body>
といった型で扱わていたりします。
このコードのポイントとしては、
struct Car<BodyT, ChassisT, EngineT> {
body: BodyT,
chassis: ChassisT,
engine: EngineT,
}
trait CarTrait<BodyT, ChassisT, EngineT> {
fn get_info(&self) -> &str;
}
Car自体はジェネリクスを使っている所です。わかりやすくするために後ろにTをつけています。
このCarに対して、ジェネリクスに当て込んだ型に応じて部品の型に見合った実装に変えていく流れになっています。
Car<Body, Chassis, Engine>
という型の場合 get_info
は simple car!
を返し、 Car<WagonBody, WagonChassis, WagonEngine>
型の場合は "wagon car!" を返します。
この2つの実装は、同じTraitで型を分けることによって実現していますが、空を飛ぶことができるFlyCarが登場した場合は、FlyCarTraitに実装を分けることでfn fly
を実装することも出来ます。
また、車と部品の関係は has-one になり、駐車場と車の関係の場合は has-many になりますがこの場合配列として扱うことで Parking<CarT>
と表現することが出来ます。しかしhas-manyの場合、これだとWagonCar専用駐車場のような表現になるので現実的ではありません。
複数の車種を駐車場に対応させるにはBox<Any>
を使います。これにより型の異なるオブジェクトでも配列として扱うことが出来ます。
use std::any::Any;
#[derive(Debug)]
struct Parking {
cars: Vec<Box<Any>>,
capacity_limit: u64,
current_capacity: u64,
}
trait ParkingTrait {
fn size(&self) -> usize;
fn get_current_capacity(&self) -> u64;
fn enter_simple(&mut self, car: Car<Body, Chassis, Engine>) -> ();
fn enter_wagon(&mut self, car: Car<WagonBody, WagonChassis, WagonEngine>) -> ();
}
impl ParkingTrait for Parking {
fn get_current_capacity(&self) -> u64 {
self.current_capacity
}
fn size(&self) -> usize {
self.cars.len()
}
fn enter_simple(&mut self, car: Car<Body, Chassis, Engine>) -> () {
self.cars.push(Box::new(car));
self.current_capacity += 1;
}
fn enter_wagon(&mut self, car: Car<WagonBody, WagonChassis, WagonEngine>) -> () {
self.cars.push(Box::new(car));
self.current_capacity += 2;
}
}
呼び出し部分はこのようになります。
let mut parking = Parking {
cars: Vec::new(),
capacity_limit: 100,
current_capacity: 0,
};
parking.enter_simple(car);
parking.enter_wagon(wagon_car);
println!("count: {}", parking.size());
println!("current_capacity: {}", parking.get_current_capacity());
enter_simple
と enter_wagon
でそれぞれの型に応じて処理を分けています。異なる型が引数として渡されようとするとコンパイルエラーになります。
しかし、Any型を使うのはオブジェクト指向としてはあまりおすすめできる方法ではありません。Rustでは、Any型を使う代わりにVec<Box<CarTrait>>
とすることでtrait objectを利用することが出来るため、trait objectによってAnyを回避することが出来ます。こちらは、@rchaser53 さんの12/9の記事を参考にさせて頂きました。注意点としては、 Box<Trait>
を使うと、#[derive(Debug)]
が使用できなくなります。
#[derive(Debug)]
struct Body;
#[derive(Debug)]
struct Chassis;
#[derive(Debug)]
struct Engine;
#[derive(Debug)]
struct WagonBody;
#[derive(Debug)]
struct WagonChassis;
#[derive(Debug)]
struct WagonEngine;
#[derive(Debug)]
struct Car<BodyT, ChassisT, EngineT> {
body: BodyT,
chassis: ChassisT,
engine: EngineT,
}
struct Parking {
cars: Vec<Box<CarTrait>>,
capacity_limit: u64,
current_capacity: u64,
}
trait ParkingTrait {
fn size(&self) -> usize;
fn get_current_capacity(&self) -> u64;
fn enter_simple(&mut self, car: Car<Body, Chassis, Engine>) -> ();
fn enter_wagon(&mut self, car: Car<WagonBody, WagonChassis, WagonEngine>) -> ();
}
impl ParkingTrait for Parking {
fn get_current_capacity(&self) -> u64 {
self.current_capacity
}
fn size(&self) -> usize {
self.cars.len()
}
fn enter_simple(&mut self, car: Car<Body, Chassis, Engine>) -> () {
self.cars.push(Box::new(car));
self.current_capacity += 1;
}
fn enter_wagon(&mut self, car: Car<WagonBody, WagonChassis, WagonEngine>) -> () {
self.cars.push(Box::new(car));
self.current_capacity += 2;
}
}
trait CarTrait {
fn get_info(&self) -> &str;
}
trait WagonCarTrait {
fn get_info(&self) -> &str;
fn fly(&self) -> &str;
}
impl CarTrait for Car<Body, Chassis, Engine> {
fn get_info(&self) -> &str {
"simple car!"
}
}
impl CarTrait for Car<WagonBody, WagonChassis, WagonEngine> {
fn get_info(&self) -> &str {
"wagon car!"
}
}
fn main() {
let car: Car<Body, Chassis, Engine> = Car {
body: Body {},
chassis: Chassis {},
engine: Engine {},
};
println!("{}", car.get_info());
let wagon_car: Car<WagonBody, WagonChassis, WagonEngine> = Car {
body: WagonBody {},
chassis: WagonChassis {},
engine: WagonEngine {},
};
println!("{}", wagon_car.get_info());
let broken_car: Car<Body, WagonChassis, WagonEngine> = Car {
body: Body {},
chassis: WagonChassis {},
engine: WagonEngine {},
};
// println!("{}", broken_car.get_info());
//=> no method named `get_info`
let mut parking = Parking {
cars: Vec::new(),
capacity_limit: 100,
current_capacity: 0,
};
parking.enter_simple(car);
parking.enter_wagon(wagon_car);
println!("count: {}", parking.size());
println!("current_capacity: {}", parking.get_current_capacity());
}
最後に
Rustのstruct, trait, implがどのような働きをしているのかをそれぞれオブジェクト指向の考え方に当てはめて説明して来ましたが、
一つだけ注意点があります。それはtraitの役割はstructの実装範囲に対して境界を決めるだけのものではなく、むしろ異なるstructだとしても実装されているtraitが同じ機能を有していることを保証するということです。
例えば、trait Clone
を実装していればあなたが作成したオリジナルのstructの仕様を第三者(外部のライブラリなどのことを指します)が知らなくても、Cloneが要求する
pub trait Clone {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self) { ... }
}
2つのメソッドを実装してさえいれば、オリジナルのstructだとしても動作することが保証されます。(また Cloneは、#[derive(Clone)]
すると便利です)
これを応用すると、Iterator を実装した場合は
struct FruitList {
count: usize,
}
impl std::iter::Iterator for FruitList {
type Item = usize;
fn next(&mut self) -> Option<usize> {
self.count += 1;
if self.count < 6 {
Some(self.count)
} else {
None
}
}
}
fn main() {
let mut fruit_list = FruitList { count: 0 };
for i in fruit_list {
println!("{}", i);
}
let mut fruit_list = FruitList { count: 0 };
let last = fruit_list.last();
println!("{:?}", last);
}
for in
文で使うことが出来るようになります。 for in
文にとっては FruitList というstructは知らない存在ですが、traitであるIteratorは知っているので使うことが出来ます。最後に、定義していないはずの .last()
が使えるのはIteratorのtraitによるデフォルト実装になっているためのようです。これで本当に最後になります。