如何在Rust中为类似但不同类型重复使用代码?

3

我有一个基本类型,包括一些功能和 trait 实现:

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

这只是一个简化的例子,真实的代码会更加复杂。

我想介绍两种类型,它们与我所描述的基本类型具有相同的字段和行为,例如 MyUserIdentifierMyGroupIdentifier。为了避免在使用它们时出现错误,编译器应将它们视为不同的类型。

我不想复制刚才编写的整段代码,而是想要重用它。对于面向对象的语言,我会使用继承。那么,在Rust中该如何做呢?


这个回答解决了你的问题吗?如何避免不同结构体中具有语义相等的字段/属性的代码重复?(https://dev59.com/3lkS5IYBdhLWcg3wzZXB) - Michael Freidgeim
2个回答

5
使用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>`

但那并不是我自己的实际操作方式。

我想介绍两种有相同字段和行为的类型

两种类型不应该具有相同的行为 - 它们应该是相同的类型。

我不想复制我刚写的整个代码,我想重用它

那就直接重用它。我们经常通过将其组合为更大的类型的一部分来重用像StringVec这样的类型。这些类型不像StringVec那样运行,它们仅使用它们。

也许在你的领域中,标识符是一种原始类型,因此应该存在。创建如UserGroup之类的类型,并传递(引用)用户或组。您当然可以添加类型安全性,但这确实需要一些程序员的代价。


3
有几种方法可以处理这种问题。以下解决方案使用所谓的“newtype”模式,即新类型包含的对象的统一特征和新类型的特征实现。
(说明将在行内进行,但如果您想同时查看整个代码并测试它,请转到playground。)
首先,我们创建一个描述标识符所需最小行为的特征。在Rust中,您没有继承,而是组合,即一个对象可以实现任意数量的特征来描述其行为。如果您想拥有所有对象中都共有的东西——这是通过继承实现的——那么您必须为它们实现相同的特征。
use std::fmt;

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

然后,我们创建一个新类型,其中包含一个单一值,该值是一个泛型类型,受限于实现我们的Identifier trait。这种模式的好处在于编译器最终会进行优化。
struct Id<T: Identifier>(T);

现在我们有了一个具体的类型,我们要为它实现Display trait。由于Id的内部对象是一个Identifier,因此我们可以调用它的value方法,这样我们只需一次实现这个trait。
impl<T> fmt::Display for Id<T>
where
    T: Identifier,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0.value())
    }
}

以下是不同标识符类型及其“标识符”特征实现的定义:
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);
}

显示器很容易,但我认为你不能统一 FromStr。就像我上面的示例所演示的那样,不同的标识符很可能有不同的字段,而不仅仅是 value(公平地说,有些甚至没有 value,在所有情况下,Identifier 特性只要求对象实现一个名为 value 的方法)。从语义上讲,FromStr 应该从字符串构造一个新的实例。因此,我会为所有类型单独实现 FromStr。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接