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

时间:2019-06-07 19:39:57

标签: types rust code-reuse

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

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做这件事?

2 个答案:

答案 0 :(得分:2)

有几种方法可以解决此类问题。下面的解决方案使用所谓的 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,如上面的示例所示,很可能不同的标识符具有不同的字段而不仅仅是{{1 }}(公平地说,有些甚至没有value,毕竟value特性只需要对象实现称为Identifier的方法)。并且在语义上value应该从字符串构造一个新实例。因此,我将为所有类型分别实现FromStr

答案 1 :(得分:2)

使用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的类型,并传递(引用)用户或组。您当然可以添加类型安全性,但这确实要付出一些程序员的代价。