The syntax of Rust is the set of rules defining how a Rust program is written and compiled.
Rust's syntax is similar to that of C and C++, although many of its features were influenced by functional programming languages such as OCaml.
Basics Although Rust syntax is heavily influenced by the syntaxes of C and C++, the syntax of Rust is far more distinct from C++ syntax than Java or C#, as those languages have more C-style declarations, primitive names, and keywords.
Below is a "Hello, World!" program in Rust. The keyword denotes a function, and the macro (see ) prints the message to standard output. Statements in Rust are separated by semicolons. fn main() { println!("Hello, World!"); }
Reserved words Keywords The following 42 words are reserved, and may not be used as identifiers.
as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return Self self static struct super trait true type union unsafe use where while
Unused words The following words are reserved as keywords, but currently have no use or purpose. There are 14 unused words.
abstract become box do final gen macro override priv try typeof unsized virtual yield
Variables Variables in Rust are defined through the keyword. The example below assigns a value to the variable with name and outputs its value.
fn main() { let foo: i32 = 10; println!("The value of foo is {foo}"); }
Variables are immutable by default, but adding the keyword allows the variable to be mutated. The following example uses , which denotes the start of a comment.
fn main() { // This code would not compile without adding "mut". let mut foo: i32 = 10; println!("The value of foo is {foo}"); foo = 20; println!("The value of foo is {foo}"); }
Multiple expressions can define multiple variables with the same name, known as variable shadowing. Variable shadowing allows transforming variables without having to name the variables differently. The example below declares a new variable with the same name that is double the original value:
fn main() { let foo: i32 = 10; // This will output "The value of foo is 10" println!("The value of foo is {foo}"); let foo: i32 = foo * 2; // This will output "The value of foo is 20" println!("The value of foo is {foo}"); }
Variable shadowing is also possible for values of different types. For example, going from a string to its length in bytes:
fn main() { let letters: str = "abc"; let letters: usize = letters.len(); }
Block expressions and control flow
A block expression is delimited by curly brackets. When the last expression inside a block does not end with a semicolon, the block evaluates to the value of that trailing expression:
fn main() { let x: i32 = { println!("this is inside the block"); 1 + 2
println!("1 + 2 = {x}"); }
Trailing expressions of function bodies are used as the return value:
fn add_two(x: i32) -> i32 { x + 2 }
expressions
An conditional expression executes code based on whether the given value is . can be used for when the value evaluates to , and can be used for combining multiple expressions.
fn main() { let x: i32 = 10; if x > 5 { println!("value is greater than five");
if x % 7 == 0 { println!("value is divisible by 7"); } else if x % 5 == 0 { println!("value is divisible by 5"); } else { println!("value is not divisible by 7 or 5");
and blocks can evaluate to a value, which can then be assigned to a variable:
fn main() { let x: i32 = 10; let new_x: i32 = if x % 2 == 0 { x / 2 } else { 3 * x + 1 }; println!("{new_x}"); }
loops while can be used to repeat a block of code while a condition is met.
fn main() { // Iterate over all integers from 4 to 10 let mut value: i32 = 4; while value <= 10 { println!("value = {value}"); value += 1;
loops and iterators
For loops in Rust loop over elements of a collection. expressions work over any iterator type.
fn main() { // Using `for` with range syntax for the same functionality as above // The syntax 4..=10 means the range from 4 to 10, up to and including 10. for value in 4..=10 { println!("value = {value}");
In the above code, is a value of type which implements the trait. The code within the curly braces is applied to each element returned by the iterator.
Iterators can be combined with functions over iterators like , , and . For example, the following adds up all numbers between 1 and 100 that are multiples of 3:
(1..=100).filter(|&x: i8| -> bool x % 3 == 0).sum()
and statements
More generally, the keyword allows repeating a portion of code until a occurs. may optionally exit the loop with a value. In the case of nested loops, labels denoted by can be used to break an outer loop rather than the innermost loop.
fn main() { let value: i32 = 456; let mut x: i32 = 1; let y = loop { x *= 10; if x > value { break x / 10;
println!("largest power of ten that is smaller than or equal to value: {y}");
let mut up: i32 = 1; 'outer: loop { let mut down: i32 = 120; loop { if up > 100 { break 'outer;
if down < 4 { break;
down /= 2; up += 1; println!("up: {up}, down: {down}");
up *= 2;
Pattern matching The and expressions can be used for pattern matching. For example, can be used to double an optional integer value if present, and return zero otherwise:
fn double(x: Option<u64>) -> u64 { match x { Some(y) => y * 2, None => 0,
Equivalently, this can be written with and :
fn double(x: Option<u64>) -> u64 { if let Some(y) = x { y * 2 } else { 0
Types Rust is strongly typed and statically typed, meaning that the types of all variables must be known at compilation time. Assigning a value of a particular type to a differently typed variable causes a compilation error. Type inference is used to determine the type of variables if unspecified.
The type (), called the "unit type" in Rust, is a concrete type that has exactly one value (itself). It occupies no memory (as it represents the absence of value). All functions that do not have an indicated return type implicitly return (). It is similar to in other C-style languages, however denotes the absence of a type and cannot have any value.
The default integer type is , and the default floating point type is . If the type of a literal number is not explicitly provided, it is either inferred from the context or the default type is used.
Primitive types Integer types in Rust are named based on the signedness and the number of bits the type takes. For example, is a signed integer that takes 32 bits of storage, whereas is unsigned and only takes 8 bits of storage. and take storage depending on the architecture of the computer that runs the code, for example, on computers with 32-bit architectures, both types will take up 32 bits of space.
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 or :
let tuple: (u32, i64) = (3, -3); let array: [i8; 5] = [1, 2, 3, 4, 5]; let value: i64 = tuple.1; // -3 let value: i8 = array; // 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 = 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 = 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 linear or affine types, meaning each value can be used exactly 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).
Lifetimes 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 corresponding to the block it is defined in:
fn main() { let x: i32 = 5; // ------------------+- Lifetime 'a // | let r: &i32 = &x; // -+-- Lifetime 'b | // | | println!("r: {}", r); // | | // | | // -+ | } // ------------------+
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: &i32; // ------------------+- Lifetime 'a // | { // | let x: i32 = 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<'a>(mut original: &'a str, prefix: &str) -> &'a str {