Constructors
Rust does not require types to have constructors, and there is no special syntax or naming given to constructors.
The Rust API Guidelines give a concise summary of recommendations, but I wanted to highlight a few points.
Constructor Functions
Types can be instantiated entirely by fields:
#![allow(unused)] fn main() { struct Vehicle { name: String, wheel_count: u8, } let v = Vehicle { name: "Old Faithful".to_string(), wheel_count: 4, }; }
If the fields of a type are not visible (e.g. a type with a private field in a different module/crate), then the type may not be constructed directly. A function must be provided which can construct an instance of the type.
Usually, the function is a static function associated with the type in an impl
block like so:
#![allow(unused)] fn main() { struct Vehicle { name: String, wheel_count: u8 } impl Vehicle { pub fn new(name: String, wheel_count: u8) -> Vehicle { Vehicle { name, wheel_count, } } } let v = Vehicle::new("Old Faithful".to_string(), 4); }
Note that the new function is not special. It is only by convention that a
Rust constructor be called new. It could have been written like:
#![allow(unused)] fn main() { struct Vehicle { name: String, wheel_count: u8 } impl Vehicle { pub fn with_name_and_wheel_count(name: String, wheel_count: u8) -> Vehicle { Vehicle { name, wheel_count, } } } let v = Vehicle::with_name_and_wheel_count("Old Faithful".to_string(), 4); }
The function could have also been a free function like:
#![allow(unused)] fn main() { struct Vehicle { name: String, wheel_count: u8 } pub fn new_vehicle(name: String, wheel_count: u8) -> Vehicle { Vehicle { name, wheel_count, } } let v = new_vehicle("Old Faithful".to_string(), 4); }
There is no special syntax, no special method name (e.g. do not need to use the
type’s name like Java or init like Swift), and no unique privileges given to
constructing functions in Rust.
Constructors are important and common in Rust, but there is not much more to learn compared to regular functions.
Use of Self
Self with an uppercaseS refers to the current impl’s type. I recommend
using Self whenever possible.
#![allow(unused)] fn main() { struct Vehicle { name: String, wheel_count: u8 } impl Vehicle { pub fn new(name: String, wheel_count: u8) -> Self { Self { name, wheel_count, } } } let v = Vehicle::new("Old Faithful".to_string(), 4); }
In the above, Self replaced the return type for new(...). Self also
replaced the type in the method body, so it becamse Self { name, wheel_count }
instead of Vehicle { name, wheel_count }.
Using Self as a return type gives a signal that the function might be a
constructor. Functions normally do not return the implementing type except when
using the Builder design pattern.
Another benefit is you do not have to change the return type if the type needs to be renamed. IDEs should handle any changes when renaming types, but in case you are not using one.
Self is not solely for constructors, but it is commonly used in constructors.
Default before empty new
When implementing an empty no argument constructor, you should always consider
implementing the Default trait.
If all of the fields in the type implement the Default trait, then Default
implementation can be derived for the type:
#![allow(unused)] fn main() { #[derive(Default)] struct Coordinates { x: i32, y: i32, } #[derive(Default)] struct Entity { coords: Coordinates, name: String, } let c = Coordinates::default(); let e1 = Entity::default(); let e2 = Entity { name: "Test".to_string(), ..Default::default() }; }
The ..Default::default() syntax is weird, but it initializes the rest of the
struct with the default values.
There are methods which use
the Default trait such as
Option::unwrap_or_default() or HashMap’s
Entry::or_default().
Default can make a type easier to use in the Rust ecosystem.
Implement From/TryFrom before conversion constructors
If there is a straightforward implementation of From or
TryFrom for a type, implement the From/TryFrom traits
before implementing a constructor.
I have never regretted implementing a From trait. By implementing the From
trait, the Into trait is also implemented. I find calling
MyType::from(<other type>) and instance_of_my_type.into() to be natural to
convert between types. In other languages, constructors would be overloaded with
different parameter types.
Be sure to follow the traits’ documentation. From should only be implemented
when the function can be called without panicking (e.g. no unwraps).
The only time when you may want to forgo a From implementation is when there
can be some confusion what the value means. For instance, if a buffer of bytes
can be interpreted differently (e.g. big endian vs. little endian), then it is
better to only have explictly named constructor functions like from_be_bytes.
Use Generic Functions If Multiple Similar Types
If you have multiple constructing functions like with_string and with_str
which uses the value similarly, try to use a generic function instead.
struct Data {
s: String,
}
impl Data {
fn from_string<S: ToString>(s: S) -> Self {
Self {
s: s.to_string(),
}
}
}
Using generics gives you overloaded (by type) functions. In addition for the
above example, I would also implement From<String> and From<&str>.
Constructor Names
Defer to the Rust API Guidelines for naming.
new, from_..., and with_... are common constructor names.
Note the importance of communicating how to interpret ambiguous values (e.g. a
random byte buffer) by having descriptive function names (e.g. from_utf8 vs
from_utf16).