Как повторно использовать код для похожих, но различных типов в Rust? - PullRequest
0 голосов
/ 07 июня 2019

У меня есть базовый тип с некоторыми функциональными возможностями, включая реализации признаков:

use std::fmt;
use std::str::FromStr;

pub struct MyIdentifier {
    value: String,
}

impl fmt::Display for MyIdentifier {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.value)
    }
}

impl FromStr for MyIdentifier {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(MyIdentifier {
            value: s.to_string(),
        })
    }
}

Это упрощенный пример, реальный код будет более сложным.

Я хочу представить два типакоторые имеют те же поля и поведение, что и описанный мною базовый тип, например MyUserIdentifier и MyGroupIdentifier.Чтобы избежать ошибок при их использовании, компилятор должен обрабатывать их как отдельные типы.

Я не хочу копировать весь код, который я только что написал, я хочу использовать его повторно.Для объектно-ориентированных языков я бы использовал наследование.Как бы я сделал это для Rust?

Ответы [ 2 ]

2 голосов
/ 08 июня 2019

Используйте PhantomData, чтобы добавить параметр типа к вашему Identifier.Это позволяет вам «брендировать» заданный идентификатор:

use std::{fmt, marker::PhantomData, str::FromStr};

pub struct Identifier<K> {
    value: String,
    _kind: PhantomData<K>,
}

impl<K> fmt::Display for Identifier<K> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.value)
    }
}

impl<K> FromStr for Identifier<K> {
    type Err = ();

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Ok(Identifier {
            value: s.to_string(),
            _kind: PhantomData,
        })
    }
}

struct User;
struct Group;

fn main() {
    let u_id: Identifier<User> = "howdy".parse().unwrap();
    let g_id: Identifier<Group> = "howdy".parse().unwrap();

    // do_group_thing(&u_id); // Fails
    do_group_thing(&g_id);
}

fn do_group_thing(id: &Identifier<Group>) {}
error[E0308]: mismatched types
  --> src/main.rs:32:20
   |
32 |     do_group_thing(&u_id);
   |                    ^^^^^ expected struct `Group`, found struct `User`
   |
   = note: expected type `&Identifier<Group>`
              found type `&Identifier<User>`

Однако вышеизложенное не так, как я бы это сделал на самом деле.

Я хочу представить два типа, которые имеют одинаковые поля и поведение

Два типа не должны иметь одинаковое поведение - это должны быть одинаковые типы.

Iя не хочу копировать весь код, который я только что написал, я хочу использовать его вместо

Затем просто повторно использовать .Мы постоянно используем такие типы, как String и Vec, составляя их как часть более крупных типов.Эти типы не действуют как String s или Vec s, они просто используют их.

Возможно, идентификатор является примитивным типом в вашем домене, и он должен существовать.Создавайте типы, такие как User или Group и передавайте (ссылки на) пользователей или группы.Вы, конечно, можете добавить безопасность типов, но это требует затрат программиста.

2 голосов
/ 08 июня 2019

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

(Объяснение будет встроенным, но если вы хотите увидеть код целиком и одновременно протестировать его, перейдите на площадку .)

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

use std::fmt;

trait Identifier {
    fn value(&self) -> &str;
}

Затем мы создаем новый тип, который содержит единственное значение, которое является универсальным типом, который ограничен для реализации нашей черты Identifier. Самое замечательное в этом шаблоне заключается в том, что он в конце концов будет оптимизирован компилятором.

struct Id<T: Identifier>(T);

Теперь, когда у нас есть конкретный тип, мы реализуем для него черту Display. Поскольку внутренний объект Id является Identifier, мы можем вызвать для него метод value, поэтому нам нужно реализовать эту черту только один раз.

impl<T> fmt::Display for Id<T>
where
    T: Identifier,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0.value())
    }
}

Ниже приведены определения различных типов идентификаторов и их реализации Identifier:

struct MyIdentifier(String);

impl Identifier for MyIdentifier {
    fn value(&self) -> &str {
        self.0.as_str()
    }
}

struct MyUserIdentifier {
    value: String,
    user: String,
}

impl Identifier for MyUserIdentifier {
    fn value(&self) -> &str {
        self.value.as_str()
    }
}

И последнее, но не менее важное: вот как бы вы их использовали:

fn main() {
    let mid = Id(MyIdentifier("Hello".to_string()));
    let uid = Id(MyUserIdentifier {
        value: "World".to_string(),
        user: "Cybran".to_string(),
    });

    println!("{}", mid);
    println!("{}", uid);
}

Display было легко, однако я не думаю, что вы могли бы объединить FromStr, как показывает мой пример выше, очень вероятно, что разные идентификаторы имеют разные поля, а не только value (чтобы быть справедливым) у некоторых даже нет value, в конце концов, черта Identifier требует, чтобы объект только реализовал метод, называемый value). И семантически FromStr должен создать новый экземпляр из строки. Поэтому я бы реализовал FromStr для всех типов отдельно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...