Predictability
Smart pointers do not add inherent methods (C-SMART-PTR)
For example, this is why the Box::into_raw
function is defined the way it
is.
#![allow(unused)] fn main() { impl<T> Box<T> where T: ?Sized { fn into_raw(b: Box<T>) -> *mut T { /* ... */ } } let boxed_str: Box<str> = /* ... */; let ptr = Box::into_raw(boxed_str); }
If this were defined as an inherent method instead, it would be confusing at the
call site whether the method being called is a method on Box<T>
or a method on
T
.
#![allow(unused)] fn main() { impl<T> Box<T> where T: ?Sized { // Do not do this. fn into_raw(self) -> *mut T { /* ... */ } } let boxed_str: Box<str> = /* ... */; // This is a method on str accessed through the smart pointer Deref impl. boxed_str.chars() // This is a method on Box<str>...? boxed_str.into_raw() }
Conversions live on the most specific type involved (C-CONV-SPECIFIC)
When in doubt, prefer to_
/as_
/into_
to from_
, because they are more
ergonomic to use (and can be chained with other methods).
For many conversions between two types, one of the types is clearly more
"specific": it provides some additional invariant or interpretation that is not
present in the other type. For example, str
is more specific than &[u8]
,
since it is a UTF-8 encoded sequence of bytes.
Conversions should live with the more specific of the involved types. Thus,
str
provides both the as_bytes
method and the from_utf8
constructor
for converting to and from &[u8]
values. Besides being intuitive, this
convention avoids polluting concrete types like &[u8]
with endless conversion
methods.
Functions with a clear receiver are methods (C-METHOD)
Prefer
#![allow(unused)] fn main() { impl Foo { pub fn frob(&self, w: widget) { /* ... */ } } }
over
#![allow(unused)] fn main() { pub fn frob(foo: &Foo, w: widget) { /* ... */ } }
for any operation that is clearly associated with a particular type.
Methods have numerous advantages over functions:
- They do not need to be imported or qualified to be used: all you need is a value of the appropriate type.
- Their invocation performs autoborrowing (including mutable borrows).
- They make it easy to answer the question "what can I do with a value of type
T
" (especially when using rustdoc). - They provide
self
notation, which is more concise and often more clearly conveys ownership distinctions.
Functions do not take out-parameters (C-NO-OUT)
Prefer
#![allow(unused)] fn main() { fn foo() -> (Bar, Bar) }
over
#![allow(unused)] fn main() { fn foo(output: &mut Bar) -> Bar }
for returning multiple Bar
values.
Compound return types like tuples and structs are efficiently compiled and do not require heap allocation. If a function needs to return multiple values, it should do so via one of these types.
The primary exception: sometimes a function is meant to modify data that the caller already owns, for example to re-use a buffer:
#![allow(unused)] fn main() { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> }
Operator overloads are unsurprising (C-OVERLOAD)
Operators with built in syntax (*
, |
, and so on) can be provided for a type
by implementing the traits in std::ops
. These operators come with strong
expectations: implement Mul
only for an operation that bears some resemblance
to multiplication (and shares the expected properties, e.g. associativity), and
so on for the other traits.
Only smart pointers implement Deref
and DerefMut
(C-DEREF)
The Deref
traits are used implicitly by the compiler in many circumstances,
and interact with method resolution. The relevant rules are designed
specifically to accommodate smart pointers, and so the traits should be used
only for that purpose.
Examples from the standard library
Constructors are static, inherent methods (C-CTOR)
In Rust, "constructors" are just a convention. There are a variety of conventions around constructor naming, and the distinctions are often subtle.
A constructor in its most basic form is a new
method with no arguments.
#![allow(unused)] fn main() { impl<T> Example<T> { pub fn new() -> Example<T> { /* ... */ } } }
Constructors are static (no self
) inherent methods for the type that they
construct. Combined with the practice of fully importing type names, this
convention leads to informative but concise construction:
#![allow(unused)] fn main() { use example::Example; // Construct a new Example. let ex = Example::new(); }
The name new
should generally be used for the primary method of instantiating
a type. Sometimes it takes no arguments, as in the examples above. Sometimes it
does take arguments, like Box::new
which is passed the value to place in the
Box
.
Some types' constructors, most notably I/O resource types, use distinct naming
conventions for their constructors, as in File::open
, Mmap::open
,
TcpStream::connect
, and UdpSocket::bind
. In these cases names are chosen
as appropriate for the domain.
Often there are multiple ways to construct a type. It's common in these cases
for secondary constructors to be suffixed _with_foo
, as in
Mmap::open_with_offset
. If your type has a multiplicity of construction
options though, consider the builder pattern (C-BUILDER) instead.
Some constructors are "conversion constructors", methods that create a new type
from an existing value of a different type. These typically have names beginning
with from_
as in std::io::Error::from_raw_os_error
. Note also though the
From
trait (C-CONV-TRAITS), which is quite similar. There are three
distinctions between a from_
-prefixed conversion constructor and a From<T>
impl.
- A
from_
constructor can be unsafe; aFrom
impl cannot. One example of this isBox::from_raw
. - A
from_
constructor can accept additional arguments to disambiguate the meaning of the source data, as inu64::from_str_radix
. - A
From
impl is only appropriate when the source data type is sufficient to determine the encoding of the output data type. When the input is just a bag of bits like inu64::from_be
orString::from_utf8
, the conversion constructor name is able to identify their meaning.
Note that it is common and expected for types to implement both Default
and a
new
constructor. For types that have both, they should have the same behavior.
Either one may be implemented in terms of the other.
Examples from the standard library
std::io::Error::new
is the commonly used constructor for an IO error.std::io::Error::from_raw_os_error
is a conversion constructor based on an error code received from the operating system.Box::new
creates a new container type, taking a single argument.File::open
opens a file resource.Mmap::open_with_offset
opens a memory-mapped file, with additional options.