特征的静态工厂方法

Static factory method for trait

我刚学 Rust,所以可能我只是没有正确理解一些概念。

我有一些实现的特点:

trait Abstract {
    fn name(&self) -> &str;
}

struct Foo {}
struct Bar {}
struct Baz {}

impl Abstract for Foo {
    fn name(&self) -> &str { "foo" }
}
impl Abstract for Bar {
    fn name(&self) -> &str { "bar" }
}
impl Abstract for Baz {
    fn name(&self) -> &str { "baz" }
}

我想向这个特征添加一个静态方法来创建一些标准的名称实现:


trait Abstract {
    fn new(name: &str) -> Self {
        match name {
            "foo" => Foo{},
            "bar" => Bar{},
            "baz" => Baz{},
        }
    }
    fn name(&self) -> &str;
}

但此代码无法编译,因为:

6 |     fn new(name: &str) -> Self {
  |                           ^^^^ doesn't have a size known at compile-time

我尝试使用 return fn new(name: &str) -> impl Abstractfn new<T: Abstract>(name: &str) -> T - 但这些变体不适用于其他错误。

在 Rust 中为特征创建工厂方法的正确方法是什么?

  • fn new<T: Abstract>(…) -> T 意味着:new 将 return 一些实现 Abstract 的具体 T,并且无论调用 new 可以决定哪个。
  • fn new() -> impl Abstract 表示:new 将 return 选择实现抽象的具体类型,但无论调用什么 new 都不知道具体类型。 (但它仍然必须是在编译时决定的某个单一具体类型,它不能是“可能 Foo 或可能 Baz”)

如果你真的想要这个,你可以用一个免费的函数来做你的构建:

trait Abstract {
    fn name(&self) -> &str;
}

fn new_abstract(name: &str) -> Box<dyn Abstract> {
    match name {
        "foo" => Box::new(Foo{}),
        "bar" => Box::new(Bar{}),
        "baz" => Box::new(Baz{}),
        _ => panic!("better not")
    }
}

new 函数不能成为 Abstract 的一部分,因为 object safety 会给您带来麻烦。本质上,如果你想 return 某个特征的 Box<dyn …> ,特征上的所有方法都必须在动态调度中可用,但 new 不是这样的方法。

总之,这不是个好主意。将单独的 new 函数添加到 FooBarBaz 是 Rust 的方法。

在 Rust 中,每个变量都必须是单一的特定类型。这与 OO 语言不同,其中变量可以具有类型 Foo,这意味着变量包含 FooFoo.

的任何子类

Rust 不支持这个。如果变量的类型为 Foo,它 必须 包含 Foo(忽略任何不安全的问题)。

Rust 也不支持使用特征作为类型(没有 dyn 关键字)。

在您的示例中,您有:

trait Abstract {
  fn new(name: &str) -> Self {
    // ...
  }
}

这里的 return 类型 Self 的意思是“无论这个特性是在什么类型上实现的”。然而,通过在 trait 定义中提供一个主体,你提供了一个默认实现,所以这个函数理论上应该适用于任何类型,因此编译器没有关于真正具体类型的信息 Self,因此Sized 没有满足它的限制(这在实践中是非常严格的,可能不是你想要的)。

如果您想要一个接受字符串和 return 的“实现 Abstract 的某种类型 T”的函数,您可以使用“特征对象”,它看起来大致像:

// outside trait definition
fn new_abstract(name: &str) -> Box<dyn Abstract> {  // dyn keyword opts into dynamic dispatch with vtables
  match name {
    "foo" => Box::new(Foo {}),
    // ...
  }
}

但是,我要警告不要使用这种模式。动态分派有一些运行时开销,并阻止了许多编译时优化。相反,可能有一种更“生锈”的方式来做,但如果没有更多的上下文就很难说。

一般来说,根据字符串的值构造类型有点反模式。

您可以考虑使用 enum 而不是动态调度它:

trait Abstract {
    fn name(&self) ->  &str;
    fn new(name: &str) -> Option<AbstractVariant> {
        match name {
            "foo" => Some(AbstractVariant::Foo(Foo {})),
            "bar" => Some(AbstractVariant::Bar(Bar {})),
            "baz" => Some(AbstractVariant::Baz(Baz {})),
            _ => None,
        }
    }
}

enum AbstractVariant {
    Foo(Foo),
    Bar(Bar),
    Baz(Baz),
}

Playground