Rust's
syntax is similar to that of
C and
C++, By default, integer literals are in base-10, but different
radices are supported with prefixes, for example, for
binary numbers, for
octals, and for
hexadecimals. By default, integer literals default to as its type. Suffixes such as can be used to explicitly set the type of a literal. Byte literals such as are available to represent the
ASCII value (as a ) of a specific character. The
Boolean type is referred to as which can take a value of either or . A takes up 32 bits of space and represents a Unicode scalar value: a
Unicode codepoint that is not a
surrogate.
IEEE 754 floating point numbers are supported with for
single precision floats and for
double precision floats.
Compound types Compound types can contain multiple values. Tuples are fixed-size lists that can contain values whose types can be different. Arrays are fixed-size lists whose values are of the same type. Expressions of the tuple and array types can be written through listing the values, and can be accessed with (with tuples) or (with arrays): let tuple: (u32, bool) = (3, true); let array: [i8; 5] = [1, 2, 3, 4, 5]; let value = tuple.1; // true let value = array[2]; // 3 Arrays can also be constructed through copying a single value a number of times: let array2: [char; 10] = [' '; 10];
Ownership and references Rust's ownership system consists of rules that ensure memory safety without using a garbage collector. At compile time, each value must be attached to a variable called the
owner of that value, and every value must have exactly one owner. Values are moved between different owners through assignment or passing a value as a function parameter. Values can also be
borrowed, meaning they are temporarily passed to a different function before being returned to the owner. With these rules, Rust can prevent the creation and use of
dangling pointers: fn print_string(s: String) { println!("{}", s); } fn main() { let s = String::from("Hello, World"); print_string(s); // s consumed by print_string // s has been moved, so cannot be used any more // another print_string(s); would result in a compile error } The function takes ownership over the value passed in; Alternatively, can be used to indicate a
reference type (in ) and to create a reference (in ): fn print_string(s: &String) { println!("{}", s); } fn main() { let s = String::from("Hello, World"); print_string(&s); // s borrowed by print_string print_string(&s); // s has not been consumed; we can call the function many times } Because of these ownership rules, Rust types are known as
affine types, meaning each value may be used at most once. This enforces a form of
software fault isolation as the owner of a value is solely responsible for its correctness and deallocation. When a value goes out of scope, it is
dropped by running its
destructor. The destructor may be programmatically defined through implementing the
trait. This helps manage resources such as file handles, network sockets, and
locks, since when objects are dropped, the resources associated with them are closed or released automatically.
Lifetimes Object lifetime refers to the period of time during which a reference is valid; that is, the time between the object creation and destruction. These
lifetimes are implicitly associated with all Rust reference types. While often inferred, they can also be indicated explicitly with named lifetime parameters (often denoted , , and so on). A value's lifetime in Rust can be thought of as
lexically scoped, meaning that the duration of an object lifetime is inferred from the set of locations in the source code (i.e., function, line, and column numbers) for which a variable is valid. For example, a reference to a local variable has a lifetime from the expression it is declared in up until the last use of it. fn main() { let mut x = 5; // ------------------+- Lifetime 'a // | let r = &x; // -+-- Lifetime 'b | // | | println!("r: {}", r); // -+ | // Since r is no longer used, | // its lifetime ends | let r2 = &mut x; // -+-- Lifetime 'c | } // ------------------+ The borrow checker in the Rust compiler then enforces that references are only used in the locations of the source code where the associated lifetime is valid. In the example above, storing a reference to variable in is valid, as variable has a longer lifetime () than variable (). However, when has a shorter lifetime, the borrow checker would reject the program: fn main() { let r; // ------------------+- Lifetime 'a // | { // | let x = 5; // -+-- Lifetime 'b | r = &x; // ERROR: x does | | } // not live long -| | // enough | println!("r: {}", r); // | } // ------------------+ Since the lifetime of the referenced variable () is shorter than the lifetime of the variable holding the reference (), the borrow checker errors, preventing from being used from outside its scope. Lifetimes can be indicated using explicit
lifetime parameters on function arguments. For example, the following code specifies that the reference returned by the function has the same lifetime as (and
not necessarily the same lifetime as ): fn remove_prefix(mut original: &'a str, prefix: &str) -> &'a str { if original.starts_with(prefix) { original = original[prefix.len()..]; } original } In the compiler, ownership and lifetimes work together to prevent memory safety issues such as dangling pointers.
User-defined types User-defined types are created with the or keywords. The keyword is used to denote a
record type that groups multiple related values. s can take on different variants at runtime, with its capabilities similar to
algebraic data types found in functional programming languages. Both records and enum variants can contain
fields with different types. Alternative names, or aliases, for the same type can be defined with the keyword. The keyword can define methods for a user-defined type. Data and functions are defined separately. Implementations fulfill a role similar to that of
classes within other languages.
Standard library The Rust
standard library defines and implements many widely used custom data types, including core data structures such as , , and , as well as
smart pointer types. Rust provides a way to exclude most of the standard library using the attribute , for applications such as embedded devices. Internally, the standard library is divided into three parts, , , and , where and are excluded by . Rust uses the
option type Option to define optional values, which can be matched using if let or match to access the inner value: fn main() { let name1: Option = None; // In this case, nothing will be printed out if let Some(name) = name1 { println!("{name}"); } let name2: Option = Some("Matthew"); // In this case, the word "Matthew" will be printed out if let Some(name) = name2 { println!("{name}"); } } Similarly, Rust's
result type Result holds either a successfully computed value (the Ok variant) or an error (the Err variant). Like Option, the use of Result means that the inner value cannot be used directly; programmers must use a match expression, syntactic sugar such as ? (the "try" operator), or an explicit unwrap assertion to access it. Both Option and Result are used throughout the standard library and are a fundamental part of Rust's explicit approach to handling errors and missing data.
Pointers The & and reference types are guaranteed to not be null and point to valid memory. The raw pointer types and opt out of the safety guarantees, thus they may be null or invalid; however, it is impossible to dereference them unless the code is explicitly declared unsafe through the use of an block. Unlike dereferencing, the creation of raw pointers is allowed inside safe Rust code.
Type conversion 's Rust team (
linux.conf.au conference, Hobart, 2017)
Polymorphism Rust supports
polymorphism through
traits,
generic functions, and
trait objects.
Traits Common behavior between types is declared using traits and blocks: trait Zero: Sized { fn zero() -> Self; fn is_zero(&self) -> bool where Self: PartialEq, { self == &Zero::zero() } } impl Zero for u32 { fn zero() -> u32 { 0 } } impl Zero for f32 { fn zero() -> Self { 0.0 } } The example above includes a method which provides a default implementation that may be overridden when implementing the trait.
Generic functions A function can be made generic by adding type parameters inside angle brackets (), which only allow types that implement the trait: // zero is a generic function with one type parameter, Num fn zero() -> Num { Num::zero() } fn main() { let a: u32 = zero(); let b: f32 = zero(); assert!(a.is_zero() && b.is_zero()); } In the examples above, as well as are trait bounds that constrain the type to only allow types that implement or . Within a trait or impl, refers to the type that the code is implementing. Generics can be used in functions to allow implementing a behavior for different types without repeating the same code (see
bounded parametric polymorphism). Generic functions can be written in relation to other generics, without knowing the actual type.
Trait objects By default, traits use
static dispatch: the compiler
monomorphizes the function for each concrete type instance, yielding performance equivalent to type-specific code at the cost of longer compile times and larger binaries. When the exact type is not known at compile time, Rust provides
trait objects &dyn Trait and Box. Trait object calls use
dynamic dispatch via a lookup table; a trait object is a "fat pointer" carrying both a data pointer and a method table pointer. This indirection adds a small runtime cost, but it keeps a single copy of the code and reduces binary size. Only "object-safe" traits are eligible to be used as trait objects. This approach is similar to
duck typing, where all data types that implement a given trait can be treated as functionally interchangeable. The following example creates a list of objects where each object implements the Display trait: use std::fmt::Display; let v: Vec> = vec![ Box::new(3), Box::new(5.0), Box::new("hi"), ]; for x in v { println!("{x}"); } If an element in the list does not implement the Display trait, it will cause a compile-time error.
Memory management Rust does not use
garbage collection. Memory and other resources are instead managed through the "resource acquisition is initialization" convention, with optional
reference counting. Rust provides deterministic management of resources, with very low
overhead. Values are
allocated on the stack by default, and all
dynamic allocations must be explicit. The built-in reference types using the & symbol do not involve run-time reference counting. The safety and validity of the underlying pointers is verified at compile time, preventing
dangling pointers and other forms of
undefined behavior. Rust's type system separates shared,
immutable references of the form &T from unique, mutable references of the form &mut T. A mutable reference can be coerced to an immutable reference, but not vice versa.
Unsafe Rust's memory safety checks (See #Safety) may be circumvented through the use of blocks. This allows programmers to dereference arbitrary raw pointers, call external code, or perform other low-level functionality not allowed by safe Rust. Some low-level functionality enabled in this way includes
volatile memory access, architecture-specific intrinsics,
type punning, and inline assembly. Unsafe code is needed, for example, in the implementation of data structures. A frequently cited example is that it is difficult or impossible to implement
doubly linked lists in safe Rust. Programmers using unsafe Rust are considered responsible for upholding Rust's memory and type safety requirements, for example, that no two mutable references exist pointing to the same location. If programmers write code which violates these requirements, this results in
undefined behavior.
Macros Macros allow generation and transformation of Rust code to reduce repetition. Macros come in two forms, with
declarative macros defined through macro_rules!, and
procedural macros, which are defined in separate crates.
Declarative macros A declarative macro (also called a "macro by example") is a macro, defined using the macro_rules! keyword, that uses pattern matching to determine its expansion. Below is an example that sums over all its arguments: macro_rules! sum { ( $initial:expr $(, $expr:expr )* $(,)? ) => { $initial $(+ $expr)* } } fn main() { let x = sum!(1, 2, 3); println!("{x}"); // prints 6 } In this example, the macro named sum is defined using the form macro_rules! sum { (...) => { ... } }. The first part inside the parentheses of the definition, the macro pattern ( $initial:expr $(, $expr:expr )* $(,)? ) specifies the structure of input it can take. Here, $initial:expr represents the first expression, while $(, $expr:expr )* means there can be zero or more additional comma-separated expressions after it. The trailing $(,)? allows the caller to optionally include a final comma without causing an error. The second part after the arrow => describes what code will be generated when the macro is invoked. In this case, $initial $(+ $expr)* means that the generated code will start with the first expression, followed by a + and each of the additional expressions in sequence. The * again means "repeat this pattern zero or more times". This means, when the macro is later called in line 8, as sum!(1, 2, 3) the macro will resolve to 1 + 2 + 3 representing the addition of all of the passed expressions.
Procedural macros Procedural macros are Rust functions that run and modify the compiler's input
token stream, before any other components are compiled. They are generally more flexible than declarative macros, but are more difficult to maintain due to their complexity. Procedural macros come in three flavors: • Function-like macros custom!(...) • Derive macros #[derive(CustomDerive)] • Attribute macros #[custom_attribute]
Interface with C and C++ Rust supports the creation of
foreign function interfaces (FFI) through the keyword. A function that uses the C
calling convention can be written using . Symbols can be exported from Rust to other languages through the attribute, and symbols can be imported into Rust through blocks: • [unsafe(no_mangle)] pub extern "C" fn exported_from_rust(x: i32) -> i32 { x + 1 } unsafe extern "C" { fn imported_into_rust(x: i32) -> i32; } The attribute enables deterministic memory layouts for s and s for use across FFI boundaries. External libraries such as and can generate Rust bindings for C/C++. == Safety ==