Constructors by Convention
Rust does not have a dedicated constructor keyword or implicit constructor syntax like new ClassName() in C++ or Java. Instead, structs are constructed directly via struct literal syntax (Type { field: value }) or through associated functions named by convention, starting with Type::new(...).
Standard Rust Constructor Naming Conventions
Following the Rust API Guidelines (C-CTOR), standard library and ecosystem crates use consistent names for constructor functions so callers immediately know their cost and behavior:
| Constructor Name | Signature Shape | Convention and Purpose |
|---|---|---|
new(...) |
fn new(...) -> Self |
The primary, natural constructor for a type |
with_*(...) |
fn with_capacity(cap: usize) -> Self |
Secondary constructor that configures an auxiliary resource or pre-allocation |
from_*(...) |
fn from_utf8(v: Vec<u8>) -> Result<Self, E> |
Constructor that converts or validates specific input components |
try_new(...) |
fn try_new(...) -> Result<Self, Error> |
Fallible primary constructor when invariant validation can fail |
Default::default() |
fn default() -> Self |
Zero-argument trait constructor for types with a sensible default value |
Primary and Secondary Constructors (new and with_*)
When a type has a clear zero-argument default state, implement both Type::new() and the Default trait:
#[derive(Debug, Default)]
pub struct RingBuffer<T> {
storage: Vec<T>,
limit: usize,
}
impl<T> RingBuffer<T> {
// Primary constructor
pub fn new() -> Self {
Self::with_limit(64)
}
// Secondary constructor specifying a custom limit and pre-allocating memory
pub fn with_limit(limit: usize) -> Self {
Self {
storage: Vec::with_capacity(limit),
limit,
}
}
}
fn main() {
let default_buf: RingBuffer<i32> = RingBuffer::new();
let custom_buf: RingBuffer<i32> = RingBuffer::with_limit(8);
println!(
"default_buf: limit = {}, capacity = {}",
default_buf.limit,
default_buf.storage.capacity()
);
println!(
"custom_buf: limit = {}, capacity = {}",
custom_buf.limit,
custom_buf.storage.capacity()
);
}
default_buf: limit = 64, capacity = 64
custom_buf: limit = 8, capacity = 8
Fallible Constructors Enforcing Type Invariants (try_new)
Because struct literals (Port { number: 0 }) bypass validation if fields are pub, keeping fields private and providing a fallible constructor (fn new(...) -> Result<Self, Error> or Option<Self>) guarantees that every existing instance of the struct satisfies its domain invariants:
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NonZeroPort(u16); // Private inner field (`0` is forbidden)
impl NonZeroPort {
pub fn new(port: u16) -> Option<Self> {
if port == 0 {
None
} else {
Some(Self(port))
}
}
pub fn get(self) -> u16 {
self.0
}
}
fn main() {
println!("Port 0: {:?}", NonZeroPort::new(0));
println!("Port 443: {:?}", NonZeroPort::new(443));
}
Port 0: None
Port 443: Some(NonZeroPort(443))
Implementing Default Alongside new()
Deriving #[derive(Default)] initializes every field with its own Default::default() value (0 for numbers, false for bool, "" for String, empty Vec). When a struct's sensible default differs from all-zeros, implement Default manually and delegate Self::new() to Self::default() (or vice versa):
#[derive(Debug)]
pub struct HttpClientConfig {
pub max_retries: u32,
pub timeout_secs: u64,
pub follow_redirects: bool,
}
impl HttpClientConfig {
pub fn new() -> Self {
Self {
max_retries: 3,
timeout_secs: 30,
follow_redirects: true,
}
}
}
impl Default for HttpClientConfig {
fn default() -> Self {
Self::new()
}
}
fn main() {
// Callers can override specific fields while falling back to `Default::default()`:
let cfg = HttpClientConfig {
timeout_secs: 5,
..Default::default()
};
println!("{cfg:?}");
}
HttpClientConfig { max_retries: 3, timeout_secs: 5, follow_redirects: true }