Type
A type is a classification of values. Values of the same type have a similar meaning and purpose, and support the same operations. Values of different types can have the same memory representation – for example, both u8
and bool
can be represented by a zero, but because they are different types, the zero has a different meaning.
In Rust, types are enforced at compile time to prevent logic errors. However, since memory safety invariants are encoded in the type system, it also prevents memory safety bugs.
Types in Rust can be divided into:
- Primitive types
- Structs
- Enums
- Unions
- Trait objects
- Opaque types (
impl Trait
)
Types can be generic. Generic types are sometimes called type constructors. For example, Box<T>
is generic over T
. By substituting T
with a type, new types such as Box<bool>
or Box<String>
can be created.
Traits aren't types, but they categorize types with certain properties or functionality.
Example
fn function(x: i32) -> u32 {
// type of x ~~^^^ ^^^ return type
// type conversion:
let y = x as u32;
// the type of 5 is inferred:
y + 5
}
Implementing a type
After declaring a type,impl
blocks can be used to add functionality to it. These are called inherent impl
s, and functions within them are called inherent functions (or methods):// declare a struct type:
pub struct Foo;
// an inherent impl:
impl Foo {
// an inherent function:
pub fn new() -> Self {
Foo
}
}
impl
blocks (except primitive types). Unlike trait functions, inherent functions can have a visibility modifier. Inherent impl
s must always be in the same crate as the type they implement.
Nominal and structural types
Types can be divided into nominal types (types with a name) and structural types. Only nominal types can have inherent impl
blocks.
Nominal types include:
- Some primitive types (numeric types, bool, char, str)
- Structs
- Enums
- Unions
- Trait objects
All other types are structural:
Type aliases
Type aliases don't create a new type, just a new name to refer to a type. Different aliases that refer to the same type can be used interchangeably. Type aliases can be generic:struct Foo<T>(T);
type MaybeFoo<T> = Result<Foo<T>, ()>;
// with a generic error type that defaults to String:
type FooResult<T, Err = String> = Result<Foo<T>, Err>;
Type inference
Types of parameters and the return type of functions need to be specified explicitly. In other places, types can often be automatically inferred.
The compiler does this by giving values whose type isn't known an {unknown}
type placeholder. Then the surrounding context is used to replace the type placeholders with actual types.
Example
Let's look at an example where a lot of type inference takes place, and see how the compiler approaches it:fn foo(iter: impl IntoIterator<Item = i32>) {
// what is x?
let x: Vec<_> = iter
.into_iter()
.map(|n| n * 2)
.collect();
}
iter
, but we know that it implements the IntoIterator
trait. IntoIterator::into_iter()
returns Self::IntoIter
, which implements Iterator<Item = Self::Item>
.
Since the Item
is i32
, this tells the compiler that n
in line 5 is an i32
. Since i32
implements Mul<Rhs = Self>
, Rust also infers the type i32
for the 2
literal. The return type is Self::Output
, which also happens to be i32
.
Iterator::map()
accepts a closure and returns a type which implements Iterator
. The Item
of the iterator is the return type of the closure (i32
).
Iterator::collect()
returns a generic type B
that implements FromIterator<Self::Item>
. The compiler knows that the return type must be a Vec
, so it looks for implementations of FromIterator<Self::Item>
for Vec
, and it finds impl<T> FromIterator<T> for Vec<T>
. This means that the type in the Vec<_>
is the same type as Self::Item
, so Rust correctly infers the type Vec<i32>
for x
.