yhara.jp

Recent Posts

Rustで継承したいときはどうする?

2022-04-03
Tech

たとえばゲームプログラミングでEnemyとしてスライムとドラゴンがいて…みたいなとき、Rubyなら継承で実装しますよね。

class Enemy
  attr_accessor :hp # 体力
end

class Slime < Enemy
end

class Dragon < Enemy
  attr_accessor :breath_power # ブレス攻撃の威力
end

こういうの(即ち、一部のプロパティとメソッドが共通するような兄弟関係のモデリング)はRustではどうしたらいい?

Rustでどうするか

まず共通部分をstructとして定義

struct EnemyBase {
  hp: usize,
  //...他にも位置、速度などなど
}

次に敵ごとのstructを定義し、EnemyBaseを持たせる

struct Slime {
  base: EnemyBase,
  //...その他、スライム固有のデータはここに
}

struct Dragon {
  base: EnemyBase,
  breath_power: usize,
}

最後に敵たちをまとめて管理できるようenumを定義する

enum Enemy {
  Slime(Slime),
  Dragon(Dragon),
}

補足1

Rust知ってる人ならこれでいいんじゃね?と思うかもしれない

enum Enemy {
  Slime { base: EnemyBase, /*など*/ },
  Dragon { base: EnemyBase, breath_power: usize, /*など*/ },
}

が、これだと「Dragonを引数にとる関数」というのが書けないんですよね。↑のDragonはstructではなくvariantなので。

もちろん以下のようにはできますが、dragon以外を渡したときにコンパイルエラーではなくランタイムエラーになるのが嫌で。

fn tame_dragon(dragon: Enemy) {
  match dragon {
    Enemy::Dragon { 要素 } => { 処理 },
    _ => panic!("runtime error")
  }
}

補足2

この実装、Vec<Enemy>みたいに敵を統一管理できるのはいいんだけど、「全ての敵に100ダメージ」みたいなことしようと思うと途端に面倒になる。

  for enemy in enemies {
    enemy.hp -= 100; // enemyが直接hpをもつわけではないのでエラー
  }

Enemyにdamageメソッドみたいのを生やす?いやいや、敵の種類を増やすたびにこういうの全部定義するのは大変すぎる。

impl Enemy {
  fn damage(&mut self, v: usize) {
    match self {
      Enemy::Slime(x) => x.base.hp -= v, 
      Enemy::Dragon(x) => x.base.hp -= v, 
  }
}

せめてこうだよね。

impl Enemy {
  pub fn damage(&mut self, v: usize) {
    self.base_mut().hp -= v;
  }

  fn base_mut(&mut self) -> &mut EnemyBase {
    self.base
  }
}

補足3

Twitterで相談したところ、traitでまとめるという方法もあるよと教えてもらった。

trait Enemy {
    fn damage(&mut self, p: u32);
}

struct Dragon {
    hp: u32,
    //...
}

impl Enemy for Dragon {
    fn damage(&mut self, p: u32) {
        self.hp -= p
    }
}

struct Slime {
    hp: u32,
    //...
}

impl Enemy for Slime {
    fn damage(&mut self, p: u32) {
        self.hp -= p
    }
}

fn create_enemies1() -> Vec<Box<dyn Enemy>> {
    vec![
        Box::new(Dragon { hp: 100 }),
        Box::new(Slime { hp: 10 }),
    ]
}

ポイントは2つ。

  • 敵の種類ごとにstructのサイズが異なり得るのでVec<Enemy>はできないが、Boxを挟んでVec<Box<dyn Enemy>>とする(つまりEnemyへのポインタの配列、とする)ことでVecにまとめられる。
  • trait Enemyでは、もはや「hpを持っていること」という規定は無くなっている。何を持っているかではなくて、「どういう操作ができるか」だけを規定するのがtrait

補足4

上記だと同じようなdamage関数を全てのEnemyに定義して回るのが大変そうだが、先ほどのbaseと組み合わせればtrait Enemy側でデフォルトのdamage関数を提供することもできそう。

struct EnemyBase {
    hp: u32,
}

trait Enemy {
    fn base_mut(&mut self) -> &mut EnemyBase;

    fn damage(&mut self, p: u32) {
        self.base_mut().hp -= p;
    }
}

struct Dragon {
    base: EnemyBase
    //...
}

impl Enemy for Dragon {
    fn base_mut(&mut self) -> &mut EnemyBase {
        &mut self.base
    }
}

補足5

enumでまとめるのとtraitでまとめるのとどういう違いがあるかというと

  • enumの方は後から新しい種別を追加することができない代わりに、matchで場合分けでき、網羅性チェックも受けられる。個数が決まっていて、場合分けしたいときはenum
  • traitの方は場合分けできない代わりに、後から新しい種別を追加することができる。ライブラリでtraitを提供して、ユーザが好きな種別を追加できるようにしたいときはtrait

More posts

Posts

(more...)

Articles

(more...)

Category

Ads

About

About the author