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.
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
}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.
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.
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.
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.
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 }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.
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.
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.
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.
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(())
}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.
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.
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 }
}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 |
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 |