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

Если с функциями все понятно, то с наследованием посложнее. Вы, наверное, знаете, что в 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.
