Kirill
Kirill

Reputation: 8311

Static factory method for trait

I'm just learning Rust, so maybe I just didn't get some concepts correctly.

I have a trait with some implementations:

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" }
}

I want to add a static method to this trait to create some standard implementation by name:


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

But this code doesn't compile because:

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

I tried to use return fn new(name: &str) -> impl Abstract and fn new<T: Abstract>(name: &str) -> T - but these variants doesn't work too with another errors.

What is the correct way exist of creating factory method for trait in Rust?

Upvotes: 4

Views: 2061

Answers (3)

Netwave
Netwave

Reputation: 42716

You can consider using an enum instead of dynamically dispatch it:

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

Upvotes: 3

cameron1024
cameron1024

Reputation: 10156

In Rust, every variable must be a single specific type. This is different to OO languages where a variable can have a type Foo, and that means that the variable contains a Foo, or any subclass of Foo.

Rust doesn't support this. If a variable has a type Foo, it must contain a Foo (ignoring any unsafe concerns).

Rust also doesn't support using traits as types (without the dyn keyword).

In your example, you have:

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

The return type Self here means "whatever type this trait is being implemented on". However, by providing a body in the trait definition, you're providing a default implementation, so this function should in theory apply to any type, and so the compiler has no information about the real concrete type Self, and therefore the Sized bound it not met (which in practice is very restrictive and probably not what you want).

If you want a function that takes a string and returns "some type T that implements Abstract", you could use a "trait object", which would look roughly like:

// 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 {}),
    // ...
  }
}

However, I'd warn against this pattern. Dynamic dispatch has some runtime overhead, and prevents many compile-time optimizations. Instead, there may be a more "rusty" way of doing it, but it's hard to tell without more context.

In general, constructing a type based on the value of a string smells like a bit of an anti-pattern.

Upvotes: 9

Caesar
Caesar

Reputation: 8514

  • fn new<T: Abstract>(…) -> T means: new will return some concrete T that implements Abstract, and whatever calls new may decide which.
  • fn new() -> impl Abstract means: new will return some concrete type of its choosing that implements Abstract, but whatever calls new won't know which concrete type. (But it must still be some single concrete type decided at compile-time, it can't be "Maybe Foo or maybe Baz")

If you absolutely want this, you can have a free function do your constructing:

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")
    }
}

The new function can't be part of Abstract, because you would get into trouble with object safety. Essentially, if you want to return a Box<dyn …> of some trait, all the methods on the trait must be usable in dynamic dispatch, but new is not such a method.

Anyway, this is not a good idea. Adding a separate new function to Foo, Bar, and Baz is the Rust way to go.

Upvotes: 3

Related Questions