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

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

我有一个具有某些功能的基本类型,包括特征实现:

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 做这个?

有几种方法可以处理这种问题。以下解决方案使用 so-called newtype 模式,新类型包含的对象的统一特征和新类型的特征实现。

(解释将是内联的,但如果您想查看整个代码并同时对其进行测试,请转到 playground。)

首先,我们创建一个特征来描述我们希望从标识符中看到的最小行为。在 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

使用 PhantomData 将类型参数添加到您的 Identifier。这允许您 "brand" 给定的标识符:

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 want to introduce two types which have the same fields and behaviour

两种类型不应该有相同的行为——那些应该是相同的类型。

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

然后重用它。我们一直通过将 StringVec 之类的类型组合为我们较大类型的一部分来重用它们。这些类型不像 Strings 或 Vecs,它们只是使用它们。

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