Skip to main content
Rust beginner Lesson 5 of 30

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.

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

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.