Operators in Rust
Learn arithmetic, bitwise, comparison, and logical operators in Rust — with no implicit type coercion.
Arithmetic Operators
Rust’s arithmetic operators work as you would expect, with one important rule: operands must be the same type. There is no implicit conversion, so you cannot accidentally add an i32 to a f64 — the compiler will tell you to make the conversion explicit. This eliminates a whole class of subtle numeric bugs common in dynamically typed languages.
fn main() {
let a = 20_i32;
let b = 6_i32;
println!("{}", a + b); // 26 addition
println!("{}", a - b); // 14 subtraction
println!("{}", a * b); // 120 multiplication
println!("{}", a / b); // 3 integer division truncates toward zero
println!("{}", a % b); // 2 remainder (not modulo — sign follows dividend)
}
Integer division always truncates toward zero — it does not floor-divide like Python:
fn main() {
println!("{}", 7 / 2); // 3 — truncates toward zero
println!("{}", -7 / 2); // -3 — also truncates toward zero, not -4
println!("{}", 7 % 2); // 1
println!("{}", -7 % 2); // -1 — sign follows the dividend
}
Floating-point arithmetic follows IEEE 754, including special values for infinity and NaN:
fn main() {
let x = 7.0_f64;
let y = 2.0_f64;
println!("{}", x / y); // 3.5 — true division, not truncation
println!("{}", x % y); // 1.0
println!("{}", f64::INFINITY); // inf
println!("{}", f64::NAN); // NaN
println!("{}", f64::NAN == f64::NAN); // false — NaN is never equal to itself (IEEE 754)
}
No Implicit Coercion
One of Rust’s firmest rules is that numeric types never convert automatically. This prevents the subtle bugs that arise in languages where, say, multiplying an integer by a float silently produces a float, or where a 64-bit integer is quietly narrowed to 32 bits. Every type change must be visible at the point where it happens.
fn main() {
let x: i32 = 10;
let y: i64 = 20;
// let z = x + y; // ERROR: mismatched types — cannot add i32 to i64
// You must cast explicitly, making the widening conversion visible
let z = x as i64 + y;
println!("{}", z); // 30
}
This prevents a whole class of subtle bugs common in C and JavaScript where mixed-type arithmetic silently produces surprising results.
Compound Assignment Operators
Compound assignment operators combine an arithmetic or bitwise operation with assignment. They work on mutable variables and are equivalent to writing n = n + 3 but more concise. Note that Rust has no ++ or -- operators — use += 1 and -= 1 instead.
fn main() {
let mut n = 10;
n += 3; println!("{}", n); // 13
n -= 2; println!("{}", n); // 11
n *= 2; println!("{}", n); // 22
n /= 4; println!("{}", n); // 5
n %= 3; println!("{}", n); // 2
// No ++ or -- in Rust — use += 1 and -= 1
n += 1; println!("{}", n); // 3
}
Comparison Operators
Comparison operators produce a bool result. All six standard comparison operators are available. As with arithmetic, Rust will not compare values of different types without an explicit cast — you cannot accidentally compare an integer with a float and get a meaningless result.
fn main() {
let a = 5;
let b = 10;
println!("{}", a == b); // false equal
println!("{}", a != b); // true not equal
println!("{}", a < b); // true less than
println!("{}", a > b); // false greater than
println!("{}", a <= b); // true less than or equal
println!("{}", a >= b); // false greater than or equal
}
Comparing values of different types is a compile error, which prevents the category of bugs where 5 == 5.0 behaves unexpectedly:
fn main() {
let i: i32 = 5;
let f: f64 = 5.0;
// println!("{}", i == f); // ERROR: can't compare i32 with f64 directly
println!("{}", i as f64 == f); // true — you must make the cast explicit
}
Logical Operators
Logical operators work on bool values and use short-circuit evaluation: && stops as soon as the left side is false, and || stops as soon as the left side is true. This matters when the right side has side effects or is expensive to compute.
fn main() {
let t = true;
let f = false;
println!("{}", t && f); // false logical AND (short-circuits)
println!("{}", t || f); // true logical OR (short-circuits)
println!("{}", !t); // false logical NOT
}
Short-circuit evaluation means the right side is only evaluated when necessary:
fn expensive() -> bool {
println!("evaluated!"); // this line will not run in the examples below
true
}
fn main() {
// The right side is never reached because false && anything is always false
let _ = false && expensive();
// The right side is never reached because true || anything is always true
let _ = true || expensive();
// No output — expensive() was never called
}
Bitwise Operators
Bitwise operators work directly on the binary representation of integers. They are essential in systems programming for tasks like working with hardware registers, network protocol flags, permission bitmasks, and any scenario where individual bits carry distinct meaning.
fn main() {
let a: u8 = 0b1100_1010; // 202 in decimal
let b: u8 = 0b1010_1100; // 172 in decimal
println!("{:08b}", a & b); // 10001000 AND — bit is 1 only if both are 1
println!("{:08b}", a | b); // 11101110 OR — bit is 1 if either is 1
println!("{:08b}", a ^ b); // 01100110 XOR — bit is 1 if exactly one is 1
println!("{:08b}", !a); // 00110101 NOT — flips every bit
println!("{:08b}", a << 2); // 00101000 left shift — multiply by 2^n
println!("{:08b}", a >> 2); // 00110010 right shift — divide by 2^n
}
A practical example — managing a set of boolean flags packed into a single byte:
fn main() {
let mut flags: u8 = 0;
// Set bit 3 (value 8)
flags |= 1 << 3;
println!("{:08b}", flags); // 00001000
// Test whether bit 3 is set
let is_set = (flags & (1 << 3)) != 0;
println!("{}", is_set); // true
// Clear bit 3
flags &= !(1 << 3);
println!("{:08b}", flags); // 00000000
// Toggle bit 5
flags ^= 1 << 5;
println!("{:08b}", flags); // 00100000
}
Overflow Behaviour
Integer overflow is handled differently in debug and release builds. Debug builds panic on overflow to catch bugs early during development. Release builds wrap silently for performance. When you need predictable behaviour regardless of build mode, use the explicit overflow methods.
fn main() {
let max = u8::MAX; // 255
println!("{:?}", max.checked_add(1)); // None — overflow detected
println!("{}", max.wrapping_add(1)); // 0 — wraps around to 0
println!("{}", max.saturating_add(1)); // 255 — clamps to maximum
println!("{:?}", max.overflowing_add(1)); // (0, true) — value and overflow flag
}
Operator Precedence
Operator precedence determines how expressions are grouped when parentheses are absent. The rules mostly match mathematical convention and other languages, but when in doubt, add parentheses — they cost nothing and make intent clear.
| Precedence | Operators |
|---|---|
| Highest | Method calls, field access, indexing [] |
Unary -, !, *, &, &mut | |
*, /, % | |
+, - | |
<<, >> | |
& | |
^ | |
| | |
==, !=, <, >, <=, >= | |
&& | |
|| | |
.., ..= (ranges) | |
| Lowest | =, +=, -=, … |
fn main() {
let x = 2 + 3 * 4; // 14, not 20 — * binds tighter than +
let y = (2 + 3) * 4; // 20 — parentheses override precedence
let z = true || false && false; // true — && binds tighter than ||
println!("{} {} {}", x, y, z);
}
The Range Operators
.. and ..= create range values used in for loops, slicing, and pattern matching. The exclusive form .. excludes the upper bound; the inclusive form ..= includes it.
fn main() {
// Exclusive range [0, 5) — does not include 5
for i in 0..5 {
print!("{} ", i); // 0 1 2 3 4
}
println!();
// Inclusive range [0, 5] — includes 5
for i in 0..=5 {
print!("{} ", i); // 0 1 2 3 4 5
}
println!();
// Ranges also work for slicing arrays and strings
let arr = [10, 20, 30, 40, 50];
println!("{:?}", &arr[1..4]); // [20, 30, 40] — exclusive
println!("{:?}", &arr[..3]); // [10, 20, 30] — from start
println!("{:?}", &arr[2..]); // [30, 40, 50] — to end
}