How do I reuse code for similar yet distinct types in Rust?

喜欢而已 提交于 2020-01-16 20:19:13

问题


I have a basic type with some functionality, including trait implementations:

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(),
        })
    }
}

This is a simplified example, real code would be more complex.

I want to introduce two types which have the same fields and behaviour as the basic type I described, for instance MyUserIdentifier and MyGroupIdentifier. To avoid making mistakes when using these, the compiler should treat them as distinct types.

I don't want to copy the entire code I just wrote, I want to reuse it instead. For object-oriented languages I would use inheritance. How would I do this for Rust?


回答1:


Use a PhantomData to add a type parameter to your Identifier. This allows you to "brand" a given 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>`

The above isn't how I'd actually do it myself, though.

I want to introduce two types which have the same fields and behaviour

Two types shouldn't have the same behavior — those should be the same type.

I don't want to copy the entire code I just wrote, I want to reuse it instead

Then just reuse it. We reuse types like String and Vec all the time by composing them as part of our larger types. These types don't act like Strings or Vecs, they just use them.

Maybe an identifier is a primitive type in your domain, and it should exist. Create types like User or Group and pass around (references to) users or groups. You certainly can add type safety, but it does come at some programmer expense.




回答2:


There are several ways to deal with this kind of problem. The following solution is using the so-called newtype pattern, a unified trait for the object the newtype contains and a trait implementation for the newtype.

(Explanation is going to be inline, but if you'd like to see the code as a whole and at the same time test it then go to the playground.)

First, we create a trait that describes the minimal behaviour we'd like to see from an identifier. In Rust you don't have inheritance, you have composition, i.e. an object can implement any number of traits which will describe its behaviour. If you'd like to have something that is common in all your objects — which you would achieve via inheritance — then you have to implement the same trait for them.

use std::fmt;

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

Then we create a newtype which contains a single value which is a generic type that is constrained to implement our Identifier trait. The great thing about this pattern is that it will actually be optimised by the compiler at the end.

struct Id<T: Identifier>(T);

Now that we have a concrete type, we implement the Display trait for it. Since Id's internal object is an Identifier, we can call the value method on it so we only have to implement this trait once.

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

The followings are definitions of different identifier types and their Identifier trait implementations:

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()
    }
}

And last but not least, this is how you would use them:

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);
}

The Display was easy, however I don't think you could unify the FromStr, as my example above demonstrates it is very likely that the different identifiers have different fields not just the value (to be fair, some don't even have the value, after all, the Identifier trait only requires the object to implement a method called value). And semantically the FromStr supposed to construct a new instance from a string. Therefore I would implement FromStr for all types separately.



来源:https://stackoverflow.com/questions/56500357/how-do-i-reuse-code-for-similar-yet-distinct-types-in-rust

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!