ZetCode

Rust operators and expressions

last modified October 25, 2026

In this article we show how to use operators and expressions in Rust.

An operator is a symbol that performs an operation on one or more values, called operands. An expression is a piece of code that evaluates to a value; it is built from literals, variables, operators, and function calls. The order in which the operators of an expression are evaluated is given by their precedence and associativity.

Rust is an expression-oriented language. Besides arithmetic, also blocks, if, and match are expressions that produce a value. Rust has no increment and decrement operators (++ and --) and no ternary operator.

The examples were tested with Rust 1.97 and the 2024 edition. They are run as Cargo projects with cargo run -q.

Unary operators

Unary operators work with a single operand.

main.rs
fn main() {
    let a = 1;
    println!("{}", -a);
    println!("{}", -(-a));

    let is_married = false;
    println!("{}", !is_married);

    let bits: u8 = 0b0000_1111;
    println!("{:08b}", !bits);

    let val = 5;
    let r = &val;
    println!("{}", *r + 1);
}

The example presents the unary minus, the not operator, and dereferencing.

let bits: u8 = 0b0000_1111;
println!("{:08b}", !bits);

For integers, the ! operator flips every bit. Rust has no separate ~ operator as C has.

let r = &val;
println!("{}", *r + 1);

The & operator creates a reference to val; the * operator reads the value behind it.

$ cargo run -q
-1
1
true
11110000
6

Arithmetic operators

The arithmetic operators are +, -, *, /, and % (remainder).

main.rs
fn main() {
    let a = 17;
    let b = 5;

    println!("{a} + {b} = {}", a + b);
    println!("{a} - {b} = {}", a - b);
    println!("{a} * {b} = {}", a * b);
    println!("{a} / {b} = {}", a / b);
    println!("{a} % {b} = {}", a % b);

    println!("17.0 / 5.0 = {}", 17.0 / 5.0);
    println!("-17 / 5 = {}", -17 / 5);
    println!("-17 % 5 = {}", -17 % 5);
    println!("(-17_i32).rem_euclid(5) = {}", (-17_i32).rem_euclid(5));
}

The program applies the arithmetic operators to two integers.

println!("{a} / {b} = {}", a / b);

Division of two integers is an integer division; the fractional part is discarded. To get 3.4, the operands must be floating-point numbers.

println!("-17 / 5 = {}", -17 / 5);
println!("-17 % 5 = {}", -17 % 5);

Integer division rounds toward zero, and the remainder has the sign of the left operand.

println!("(-17_i32).rem_euclid(5) = {}", (-17_i32).rem_euclid(5));

The rem_euclid method returns a remainder that is never negative, which is often what we need, for instance when computing positions in a circular buffer.

$ cargo run -q
17 + 5 = 22
17 - 5 = 12
17 * 5 = 85
17 / 5 = 3
17 % 5 = 2
17.0 / 5.0 = 3.4
-17 / 5 = -3
-17 % 5 = -2
(-17_i32).rem_euclid(5) = 3

Mixing types

Rust does not convert numeric types implicitly. Both operands of an arithmetic operator must have the same type; conversions are written with the as operator.

main.rs
fn main() {
    let count: i32 = 7;
    let price: f64 = 2.5;

    let total = count as f64 * price;
    println!("{total}");

    let sum = 10;
    let n = 4;
    println!("{} {}", sum / n, sum as f64 / n as f64);

    let big: i32 = 300;
    println!("{}", big as u8);
    println!("{}", -1_i32 as u32);
    println!("{}", 3.99_f64 as i32);
}

We multiply an integer by a floating-point number and convert between integer types.

let total = count as f64 * price;

The expression count * price would not compile, because it multiplies an i32 by an f64. We convert the count first. The as operator has a higher precedence than *.

println!("{}", big as u8);
println!("{}", -1_i32 as u32);
println!("{}", 3.99_f64 as i32);

The as conversions never fail, but they can lose information. A value too large for the target integer type is truncated to its lowest bits: 300 becomes 44. A negative number cast to an unsigned type is reinterpreted, and a float is truncated toward zero. For checked conversions, there are the TryFrom and TryInto traits.

$ cargo run -q
17.5
2 2.5
44
4294967295
3

Integer overflow

When the result of an integer operation does not fit into its type, an overflow occurs. In debug builds, which cargo run creates by default, overflow causes a panic; in release builds, the value silently wraps around. The integer types have methods that make the intended behaviour explicit.

main.rs
fn main() {
    let x: u8 = 250;

    println!("{:?}", x.checked_add(10));
    println!("{:?}", x.checked_add(5));
    println!("{}", x.wrapping_add(10));
    println!("{}", x.saturating_add(10));
    println!("{:?}", x.overflowing_add(10));

    let y: u8 = "10".parse().unwrap();
    println!("{}", x + y);
}

The maximum value of a u8 is 255. We add 10 to 250 in several ways.

println!("{:?}", x.checked_add(10));
println!("{:?}", x.checked_add(5));

The checked_add method returns None on overflow and Some with the result otherwise.

println!("{}", x.wrapping_add(10));
println!("{}", x.saturating_add(10));
println!("{:?}", x.overflowing_add(10));

The wrapping_add method wraps around past the maximum, saturating_add stops at the maximum, and overflowing_add returns the wrapped value together with a flag that tells whether an overflow occurred.

let y: u8 = "10".parse().unwrap();
println!("{}", x + y);

The plain + operator panics in a debug build. The value of y is parsed at runtime; with a constant, the compiler would report the overflow already at compile time.

$ cargo run -q
None
Some(255)
4
255
(4, true)

thread 'main' (25948) panicked at src/main.rs:11:20:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Comparison operators

Comparison operators compare two values and return a bool.

main.rs
fn main() {
    let a = 10;
    let b = 20;

    println!("{a} == {b}: {}", a == b);
    println!("{a} != {b}: {}", a != b);
    println!("{a} > {b}: {}", a > b);
    println!("{a} < {b}: {}", a < b);
    println!("{a} >= {b}: {}", a >= b);
    println!("{a} <= {b}: {}", a <= b);

    println!("{}", "apple" < "banana");
    println!("{}", (1, 5) < (2, 0));
    println!("{:?}", a.cmp(&b));
}

The program compares two integers with all six comparison operators.

println!("{}", "apple" < "banana");
println!("{}", (1, 5) < (2, 0));

Strings are compared lexicographically, by their bytes. Tuples, arrays, and other compound values are compared element by element; the first pair of different elements decides.

println!("{:?}", a.cmp(&b));

The cmp method returns an Ordering: Less, Equal, or Greater. It is used in sorting functions and in match expressions.

$ cargo run -q
10 == 20: false
10 != 20: true
10 > 20: false
10 < 20: true
10 >= 20: false
10 <= 20: true
true
true
Less

Logical operators

The logical operators && (AND), || (OR), and ! (NOT) combine boolean values.

main.rs
fn is_even(n: i32) -> bool {
    println!("  checking {n}");
    n % 2 == 0
}

fn main() {
    let a = true;
    let b = false;

    println!("a && b: {}", a && b);
    println!("a || b: {}", a || b);
    println!("!a: {}", !a);

    println!("false && is_even(4):");
    println!("{}", false && is_even(4));

    println!("true && is_even(4):");
    println!("{}", true && is_even(4));
}

The is_even function prints a line each time it is called, so we can see when it is evaluated.

println!("{}", false && is_even(4));

The && and || operators are short-circuiting: the right operand is evaluated only when the left one does not decide the result. Since the left operand is false, the whole expression is false and is_even is not called.

println!("{}", true && is_even(4));

Here the left operand is true, so the right operand must be evaluated; the output shows the "checking 4" line.

$ cargo run -q
a && b: false
a || b: true
!a: false
false && is_even(4):
false
true && is_even(4):
  checking 4
true

Bitwise operators

Bitwise operators work with the individual bits of integers: & (AND), | (OR), ^ (XOR), << (left shift), and >> (right shift).

main.rs
fn main() {
    let a: u8 = 0b1100;
    let b: u8 = 0b1010;

    println!("{:04b} & {:04b} = {:04b}", a, b, a & b);
    println!("{:04b} | {:04b} = {:04b}", a, b, a | b);
    println!("{:04b} ^ {:04b} = {:04b}", a, b, a ^ b);
    println!("{:04b} << 2 = {:06b}", a, a << 2);
    println!("{:04b} >> 2 = {:04b}", a, a >> 2);

    let flags = 0b0101_u8;
    let has_third = flags & 0b0100 != 0;
    println!("third bit set: {has_third}");
}

We apply the bitwise operators to two four-bit patterns and print the results in binary.

println!("{:04b} << 2 = {:06b}", a, a << 2);

Shifting left by two places multiplies the value by four; shifting right by two divides it by four.

let has_third = flags & 0b0100 != 0;

A common use of & is testing a bit with a mask. Unlike in C, the bitwise operators have a higher precedence than comparisons in Rust, so no parentheses are needed.

$ cargo run -q
1100 & 1010 = 1000
1100 | 1010 = 1110
1100 ^ 1010 = 0110
1100 << 2 = 110000
1100 >> 2 = 0011
third bit set: true

Compound assignment operators

Compound assignment operators combine an operation with an assignment; a += 5 is a shorter form of a = a + 5. The variable must be mutable.

main.rs
fn main() {
    let mut a = 10;

    a += 5;
    println!("a += 5  -> {a}");
    a -= 3;
    println!("a -= 3  -> {a}");
    a *= 4;
    println!("a *= 4  -> {a}");
    a /= 6;
    println!("a /= 6  -> {a}");
    a %= 5;
    println!("a %= 5  -> {a}");
    a <<= 3;
    println!("a <<= 3 -> {a}");

    let mut s = String::from("sky");
    s += " is blue";
    println!("{s}");
}

The program changes a mutable integer with several compound assignment operators.

a <<= 3;

The bitwise operators have compound forms too: &=, |=, ^=, <<=, and >>=.

let mut s = String::from("sky");
s += " is blue";

The += operator also appends a string slice to a String.

$ cargo run -q
a += 5  -> 15
a -= 3  -> 12
a *= 4  -> 48
a /= 6  -> 8
a %= 5  -> 3
a <<= 3 -> 24
sky is blue

Range operators

The .. and ..= operators create ranges. A range is a value; it is used in loops, for slicing, and in patterns.

main.rs
fn main() {
    let r1 = 1..5;
    let r2 = 1..=5;

    println!("{:?}", r1.clone().collect::<Vec<_>>());
    println!("{:?}", r2.clone().collect::<Vec<_>>());
    println!("{} {}", r1.contains(&5), r2.contains(&5));

    let word = "falcon";
    println!("{} {} {}", &word[..3], &word[3..], &word[1..=2]);
}

We create an exclusive and an inclusive range and use ranges to slice a string.

let r1 = 1..5;
let r2 = 1..=5;

The range 1..5 excludes the upper bound, 1..=5 includes it.

println!("{} {} {}", &word[..3], &word[3..], &word[1..=2]);

One of the bounds can be omitted. The indexes of string slices are byte positions.

$ cargo run -q
[1, 2, 3, 4]
[1, 2, 3, 4, 5]
false true
fal con al

Operator precedence

The operator precedence determines which operators are evaluated first. Unary operators bind tighter than binary ones, multiplication and division tighter than addition, and comparisons tighter than the logical operators.

main.rs
fn main() {
    println!("{}", 3 + 5 * 5);
    println!("{}", (3 + 5) * 5);
    println!("{}", 2 * 3 % 4);
    println!("{}", -2_i32.pow(2));
    println!("{}", (-2_i32).pow(2));

    let x = 3;
    println!("{}", x + 2 == 5 && x > 1);
}

The program evaluates several expressions.

println!("{}", 3 + 5 * 5);
println!("{}", (3 + 5) * 5);

Multiplication has a higher precedence than addition. Parentheses change the order of evaluation.

println!("{}", 2 * 3 % 4);

The * and % operators have the same precedence and are left-associative; the expression is evaluated as (2 * 3) % 4.

println!("{}", -2_i32.pow(2));
println!("{}", (-2_i32).pow(2));

Method calls bind tighter than the unary minus. The first expression is -(2.pow(2)), which is -4.

$ cargo run -q
28
40
2
-4
4
true

Block expressions

A block in curly braces is an expression; its value is the value of the last expression in the block, written without a semicolon. The same holds for if and match.

main.rs
fn main() {
    let x = 10;
    let y = 20;

    let min = if x < y { x } else { y };
    println!("min: {min}");

    let area = {
        let width = 4;
        let height = 3;
        width * height
    };
    println!("area: {area}");

    let parity = match x % 2 {
        0 => "even",
        _ => "odd",
    };
    println!("{x} is {parity}");
}

The program assigns the values of an if expression, a block, and a match expression to variables.

let min = if x < y { x } else { y };

The if expression takes the place of the ternary operator of other languages.

let area = {
    let width = 4;
    let height = 3;
    width * height
};

The block computes a value with the help of temporary variables. The width and height variables are not visible outside of the block.

let parity = match x % 2 {
    0 => "even",
    _ => "odd",
};

The match expression returns the value of the matching arm. See Rust match expression for more.

$ cargo run -q
min: 10
area: 12
10 is even

Source

The Rust Reference: Operator expressions

The Rust Reference: Expression precedence

In this article we have worked with operators and expressions in Rust.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.