Rust - Syntax & Basics
Overview
Estimated time: 30–45 minutes
Understand Rust's fundamental syntax, the distinction between expressions and statements, and basic language conventions that make Rust unique among systems programming languages.
Learning Objectives
- Understand expressions vs statements in Rust
- Master basic syntax patterns and conventions
- Learn code organization and style guidelines
- Practice with fundamental language constructs
Prerequisites
Expressions vs Statements
One of Rust's key concepts is the distinction between expressions and statements:
Statements
Statements perform actions but don't return values:
fn main() {
let x = 5; // Statement: variable binding
let y = 10; // Statement: another binding
println!("Hello"); // Statement: macro call
}
Expressions
Expressions evaluate to values and can be used anywhere a value is expected:
fn main() {
let x = 5;
let y = {
let inner = 3;
inner + 1 // Expression: no semicolon!
}; // y = 4
println!("y is: {}", y);
// If is an expression
let condition = true;
let number = if condition { 5 } else { 6 };
println!("number is: {}", number);
}
Output:
y is: 4
number is: 5
Basic Syntax Elements
Code Blocks
Blocks create new scopes and are expressions:
fn main() {
let x = 5;
let y = {
let x = 3; // Shadows outer x
x + 1 // Returns 4
}; // x goes back to 5
println!("x: {}, y: {}", x, y);
}
Output:
x: 5, y: 4
Semicolons Matter
Adding a semicolon turns an expression into a statement:
fn returns_value() -> i32 {
42 // Expression: returns 42
}
fn returns_unit() -> () {
42; // Statement: returns () (unit type)
}
fn main() {
let x = returns_value();
let y = returns_unit();
println!("x: {}, y: {:?}", x, y);
}
Output:
x: 42, y: ()
Function Syntax
// Function with parameters and return type
fn add_numbers(a: i32, b: i32) -> i32 {
a + b // Expression return
}
// Function with explicit return
fn multiply(a: i32, b: i32) -> i32 {
return a * b; // Statement return
}
// Function with no return value (returns unit type)
fn print_sum(a: i32, b: i32) {
println!("Sum: {}", a + b);
}
fn main() {
let sum = add_numbers(5, 3);
let product = multiply(4, 6);
print_sum(sum, product);
println!("Sum: {}, Product: {}", sum, product);
}
Output:
Sum: 29
Sum: 8, Product: 24
Variable Bindings and Patterns
Basic Bindings
fn main() {
let x = 5; // Immutable binding
let mut y = 10; // Mutable binding
y = 15; // OK: y is mutable
// x = 10; // Error: x is immutable
println!("x: {}, y: {}", x, y);
}
Destructuring
fn main() {
let tuple = (1, 2, 3);
let (a, b, c) = tuple; // Destructure tuple
let array = [1, 2, 3, 4, 5];
let [first, second, ..] = array; // Destructure array
println!("a: {}, b: {}, c: {}", a, b, c);
println!("first: {}, second: {}", first, second);
}
Output:
a: 1, b: 2, c: 3
first: 1, second: 2
Comments and Documentation
Regular Comments
fn main() {
// Single line comment
let x = 5; // Comment at end of line
/*
* Multi-line comment
* Can span multiple lines
*/
let y = 10;
}
Documentation Comments
/// This is a documentation comment for the function below
/// It supports **markdown** formatting
///
/// # Examples
///
/// ```
/// let result = add_one(5);
/// assert_eq!(result, 6);
/// ```
fn add_one(x: i32) -> i32 {
x + 1
}
fn main() {
let result = add_one(5);
println!("Result: {}", result);
}
Naming Conventions
Standard Conventions
// Variables and functions: snake_case
let my_variable = 42;
fn my_function() {}
// Types: PascalCase
struct MyStruct {}
enum MyEnum {}
// Constants: SCREAMING_SNAKE_CASE
const MAX_SIZE: usize = 100;
static GLOBAL_VAR: i32 = 42;
// Modules: snake_case
mod my_module {}
Macros vs Functions
fn main() {
// Function call
println("Hello"); // Error: not a function
// Macro call (note the !)
println!("Hello"); // OK: macro
// Other common macros
vec![1, 2, 3]; // vec! macro
format!("Hello {}", "world"); // format! macro
panic!("Something went wrong"); // panic! macro
}
Control Flow Syntax
fn main() {
let number = 6;
// If expression
let description = if number % 2 == 0 {
"even"
} else {
"odd"
};
// Match expression
let size = match number {
1..=5 => "small",
6..=10 => "medium",
_ => "large",
};
println!("Number {} is {} and {}", number, description, size);
}
Output:
Number 6 is even and medium
Common Pitfalls
Semicolon Confusion
fn wrong() -> i32 {
let x = 5;
x + 1; // Error: returns (), not i32
}
fn correct() -> i32 {
let x = 5;
x + 1 // OK: returns i32
}
Shadowing vs Mutation
fn main() {
let x = 5;
let x = x + 1; // Shadowing: creates new variable
let mut y = 5;
y = y + 1; // Mutation: changes existing variable
println!("x: {}, y: {}", x, y);
}
Checks for Understanding
Question 1
What's the difference between these two code blocks?
let x = {
let y = 3;
y + 1
};
let x = {
let y = 3;
y + 1;
};
Click to see answer
The first block returns the value 4 (expression), while the second returns () (statement due to semicolon). The first x will be 4, the second won't compile if expecting an integer.
Question 2
Will this function compile? Why or why not?
fn mystery() -> i32 {
if true {
42
} else {
"hello"
}
}
Click to see answer
No, it won't compile. Both branches of an if expression must return the same type. One returns i32, the other returns &str.
Question 3
What will this print?
fn main() {
let x = 5;
let x = "hello";
let x = x.len();
println!("{}", x);
}
Click to see answer
It will print "5". This demonstrates variable shadowing - each let creates a new variable that shadows the previous one.