RustOperators

Operators in Rust

Rust provides a rich set of operators for arithmetic, comparison, logic, and more. Unlike some languages, Rust is strict about types — operators generally require both operands to be the same type, and there is no implicit coercion between numeric types.

Arithmetic Operators

Rust supports the five standard arithmetic operators. The most important thing to understand is that integer division truncates toward zero — it does not round, and it does not produce a float.

RUST
fn main() {
    let a = 10;
    let b = 3;

    println!("{}", a + b);  // 13 — addition
    println!("{}", a - b);  // 7  — subtraction
    println!("{}", a * b);  // 30 — multiplication
    println!("{}", a / b);  // 3  — integer division (truncates, NOT 3.333...)
    println!("{}", a % b);  // 1  — remainder (modulo)

    // Float arithmetic behaves as expected
    let x: f64 = 10.0;
    let y: f64 = 3.0;
    println!("{:.4}", x / y); // 3.3333
}
Warning
Integer division in Rust always truncates toward zero. `7 / 2` is `3`, not `3.5`. Dividing an integer by zero causes a runtime panic. For floats, dividing by zero produces `inf` or `NaN` without panicking.
No ++ or -- in Rust

Rust deliberately omits the increment (++) and decrement (--) operators found in C, C++, Java, and JavaScript. The Rust designers considered them a source of subtle bugs — the difference between pre-increment and post-increment is easy to misread. Rust requires you to be explicit with += 1 and -= 1.

RUST
fn main() {
    let mut count = 0;

    // The Rust way — explicit and unambiguous
    count += 1; // count is now 1
    count += 1; // count is now 2
    count -= 1; // count is now 1

    // count++; // ERROR: Rust has no ++ operator
    // count--; // ERROR: Rust has no -- operator
}
Comparison Operators

Comparison operators always produce a bool value. Both operands must be the same type — Rust will not silently coerce an i32 to f64 for a comparison.

RUST
fn main() {
    let a = 5;
    let b = 10;

    println!("{}", a == b);  // false — equal to
    println!("{}", a != b);  // true  — not equal to
    println!("{}", a <  b);  // true  — less than
    println!("{}", a >  b);  // false — greater than
    println!("{}", a <= b);  // true  — less than or equal to
    println!("{}", a >= b);  // false — greater than or equal to
}
Logical Operators

Logical operators work on bool values only. Rust does not treat integers, strings, or other values as truthy or falsy — only an actual bool is accepted.

RUST
fn main() {
    let t = true;
    let f = false;

    println!("{}", t && f); // false — logical AND
    println!("{}", t || f); // true  — logical OR
    println!("{}", !t);     // false — logical NOT

    // if 1 { }  // ERROR: Rust requires a bool, not an integer
}
Short-Circuit Evaluation

Both && and || short-circuit: they stop evaluating as soon as the result is determined. This is important both for performance and for avoiding panics.

RUST
fn main() {
    let items: Vec<i32> = vec![1, 2, 3];

    // Safe: if items is empty, items[0] is never evaluated
    if !items.is_empty() && items[0] == 1 {
        println!("first item is 1");
    }

    // || stops at the first true — expensive_fn() is never called
    if true || expensive_fn() {
        println!("short-circuited");
    }
}

fn expensive_fn() -> bool { true }
Tip
Use short-circuit evaluation to guard against panics: check `is_empty()`, `is_some()`, or bounds on the left side of `&&` before accessing on the right.
Bitwise Operators

Bitwise operators work directly on the binary representation of integer values. They are commonly used in systems programming, flag manipulation, and performance-critical code.

RUST
fn main() {
    let a: u8 = 0b1010_1010; // 170
    let b: u8 = 0b1100_1100; // 204

    println!("{:08b}", a & b);  // 10001000 — AND:  bits set in BOTH
    println!("{:08b}", a | b);  // 11101110 — OR:   bits set in EITHER
    println!("{:08b}", a ^ b);  // 01100110 — XOR:  bits in ONE but not both
    println!("{:08b}", !a);     // 01010101 — NOT:  flip every bit

    println!("{}", 1u32 << 3);  // 8  — left shift  (same as * 2^3)
    println!("{}", 16u32 >> 2); // 4  — right shift (same as / 2^2)
}
Assignment Operators

Compound assignment operators combine an operation with assignment in one step. The variable must be declared mut.

RUST
fn main() {
    let mut n = 10;

    n += 5;  // 15
    n -= 3;  // 12
    n *= 2;  // 24
    n /= 4;  //  6
    n %= 4;  //  2
    println!("n = {}", n); // n = 2

    // Bitwise compound assignment
    let mut flags: u8 = 0b0000_0000;
    flags |=  0b0000_0001; // set bit 0
    flags |=  0b0000_0100; // set bit 2
    flags &= !0b0000_0001; // clear bit 0
    println!("{:08b}", flags); // 00000100

    // Shift assign
    let mut val: u32 = 1;
    val <<= 4;  // val is now 16
    val >>= 1;  // val is now 8
}
Range Operators

Rust has two range operators used heavily with for loops and match arms. Ranges are lazy iterators — they produce values on demand.

RUST
fn main() {
    // .. exclusive range: includes start, excludes end
    for i in 0..5 {
        print!("{} ", i); // 0 1 2 3 4
    }
    println!();

    // ..= inclusive range: includes both start AND end
    for i in 0..=5 {
        print!("{} ", i); // 0 1 2 3 4 5
    }
    println!();

    // Ranges in match arms
    let score = 85;
    let grade = match score {
        90..=100 => "A",
        80..=89  => "B",
        70..=79  => "C",
        60..=69  => "D",
        _        => "F",
    };
    println!("Grade: {}", grade); // Grade: B

    // Collect a range into a Vec
    let digits: Vec<i32> = (1..=9).collect();
    println!("{:?}", digits); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
}
The ? Operator (Error Propagation)

The ? operator is Rust's ergonomic shorthand for propagating errors up the call stack. When placed after an expression returning Result<T, E> or Option<T>, it either unwraps the success value or immediately returns the error from the enclosing function.

RUST
use std::fs;
use std::io;

// Without ? — verbose manual matching
fn read_file_verbose(path: &str) -> Result<String, io::Error> {
    let content = match fs::read_to_string(path) {
        Ok(val) => val,
        Err(e)  => return Err(e),
    };
    Ok(content.trim().to_string())
}

// With ? — concise and idiomatic Rust
fn read_file(path: &str) -> Result<String, io::Error> {
    let content = fs::read_to_string(path)?; // returns Err early if it fails
    Ok(content.trim().to_string())
}

// Chain multiple ? calls cleanly
fn first_line(path: &str) -> Result<String, io::Error> {
    let content = fs::read_to_string(path)?;
    let line = content.lines().next().unwrap_or("").to_string();
    Ok(line)
}

// main can use ? when it returns Result
fn main() -> Result<(), io::Error> {
    let line = first_line("example.txt")?;
    println!("{}", line);
    Ok(())
}
Note
The `?` operator can only be used inside functions that return `Result` or `Option`. It also performs an implicit type conversion via the `From` trait, so functions can propagate errors of different types as long as the conversion is defined.
The @ Binding Operator in Patterns

The @ operator lets you bind a matched value to a variable name while also testing it against a pattern at the same time. Without @, you can either test a range or capture the value — but not both simultaneously.

RUST
fn main() {
    let number = 7;

    match number {
        // n @ 1..=12 means: match values 1–12 AND bind that value to n
        n @ 1..=12  => println!("{} is a small number (1–12)", n),
        n @ 13..=19 => println!("{} is a teen", n),
        n           => println!("{} is 20 or above", n),
    }

    // Useful with enum variants too
    #[derive(Debug)]
    enum Message { Hello { id: i32 } }

    let msg = Message::Hello { id: 5 };
    match msg {
        Message::Hello { id: id_var @ 3..=7 } => {
            println!("Found id in range: {}", id_var);
        }
        Message::Hello { id } => {
            println!("Other id: {}", id);
        }
    }
}
Operator Overloading via Traits

Rust allows you to define what operators mean for your own types by implementing traits from the std::ops module. This is how String supports + for concatenation, and how numeric wrapper types work.

RUST
use std::ops::{Add, Sub, Neg};

#[derive(Debug, Clone, Copy, PartialEq)]
struct Vec2 {
    x: f64,
    y: f64,
}

impl Add for Vec2 {
    type Output = Vec2;
    fn add(self, other: Vec2) -> Vec2 {
        Vec2 { x: self.x + other.x, y: self.y + other.y }
    }
}

impl Sub for Vec2 {
    type Output = Vec2;
    fn sub(self, other: Vec2) -> Vec2 {
        Vec2 { x: self.x - other.x, y: self.y - other.y }
    }
}

impl Neg for Vec2 {
    type Output = Vec2;
    fn neg(self) -> Vec2 {
        Vec2 { x: -self.x, y: -self.y }
    }
}

fn main() {
    let a = Vec2 { x: 1.0, y: 2.0 };
    let b = Vec2 { x: 3.0, y: 4.0 };

    println!("{:?}", a + b); // Vec2 { x: 4.0, y: 6.0 }
    println!("{:?}", b - a); // Vec2 { x: 2.0, y: 2.0 }
    println!("{:?}", -a);    // Vec2 { x: -1.0, y: -2.0 }
}
Tip
Common overloadable traits in `std::ops`: `Add`, `Sub`, `Mul`, `Div`, `Rem`, `Neg`, `Not`, `BitAnd`, `BitOr`, `BitXor`, `Shl`, `Shr`, `Index`, `IndexMut`.
Operator Precedence

When multiple operators appear in one expression, precedence controls which operations bind more tightly. The table below goes from highest to lowest priority.

Level

Operators

Notes

Highest

Method calls, field access (.x), indexing ([])

Evaluated first

Unary

!, - (negate)

Right-to-left

Cast

as

Type casting

Multiplicative

*, /, %

Left-to-right

Additive

+, -

Left-to-right

Shift

<<, >>

Left-to-right

Bitwise AND

&

Left-to-right

Bitwise XOR

^

Left-to-right

Bitwise OR

|

Left-to-right

Comparison

==, !=, <, >, <=, >=

Non-chainable

Logical AND

&&

Short-circuits

Logical OR

||

Short-circuits

Range

.., ..=

Assignment

=, +=, -=, *=, /=, etc.

Right-to-left

Lowest

return, break, closures |..| expr

Evaluated last

Note
When in doubt, use parentheses. `(a + b) * c` is always clearer than relying on precedence rules alone, and the Rust compiler does not penalize extra parentheses.
All Operators at a Glance

Operator

Category

Example

Result

+

Arithmetic

3 + 4

7

-

Arithmetic

10 - 3

7

Arithmetic

4 * 5

20

/

Arithmetic

10 / 3

3 (truncated)

%

Remainder

10 % 3

1

==

Comparison

3 == 3

true

!=

Comparison

3 != 4

true

<

Comparison

2 < 5

true

>

Comparison

5 > 2

true

<=

Comparison

3 <= 3

true

>=

Comparison

5 >= 4

true

&&

Logical

true && false

false

||

Logical

true || false

true

!

Logical

!true

false

&

Bitwise

0b1010 & 0b1100

0b1000

|

Bitwise

0b1010 | 0b0101

0b1111

^

Bitwise

0b1010 ^ 0b1100

0b0110

<<

Bitwise

1 << 3

8

>>

Bitwise

16 >> 2

4

+=

Assignment

x += 1

x = x + 1

-=

Assignment

x -= 2

x = x - 2

..

Range

0..5

0, 1, 2, 3, 4

..=

Range

0..=5

0, 1, 2, 3, 4, 5

?

Error prop

expr?

unwrap or return Err

@

Binding

n @ 1..=9

bind and test

as

Cast

3u8 as i32

type conversion

Success
You now know all of Rust's core operators. Key takeaways: integer division truncates toward zero, there is no ++ or -- operator, both operands must be the same type (no implicit coercion), the ? operator makes error propagation clean, and you can overload operators for custom types via std::ops traits.