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_variables)]
#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_variables)]
#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_variables)]
#fn main() {
impl Foo {
    pub fn frob(&self, w: widget) { /* ... */ }
}
#}

over


# #![allow(unused_variables)]
#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_variables)]
#fn main() {
fn foo() -> (Bar, Bar)
#}

over


# #![allow(unused_variables)]
#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_variables)]
#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_variables)]
#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_variables)]
#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 UpdSocket::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; a From impl cannot. One example of this is Box::from_raw.
  • A from_ constructor can accept additional arguments to disambiguate the meaning of the source data, as in u64::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 in u64::from_be or String::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