Rust · lesson 5 of 30
Beginner
15- 1 Introduction to Rust
- 2 Setting Up Rust
- 3 Variables in Rust
- 4 Data Types in Rust
- 5 Operators in Rust
- 6 Control Flow in Rust
- 7 Functions in Rust
- 8 Ownership in Rust
- 9 Borrowing and References
- 11 Structs in Rust
- 12 Enums in Rust
- 13 Pattern Matching in Rust
- 17 Collections in Rust
- 25 Testing in Rust
- 26 File I/O in Rust
Intermediate
9Advanced
6Operators in Rust
Learn arithmetic, bitwise, comparison, and logical operators in Rust — with no implicit type coercion.
By SkillByExample · Updated
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); // addition
println!("{}", a - b); // subtraction
println!("{}", a * b); // multiplication
println!("{}", a / b); // integer division truncates toward zero
println!("{}", a % b); // remainder (not modulo — sign follows dividend)
}
$ rustc operators_arith.rs && ./operators_arith
26
14
120
3
2
Integer division always truncates toward zero — it does not floor-divide like Python:
fn main() {
println!("{}", 7 / 2); // truncates toward zero
println!("{}", -7 / 2); // also truncates toward zero, not -4
println!("{}", 7 % 2);
println!("{}", -7 % 2); // sign follows the dividend
}
$ rustc operators_intdiv.rs && ./operators_intdiv
3
-3
1
-1
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); // true division, not truncation
println!("{}", x % y);
println!("{}", f64::INFINITY);
println!("{}", f64::NAN);
println!("{}", f64::NAN == f64::NAN); // NaN is never equal to itself (IEEE 754)
}
$ rustc operators_float.rs && ./operators_float
warning: incorrect NaN comparison, NaN cannot be directly compared to itself
--> operators_float.rs:9:20
|
9 | println!("{}", f64::NAN == f64::NAN);
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(invalid_nan_comparisons)]` on by default
help: use `f32::is_nan()` or `f64::is_nan()` instead
3.5
1
inf
NaN
false
rustc itself now warns about this exact comparison — invalid_nan_comparisons is a
built-in lint precisely because NAN == NAN is a common mistake. The program still
compiles and runs (false is the real, correct answer per IEEE 754), but idiomatic code
should call .is_nan() instead, as the lint suggests. Note also that {} on an f64
prints 1, not 1.0 — Rust’s Display for floats omits a trailing .0 on whole
numbers; use {:?} (Debug) if you want 1.0 to always show the decimal point.
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);
}
$ rustc operators_coerce.rs && ./operators_coerce
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);
n -= 2; println!("{}", n);
n *= 2; println!("{}", n);
n /= 4; println!("{}", n);
n %= 3; println!("{}", n);
// No ++ or -- in Rust — use += 1 and -= 1
n += 1; println!("{}", n);
}
$ rustc operators_compound.rs && ./operators_compound
13
11
22
5
2
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); // equal
println!("{}", a != b); // not equal
println!("{}", a < b); // less than
println!("{}", a > b); // greater than
println!("{}", a <= b); // less than or equal
println!("{}", a >= b); // greater than or equal
}
$ rustc operators_cmp.rs && ./operators_cmp
false
true
true
false
true
false
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); // you must make the cast explicit
}
$ rustc operators_cmpcast.rs && ./operators_cmpcast
true
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); // logical AND (short-circuits)
println!("{}", t || f); // logical OR (short-circuits)
println!("{}", !t); // logical NOT
}
$ rustc operators_logic.rs && ./operators_logic
false
true
false
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();
}
$ rustc operators_shortcircuit.rs && ./operators_shortcircuit
No output — expensive()’s println!("evaluated!") never runs, because false && ...
short-circuits on the false and true || ... short-circuits on the true.
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); // AND — bit is 1 only if both are 1
println!("{:08b}", a | b); // OR — bit is 1 if either is 1
println!("{:08b}", a ^ b); // XOR — bit is 1 if exactly one is 1
println!("{:08b}", !a); // NOT — flips every bit
println!("{:08b}", a << 2); // left shift — multiply by 2^n
println!("{:08b}", a >> 2); // right shift — divide by 2^n
}
$ rustc operators_bitwise.rs && ./operators_bitwise
10001000
11101110
01100110
00110101
00101000
00110010
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);
// Test whether bit 3 is set
let is_set = (flags & (1 << 3)) != 0;
println!("{}", is_set);
// Clear bit 3
flags &= !(1 << 3);
println!("{:08b}", flags);
// Toggle bit 5
flags ^= 1 << 5;
println!("{:08b}", flags);
}
$ rustc operators_flags.rs && ./operators_flags
00001000
true
00000000
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)); // overflow detected
println!("{}", max.wrapping_add(1)); // wraps around to 0
println!("{}", max.saturating_add(1)); // clamps to maximum
println!("{:?}", max.overflowing_add(1)); // value and overflow flag
}
$ rustc operators_overflow.rs && ./operators_overflow
None
0
255
(0, true)
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; // not 20 — * binds tighter than +
let y = (2 + 3) * 4; // parentheses override precedence
let z = true || false && false; // && binds tighter than ||
println!("{} {} {}", x, y, z);
}
$ rustc operators_prec.rs && ./operators_prec
14 20 true
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);
}
println!();
// Inclusive range [0, 5] — includes 5
for i in 0..=5 {
print!("{} ", i);
}
println!();
// Ranges also work for slicing arrays and strings
let arr = [10, 20, 30, 40, 50];
println!("{:?}", &arr[1..4]); // exclusive
println!("{:?}", &arr[..3]); // from start
println!("{:?}", &arr[2..]); // to end
}
$ rustc operators_range.rs && ./operators_range
0 1 2 3 4
0 1 2 3 4 5
[20, 30, 40]
[10, 20, 30]
[30, 40, 50]
Practice
-
Divide by zero with two integer literals:
println!("{}", 1 / 0);. Does this panic at runtime like you’d expect, or does something else happen?Solution
Neither, quite — it’s a compile error:
error: this operation will panic at runtime/attempt to divide \1_i32` by zero, under theunconditional_paniclint. The compiler can const-evaluate both literals and prove the panic is unconditional, so it refuses to build at all. Make the divisor come from something the compiler can't see at compile time (e.g. a value read fromstd::env::args()) and *then* it becomes a genuine runtime panic:thread ‘main’ panicked at …: attempt to divide by zero`. -
Shift a
u8left by 8:let x: u8 = 1; let y = x << 8;. Predict what happens, then run it.Solution
Compile error:
error: this arithmetic operation will overflow/attempt to shift left by \8_i32`, which would overflow, under thearithmetic_overflowlint — au8only has 8 bits (valid shift amounts are 0-7), and the compiler can prove<< 8` overflows since both operands are compile-time constants. Same shape as the divide-by-zero case above: constant-foldable panics are caught before the program ever runs. -
Try to chain a comparison like Python does:
a < b < c. What does Rust say, and what is the idiomatic fix?Solution
error: comparison operators cannot be chained— Rust has no notion of chained comparisons;<is strictly a binary operator returningbool, andbool < cisn’t meaningful. The compiler’s own suggested fix isa < b && b < c, which is also the idiomatic way to write it. -
-7 % 3in Rust follows the dividend’s sign. Predict the result, then compute Python’s-7 % 3(which follows the divisor’s sign) for comparison — are they the same number?Solution
Rust:
-7 % 3is-1(result takes the sign of-7, the dividend). Python’s-7 % 3is2(result takes the sign of3, the divisor) — a different, non-negative answer for the identical expression. Porting modulo-based logic (like wrapping an index) from Python to Rust without accounting for this is a classic source of off-by-a-sign bugs; Rust’srem_euclidmethod gives you Python’s always-non-negative behaviour explicitly when you want it. -
!aona: u8 = 0b1100_1010flips every bit (bitwise NOT). What does!5i32print, and why is it not simply-6’s absolute-value-looking bit pattern you might expect from a quick mental flip?Solution
!5i32is-6. Signed integers use two’s complement, where!nis always-n - 1(here-5 - 1 = -6) — flipping every bit of a two’s-complement number is mathematically equivalent to negating and subtracting one. This is genuinely-6, not a display quirk:!5i32 == -6evaluates totrue.