Skip to main content
Rust · lesson 5 of 30
Rust beginner Lesson 5 of 30

Operators 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.

PrecedenceOperators
HighestMethod 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

  1. 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 the unconditional_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`.

  2. Shift a u8 left 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 the arithmetic_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.

  3. 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 returning bool, and bool < c isn’t meaningful. The compiler’s own suggested fix is a < b && b < c, which is also the idiomatic way to write it.

  4. -7 % 3 in 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 % 3 is -1 (result takes the sign of -7, the dividend). Python’s -7 % 3 is 2 (result takes the sign of 3, 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’s rem_euclid method gives you Python’s always-non-negative behaviour explicitly when you want it.

  5. !a on a: u8 = 0b1100_1010 flips every bit (bitwise NOT). What does !5i32 print, and why is it not simply -6’s absolute-value-looking bit pattern you might expect from a quick mental flip?

    Solution

    !5i32 is -6. Signed integers use two’s complement, where !n is 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 == -6 evaluates to true.

Frequently Asked Questions

Does Rust have implicit type coercion between numeric types?
No. Rust never implicitly converts between numeric types. You must use explicit casts with `as` or conversion traits.
Does Rust have a ++ increment operator?
No. Use += 1 instead.
What does the % operator return for negative numbers in Rust?
Rust's % is the remainder operator, not modulo. The result takes the sign of the dividend: -7 % 3 == -1.