Первое правило хорошего тона в программировании (или одно из первых) гласит: "Не копируй код". Используй функции и наследование.

Если с функциями все понятно, то с наследованием посложнее. Вы, наверное, знаете, что в Rust нет прямого наследования, но есть способы добиться чего-то на него похожего. О них я и расскажу.

impl dyn Trait

Если вы посмотрите на реализацию итератора, то увидите, что у него есть один required метод (то есть тот, который нужно реализовать нам) и provided методы (те, которые реализованы за нас). Что нам это даёт? А то, что если мы напишем свою реализацию какой-либо коллекции, то чтобы реализовать для нее итератор нам нужно всего-навсего имплементировать next(). Все остальные фп-шные штуки вроде map(), filter(), fold() и т.д. будут реализованы автоматически.

Представим, что вы пишете иерархию классов для игры в жанре RPG. Так вот, вместо того, чтобы в каждом новом классе писать один и тот же код, например, получения урона, можно сделать единый интерфейс, который подставит нужный код там, где вы этого попросите:

enum Damage{
    Physical(f32),
    Magic(f32)
}

trait Character{
  fn get_magic_resistance(&self) -> f32;
  fn get_physical_resistance(&self) -> f32;
  fn set_hp(&mut self, new_value: f32);
  fn get_hp(&self) -> f32;
  fn get_type(&self) -> CharacterType;
  fn get_dmg(&self) -> Damage;
}

impl dyn Character {
  fn make_hurt(&mut self, dmg: Damage) {
    match dmg{
      Damage::Physical(dmg) => self.set_hp(self.get_hp() - dmg / self.get_physical_resistance().exp()),
      Damage::Magic(dmg) => self.set_hp(self.get_hp() - dmg / self.get_magic_resistance().exp())
    }
  }
}

#[derive(Debug, Clone, Copy)]
enum CharacterType {
    Mage,
    Warrior,
    Rogue
}

impl Default for CharacterType {
    fn default() -> Self{
        CharacterType::Warrior
    }
}

#[derive(Default)]
struct Player{
    ty: CharacterType,
    phys_resist: f32,
    mag_resist: f32,
    hp: f32,
    dmg: f32
}

impl Player {
    pub fn new(ty: CharacterType, hp: f32, dmg: f32) -> Self {
        Self{ ty, hp, dmg, .. Default::default() }
    }
}

impl Character for Player{
    #[inline]
    fn get_magic_resistance(&self) -> f32 {
        self.mag_resist
    }
    
    #[inline]
    fn get_physical_resistance(&self) -> f32{
        self.phys_resist
    }
    
    #[inline]
    fn set_hp(&mut self, new_value: f32){
        self.hp = new_value;
    }
    
    #[inline]
    fn get_hp(&self) -> f32 {
        self.hp
    }
    
    #[inline]
    fn get_type(&self) -> CharacterType {
        self.ty
    }
    
    fn get_dmg(&self) -> Damage{
        match self.ty {
            CharacterType::Mage => Damage::Magic(self.dmg),
            _ => Damage::Physical(self.dmg)
        }
    }
}

struct EnemyWarrior{
    ty: CharacterType,
    phys_resist: f32,
    mag_resist: f32,
    hp: f32,
    dmg: f32
}

impl Default for EnemyWarrior {
    fn default() -> Self {
        Self{
            ty: CharacterType::Warrior,
            phys_resist: 0.,
            mag_resist: 0.,
            hp: 10.,
            dmg: 1.
        }
    }
}

impl Character for EnemyWarrior{
    #[inline]
    fn get_magic_resistance(&self) -> f32 {
        self.mag_resist
    }
    
    #[inline]
    fn get_physical_resistance(&self) -> f32{
        self.phys_resist
    }
    
    #[inline]
    fn set_hp(&mut self, new_value: f32){
        self.hp = new_value;
    }
    
    #[inline]
    fn get_hp(&self) -> f32 {
        self.hp
    }
    
    fn get_type(&self) -> CharacterType {
        CharacterType::Warrior
    }
    
    fn get_dmg(&self) -> Damage{
        match self.ty {
            CharacterType::Mage => Damage::Magic(self.dmg),
            _ => Damage::Physical(self.dmg)
        }
    }
}

fn main(){
    let mut player = Player::new(CharacterType::Warrior, 10., 1.);
    let mut enemy = EnemyWarrior::default();
    
    <dyn Character>::make_hurt(&mut enemy, player.get_dmg());
    println!("{}", enemy.get_hp());
}

TL;DR сделали 2 структуры и реализовали для них трейт Character, в котором 6 required методов и 1 provided метод.

Но внимательный читатель совершенно справедливо спросит: "Так мы ведь всё равно скопировали код?" Верно, из-за того, что в трейтах нельзя объявлять поля, нам пришлось городить required методы для получения нужных данных. Сейчас-то, конечно, их всего 6, но потом у нас могут добавиться передвижения, анимации, левел-апы и проч. Конечно, целый один метод у нас единый на все имплементации, но код-то мы все равно копируем?

Deref<Target=_>

Тут нам на помощь приходит трейт Deref. Честно говоря, не знаю, насколько это хорошая практика использовать его с такой целью, но, например, в image есть такое: ImageBuffer реализует Deref для [P::Subpixel] и, соответственно, имеет все методы массива, которые есть в стандартной библиотеке. Давайте перепишем наш пример под Deref и посмотрим, сколько места нам удалось сэкономить.

use std::ops::Deref;
use std::ops::DerefMut;

enum Damage{
    Physical(f32),
    Magic(f32)
}

#[derive(Default)]
struct Character{
    ty: CharacterType,
    phys_resist: f32,
    mag_resist: f32,
    hp: f32,
    dmg: f32
}

impl Character {
    #[inline]
    fn get_magic_resistance(&self) -> f32 {
        self.mag_resist
    }
    
    #[inline]
    fn get_physical_resistance(&self) -> f32{
        self.phys_resist
    }
    
    #[inline]
    fn set_hp(&mut self, new_value: f32){
        self.hp = new_value;
    }
    
    #[inline]
    fn get_hp(&self) -> f32 {
        self.hp
    }
    
    #[inline]
    fn get_type(&self) -> CharacterType {
        self.ty
    }
    
    fn get_dmg(&self) -> Damage{
        match self.ty {
            CharacterType::Mage => Damage::Magic(self.dmg),
            _ => Damage::Physical(self.dmg)
        }
    }

  fn make_hurt(&mut self, dmg: Damage) {
    match dmg{
      Damage::Physical(dmg) => self.set_hp(self.get_hp() - dmg / self.get_physical_resistance().exp()),
      Damage::Magic(dmg) => self.set_hp(self.get_hp() - dmg / self.get_magic_resistance().exp())
    }
  }
}


#[derive(Debug, Clone, Copy)]
enum CharacterType {
    Mage,
    Warrior,
    Rogue
}

impl Default for CharacterType {
    fn default() -> Self{
        CharacterType::Warrior
    }
}

#[derive(Default)]
struct Player(Character);

impl Player {
    pub fn new(ty: CharacterType, hp: f32, dmg: f32) -> Self {
        Self(Character{ ty, hp, dmg, .. Default::default() })
    }
}

impl Deref for Player {
    type Target = Character;
    
    #[inline]
    fn deref(&self) -> &Self::Target{
        &self.0
    }
}

impl DerefMut for Player {
    #[inline]
    fn deref_mut(&mut self) -> &mut Self::Target{
        &mut self.0
    }
}


struct EnemyWarrior(Character);

impl Default for EnemyWarrior {
    fn default() -> Self {
        Self(Character {
            ty: CharacterType::Warrior,
            phys_resist: 0.,
            mag_resist: 0.,
            hp: 10.,
            dmg: 1.
        })
    }
}

impl Deref for EnemyWarrior {
    type Target = Character;
    
    #[inline]
    fn deref(&self) -> &Self::Target{
        &self.0
    }
}

impl DerefMut for EnemyWarrior {
    #[inline]
    fn deref_mut(&mut self) -> &mut Self::Target{
        &mut self.0
    }
}

fn main(){
    let mut player = Player::new(CharacterType::Warrior, 10., 1.);
    let mut enemy = EnemyWarrior::default();
    
    enemy.make_hurt(player.get_dmg());
    println!("{}", enemy.get_hp());
}

Неплохо, несколько строчек мы сэкономили. Ну и, конечно, надо понимать, что расширять такую иерархию проще, чем интерфейс, ведь в том случае при добавлении методов, нужно затаскивать их и в имплементации, а в данном случае достаточно лишь реализовать метод в "родителе", и мы уже получим его во всех "наследниках".

Но у такого подхода тоже есть минус. Е��ли нам нужны другие данные, помимо "родительских", тогда нам придётся создавать дополнительные структуры, чтобы хранить уже свои, независимые от "родительских", данные. Тогда нам придется писать код вроде:

struct Child (Parent, ChildInner);

impl Child {
  pub fn get_some_field_from_inner(&self) -> &ChildInner::Field {
    &self.1.field
  }
}

И, согласитесь, сигнатура этого метода не очень удобочитаемая. А если у нас будет несколько внутренних структур, то остаётся только пожелать здоровья людям, которые будут это ревьюить и/или поддерживать.

Заключение

В заключение хочу сказать, что в расте не помешало бы явное наследование структур. Всё-таки, как ни крути, в других языках оно позволяет писать с одной стороны хорошо читаемый, с другой стороны лаконичный код (если, конечно, это не множественное наследование). Подходы в Rust, конечно, позволяют экономить какое-то количество места, но хотелось бы чего-то более явного. Да и интерфейсы Deref, DerefMut вообще не предназначены для того, для чего мы их использовали в данной статье. Они, как следует из названия, нужны чтобы разыменовывать умные указатели. А если вы объединяете несколько структур данных, то вам придется использовать синтаксис self.1, который далеко не очевидный. В общем, как и всегда, есть и плюсы и минусы.

Вот, собственно, и все, что я хотел сказать на эту тему. Может, я что-то упустил? Напишите в комментариях, если вас тоже волнует эта тема, хочется знать, что не мне одному не хватает наследования в Rust.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какой вариант лучше?
74.47%impl dyn Trait35
10.64%Deref<Traget=_>5
14.89%Копировать код7
Проголосовали 47 пользователей. Воздержались 50 пользователей.