Expressions

In C, expressions and statements are different. Expressions have values. Statements don’t.

In Rust, if and match can produce values. Most of the control flow tools in C are statements. In Rust, they are all expressions.

Declarations

You may occasionally see code that seems to redeclare an existing variable, like this:

for line in file.lines() {
    let line = line?;
    ...
}

// equivalent to

for line_result in file.lines() {
    let line = line_result?;
    ...
}

The let declaration creates a new, second variable, of a different type. The type of the first variable line is Result<String, io::Error>. The second line is a String. Its definition supersedes the first’s for the rest of the block. This is called shadowing and is very common in Rust programs.

When an fn is declared inside a block, its scope is the entire block—that is, it can be used throughout the enclosing block. A nested fn cannot access local variables or arguments that happen to be in scope.

if and match

match code {
    0 => println!("OK"),
    1 => println!("Wires Tangled"),
    2 => println!("User Asleep"),
    _ => println!("Unrecognized Error {}", code)
}

All blocks of an if expression must produce values of the same type. Similarly, all arms of a match expression must have the same type.

if let

if let pattern = expr {
    block1
} else {
    block2
}

The given expr either matches the pattern, in which case block1 runs, or doesn’t match, and block2 runs.

Loops

while condition {
  block
}

while let pattern = expr {
  block
}

loop {
  // Use loop to write infinite loops. It executes the block repeatedly forever (or until a break or return is reached or the thread panics).
  block
}

for pattern in iterable {
  block
}

The .. operator produces a range, a simple struct with two fields: start and end. 0..20 is the same as std::ops::Range { start: 0, end: 20 }. Ranges can be used with for loops because Range is an iterable type: it implements the std::iter::IntoIterator trait.

let strings: Vec<String> = error_messages();
for s in strings {                  // each String is moved into s here...
    println!("{}", s);
}                                   // ...and dropped here
println!("{} error(s)", strings.len()); // error: use of moved value

Control Flow in Loops

Within the body of a loop, you can give break an expression, whose value becomes that of the loop:

// Each call to `next_line` returns either `Some(line)`, where
// `line` is a line of input, or `None`, if we've reached the end of
// the input. Return the first line that starts with "answer: ".
// Otherwise, return "answer: nothing".
let answer = loop {
    if let Some(line) = next_line() {
        if line.starts_with("answer: ") {
            break line;
        }
    } else {
        break "answer: nothing";
    }
};

A loop can be labeled with a lifetime. In the following example, 'search: is a label for the outer for loop. Thus, break 'search exits that loop, not the inner loop:

'search:
for room in apartment {
    for spot in room.hiding_spots() {
        if spot.contains(keys) {
            println!("Your keys are {} in the {}.", spot, room);
            break 'search;
        }
    }
}

A break can have both a label and a value expression. Labels can also be used with continue.

return Expressions

return without a value is shorthand for return ().

We used the ? operator to check for errors after calling a function that can fail:

let output = File::create(filename)?;
let output = match File::create(filename) {
    Ok(f) => f,
    Err(err) => return Err(err)
};

Expressions that don’t finish normally are assigned the special type !, and they’re exempt from the rules about types having to match. You can see ! in the function signature of std::process::exit():

fn exit(code: i32) -> !

The ! means that exit() never returns. It’s a divergent function.

You can write divergent functions of your own using the same syntax, and this is perfectly natural in some cases:

fn serve_forever(socket: ServerSocket, handler: ServerHandler) -> ! {
    socket.listen();
    loop {
        let s = socket.accept();
        handler.handle(s);
    }
}

Rust then considers it an error if the function can return normally.

Function and Method Calls

. operator is ease with the types.

Syntax for generic types:

return Vec::<i32>::with_capacity(1000);  // ok, using ::<

let ramp = (0 .. n).collect::<Vec<i32>>();  // ok, using ::<

The symbol ::<...> is affectionately known in the Rust community as the turbofish.

Fields and Elements

ame.black_pawns   // struct field
coords.1           // tuple element
pieces[i]          // array element

Expressions like these three are called lvalues, because they can appear on the left side of an assignment.

Extracting a slice from an array or vector is straightforward:

let second_half = &game_moves[midpoint .. end];

The ..= operator produces end-inclusive (or closed) ranges, which do include the end value:

..= b    // RangeToInclusive { end: b }
a ..= b  // RangeInclusive::new(a, b)

Arithmetic, Bitwise, Comparison, and Logical Operators

Rust uses ! instead of ~ for bitwise NOT.

Bit shifting is always sign-extending on signed integer types and zero-extending on unsigned integer types. Since Rust has unsigned integers, it does not need an unsigned shift operator, like Java’s >>> operator.

Bitwise operations have higher precedence than comparisons, unlike C, so if you write x & BIT != 0, that means (x & BIT) != 0, as you probably intended. This is much more useful than C’s interpretation, x & (BIT != 0), which tests the wrong bit!

Type Casts

Rust does not have C’s increment and decrement operators ++ and --.

Numbers may be cast from any of the built-in numeric types to any other.

Values of type bool or char, or of a C-like enum type, may be cast to any integer type.


Some casts involving unsafe pointer types are also allowed.

  • Values of type &String auto-convert to type &str without a cast.

  • Values of type &Vec<i32> auto-convert to &[i32].

  • Values of type &Box<Chessboard> auto-convert to &Chessboard.

These are called deref coercions, because they apply to types that implement the Deref built-in trait. The purpose of Deref coercion is to make smart pointer types, like Box, behave as much like the underlying value as possible. Using a Box<Chessboard> is mostly just like using a plain Chessboard, thanks to Deref.

Closures

Rust has closures, lightweight function-like values. A closure usually consists of an argument list, given between vertical bars, followed by an expression:

let is_even = |x| x % 2 == 0;

Last updated