Как стать автором
Обновить

Как не копировать код в Rust

Время на прочтение5 мин
Количество просмотров8.5K

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

Если с функциями все понятно, то с наследованием посложнее. Вы, наверное, знаете, что в 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 пользователей. Воздержались 49 пользователей.
Теги:
Хабы:
Всего голосов 9: ↑5 и ↓4+3
Комментарии46

Публикации

Истории

Работа

Rust разработчик
9 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань