Using the constructor pattern
You may have asked yourself how to idiomatically initialize complex structs in Rust, considering it doesn't have constructors. The answer is simple, there is a constructor, it's just a convention rather than a rule. Rust's standard library uses this pattern very often, so we need to understand it if we want to use the std effectively.
Getting ready
In this recipe, we are going to talk about how a user interacts with a struct
. When we say user in this context, we don't mean the end user that clicks on the GUI of the app you're writing. We're referring to the programmer that instantiates and manipulates the struct
.
How to do it...
In the
src/bin
folder, create a file calledconstructor.rs
Add the following code and run it with
cargo run --bin constructor
:
1 fn main() { 2 // We don't need to care about 3 // the internal structure of NameLength 4 // Instead, we can just call it's constructor 5 let name_length = NameLength::new("John"); 6 7 // Prints "The name 'John' is '4' characters long" 8 name_length.print(); 9 } 10 11 struct NameLength { 12 name: String, 13 length: usize, 14 } 15 16 impl NameLength { 17 // The user doesn't need to setup length 18 // We do it for him! 19 fn new(name: &str) -> Self { 20 NameLength { 21 length: name.len(), 22 name, 23 } 24 } 25 26 fn print(&self) { 27 println!( 28 "The name '{}' is '{}' characters long", 29 self.name, 30 self.length 31 ); 32 } 33 }
How it works...
If a struct
provides a method called new
that returns Self
, the user of the struct
will not configure or depend upon the members of the struct
, as they are considered to be in an internal hidden state.
In other words, if you see a struct
that has a new
function, always use it to create the structure.
This has the nice effect of enabling you to change as many members of the struct as you want without the user noticing anything, as they are not supposed to look at them anyway.
The other reason to use this pattern is to guide the user to the correct way of instantiating a struct
. If one has nothing but a big list of members that have to be filled with values, one might feel a bit lost. If one, however, has a method with only a few self-documenting parameters, it feels way more inviting.
There's more...
You might have noticed that for our example we really didn't need a length
member and could have just calculated a length whenever we print. We use this pattern anyway, to illustrate the point of its usefulness in hiding implementations. Another good use for it is when the members of a struct
themselves have their own constructors and one needs to cascade the constructor calls. This happens, for example, when we have a Vec
as a member, as we will see later in the book, in the, Using a vector section in Chapter 2, Working with Collections.
Sometimes, your structs might need more than one way to initialize themselves. When this happens, try to still provide a new()
method as your default way of construction and name the other options according to how they differ from the default. A good example of this is again vector, which not only provides a Vec::new()
constructor but also a Vec::with_capacity(10)
, which initializes it with enough space for 10
items. More on that again in the Using a vector section in Chapter 2, Working with Collections.
When accepting a kind of string (either &str
, that is, a borrowed string slice, or String
, that is, an owned string) with plans to store it in your struct
, like we do in our example, also considering a Cow
. No, not the big milk animal friends. A Cow
in Rust is a Clone On Write wrapper around a type, which means that it will try to borrow a type for as long as possible and only make an owned clone of the data when absolutely necessary, which happens at the first mutation. The practical effect of this is that, if we rewrote our NameLength
struct in the following way, it would not care whether the called passed a &str
or a String
to it, and would instead try to work in the most efficient way possible:
use std::borrow::Cow; struct NameLength<'a> { name: Cow<'a, str>, length: usize, } impl<'a> NameLength<'a> { // The user doesn't need to setup length // We do it for him! fn new<S>(name: S) -> Self where S: Into<Cow<'a, str>>, { let name: Cow<'a, str> = name.into(); NameLength { length: name.len(), name, } } fn print(&self) { println!( "The name '{}' is '{}' characters long", self.name, self.length ); } }
If you want to read more about Cow
, check out this easy-to-understand blog post by Joe Wilm: https://jwilm.io/blog/from-str-to-cow/.
The Into
trait used in the Cow
code is going to be explained in the Converting types into each other section in Chapter 5, Advanced Data Structures.
See also
- Using a vector recipe inChapter 2, Working with Collections
- Converting types into each other recipe inChapter 5, Advanced Data Structures