如何在 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(),
})
}
}
这是一个简化的示例,实际代码会更复杂。
我想介绍两种与我描述的基本类型具有相同字段和行为的类型,例如MyUserIdentifier
和MyGroupIdentifier
。为避免在使用这些时出错,编译器应将它们视为不同的类型。
我不想复制我刚写的整个代码,我想重用它。对于面向对象的语言,我会使用继承。我将如何为 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
然后重用它。我们一直通过将 String
和 Vec
之类的类型组合为我们较大类型的一部分来重用它们。这些类型不像 String
s 或 Vec
s,它们只是使用它们。
也许标识符是您域中的原始类型,它应该存在。创建像 User
或 Group
这样的类型并传递(引用)用户或组。您当然可以添加类型安全,但它确实需要程序员付出一些代价。
我有一个具有某些功能的基本类型,包括特征实现:
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 做这个?
有几种方法可以处理这种问题。以下解决方案使用 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
然后重用它。我们一直通过将 String
和 Vec
之类的类型组合为我们较大类型的一部分来重用它们。这些类型不像 String
s 或 Vec
s,它们只是使用它们。
也许标识符是您域中的原始类型,它应该存在。创建像 User
或 Group
这样的类型并传递(引用)用户或组。您当然可以添加类型安全,但它确实需要程序员付出一些代价。