Create flexible constructor method accepting either String or &str

Viewed 43

I am completely new to rust. I created a program to store information about persons to get in touch with the language:

person.rs

pub struct Person {
    firstname: String,
    lastname: String,
    pub age: u8
}


impl Person {
    pub fn new(firstname: String, lastname: String, age: u8) -> Person {
        return Person { firstname: firstname, lastname: lastname, age: age };
    }

    pub fn from_str(firstname: &str, lastname: &str, age: u8) -> Person {
        return Person::new(firstname.to_string(), lastname.to_string(), age);
    }

    pub fn name(&self) -> String {
        return self.firstname.to_owned() + " " + &self.lastname;
    }
}

main.rs

mod person;
use person::Person;


fn main() {
    let me = Person::from_str("John", "Doe", 42);
    println!("{} is {} years old.", me.name(), me.age);
}

I understand, that there is no function overloading in rust. However, I have read about a concept called "traits", which I have not fully understood. Do they behave similar to templates in C++ (I'm also not a C++ pro)? Is there another way, e.g. using traits, so that I can write Person::new() to accept either String or &str?

I tried this to no avail:

person.rs

pub struct Person {
    firstname: String,
    lastname: String,
    pub age: u8
}


impl Person {
    /*
    pub fn new(firstname: String, lastname: String, age: u8) -> Person {
        return Person { firstname: firstname, lastname: lastname, age: age };
    }

    pub fn from_str(firstname: &str, lastname: &str, age: u8) -> Person {
        return Person::new(firstname.to_string(), lastname.to_string(), age);
    }
    */
    
    pub fn new<T>(firstname: &T, lastname: &T, age: u8) -> Person {
        return Person::new(firstname.to_string(), lastname.to_string(), age);
    }

    pub fn name(&self) -> String {
        return self.firstname.to_owned() + " " + &self.lastname;
    }
}

main.rs

mod person;
use person::Person;


fn main() {
    let me = Person::new("John", "Doe", 42);
    println!("{} is {} years old.", me.name(), me.age);
}

Resulting in:

$ rustc main.rs -o main
error[E0599]: the method `to_string` exists for reference `&T`, but its trait bounds were not satisfied
  --> person.rs:20:38
   |
20 |         return Person::new(firstname.to_string(), lastname.to_string(), age);
   |                                      ^^^^^^^^^ method cannot be called on `&T` due to unsatisfied trait bounds
   |
   = note: the following trait bounds were not satisfied:
           `T: std::fmt::Display`
           which is required by `T: ToString`
           `&T: std::fmt::Display`
           which is required by `&T: ToString`

error[E0599]: the method `to_string` exists for reference `&T`, but its trait bounds were not satisfied
  --> person.rs:20:60
   |
20 |         return Person::new(firstname.to_string(), lastname.to_string(), age);
   |                                                            ^^^^^^^^^ method cannot be called on `&T` due to unsatisfied trait bounds
   |
   = note: the following trait bounds were not satisfied:
           `T: std::fmt::Display`
           which is required by `T: ToString`
           `&T: std::fmt::Display`
           which is required by `&T: ToString`

error[E0277]: the size for values of type `str` cannot be known at compilation time
  --> main.rs:6:14
   |
6  |     let me = Person::new("John", "Doe", 42);
   |              ^^^^^^^^^^^ doesn't have a size known at compile-time
   |
   = help: the trait `Sized` is not implemented for `str`
note: required by a bound in `Person::new`
  --> person.rs:19:16
   |
19 |     pub fn new<T>(firstname: &T, lastname: &T, age: u8) -> Person {
   |                ^ required by this bound in `Person::new`
help: consider relaxing the implicit `Sized` restriction
  --> person.rs:19:17
   |
19 |     pub fn new<T: ?Sized>(firstname: &T, lastname: &T, age: u8) -> Person {
   |                 ++++++++

error: aborting due to 3 previous errors

Some errors have detailed explanations: E0277, E0599.
For more information about an error, try `rustc --explain E0277`.
2 Answers

Rust has generics, not templates. Generics can be bounded by traits, which provide a system for standard interfaces.

In your case, what you need is a trait that will give you a String from either a String or a &str. You have a few options, but I'd choose Into<String>.

You can do this:

    pub fn new(firstname: impl Into<String>, lastname: impl Into<String>, age: u8) -> Person {
        Person {
            firstname: firstname.into(),
            lastname: lastname.into(),
            age
        }
    }

Essentially what this says is:

  • new is a function
  • which takes three arguments
  • the first two args can be any type that implements Into<String>
  • the last argument must be a u8

Into<T> is a standard trait in Rust meant for the use in conversions. It defines an associated fn into(self) -> T that takes the given object by value and returns the target T.

Into is really just the complementary helper trait to From<T> which has an associated fn from(other: T) -> Self that takes some other type and returns the type you want.

From has a reflexive implementation, which essentially means that for any type, you can do T::from(x) where x is of type T. This also applies to Into, so any type can be converted into itself, which is why String will work when the compiler expects Into<String>.

You can accept impl Into<String>:

pub fn new(firstname: impl Into<String>, lastname: impl Into<String>, age: u8) -> Person {
    Person {
        firstname: firstname.into(),
        lastname: lastname.into(),
        age: age,
    }
}

impl Trait in parameter position is a short way to declare that a parameter is generic. You could alternatively write out the function signature with explicitly named generic types:

pub fn new<F, L>(firstname: F, lastname: L, age: u8) -> Person
where
    F: Into<String>,
    L: Into<String>,

To explain the use of Into in excessive detail:

  1. Into is the inverse of From thanks to this blanket implementation:

    impl<T, U> const Into<U> for T
    where
        U: ~const From<T>,
    {
        fn into(self) -> U {
            U::from(self)
        }
    }
    

    Whenever impl From<T> for U exists, so does the matching impl Into<U> for T.

  2. The standard library provides a variety of such implementations for String:

    impl From<char> for String
    impl From<Box<str>> for String
    impl From<&str> for String
    impl From<&String> for String
    impl From<&mut str> for String
    impl From<Cow<'_, str>> for String
    

    Thanks to impl From<&str> for String you can build a String from a &str.

  3. Additionally, there is a blanket implementation for all T:

    impl<T> From<T> for T {
        fn from(t: T) -> T {
            t
        }
    }
    

    Any type can be trivially converted into itself. That means impl From<String> for String is also available as a zero-cost conversion.

Related