Operators #

Operators in Rust mostly feel familiar if you already know C, Java, or Go — +, -, ==, && are all there. But there are a few important differences worth noting: Rust has no increment ++ and decrement -- operators, no implicit type conversion (every cast must be explicit with as), and the .. and ..= range operators are core constructs used everywhere from loops to slices. On top of that, Rust lets you redefine operator behavior for custom types via traits — + for your own struct can mean whatever you decide. This article covers every operator category thoroughly, along with the traps that often surprise new developers.

Arithmetic Operators #

Arithmetic operators work on numeric types: integers and floats. One critical rule distinguishes Rust from many other languages: there is no implicit type conversion. You can’t add an i32 to an f64 directly — one of them must be cast explicitly first.

fn main() {
    let a: i32 = 20;
    let b: i32 = 7;

    println!("{} + {} = {}", a, b, a + b);  // 27
    println!("{} - {} = {}", a, b, a - b);  // 13
    println!("{} * {} = {}", a, b, a * b);  // 140
    println!("{} / {} = {}", a, b, a / b);  // 2  ← integer division: truncates down
    println!("{} % {} = {}", a, b, a % b);  // 6  ← remainder

    // ANTI-PATTERN: mixing types without casting
    let x: i32 = 10;
    let y: f64 = 3.5;
    // let hasil = x + y; // error[E0308]: mismatched types

    // CORRECT: explicit cast
    let hasil = x as f64 + y;  // 13.5
    println!("Cast: {}", hasil);
}

Integer vs Float Division #

Integer division in Rust always truncates (drops the decimal part) — it doesn’t round to the nearest number:

fn main() {
    // Integer division: always truncates toward zero
    println!("7 / 2 = {}", 7 / 2);      //  3, not 3.5 or 4
    println!("-7 / 2 = {}", -7 / 2);    // -3, not -4 (truncates toward zero)
    println!("7 / -2 = {}", 7 / -2);    // -3, not -4

    // Float division: precise decimal results
    println!("7.0 / 2.0 = {}", 7.0_f64 / 2.0);  // 3.5

    // Division by zero
    // 5 / 0     → panics at runtime: attempt to divide by zero
    // 5.0 / 0.0 → f64::INFINITY (no panic)
    println!("5.0 / 0.0 = {}", 5.0_f64 / 0.0);  // inf

    // ANTI-PATTERN: assuming integer division produces a float
    let persen = 1 / 3;               // 0, not 0.333...
    // CORRECT: cast first if you need a float result
    let persen_f = 1_f64 / 3.0;      // 0.333...
    println!("1/3 integer: {}, float: {:.3}", persen, persen_f);
}

Unary Minus Operator #

Rust supports unary negation with - for signed types:

fn main() {
    let positif: i32 = 42;
    let negatif = -positif;  // -42

    // ANTI-PATTERN: negation on an unsigned type
    let u: u32 = 5;
    // let neg_u = -u; // error: cannot apply unary operator `-` to type `u32`
    // u32 can't be negative — use i32 if the value can be negative

    println!("{} {}", positif, negatif);
}

Comparison Operators #

Comparison operators always produce a bool. Like arithmetic, Rust doesn’t allow comparing different types directly.

OperatorMeaningExampleResult
==Equal to5 == 5true
!=Not equal to5 != 6true
>Greater than10 > 8true
<Less than3 < 5true
>=Greater than or equal10 >= 10true
<=Less than or equal3 <= 5true
fn main() {
    let x = 10;
    let y = 20;

    println!("x == y : {}", x == y);   // false
    println!("x != y : {}", x != y);   // true
    println!("x >  y : {}", x > y);    // false
    println!("x <  y : {}", x < y);    // true
    println!("x >= y : {}", x >= y);   // false
    println!("x <= y : {}", x <= y);   // true

    // String comparison
    let s1 = "apel";
    let s2 = "mangga";
    println!("\"{}\" < \"{}\" : {}", s1, s2, s1 < s2);  // true — lexicographic
}

Comparing Custom Types #

For structs and enums, comparison isn’t automatically available — you need to derive the PartialEq trait (for == and !=) and PartialOrd (for <, >, etc.):

#[derive(Debug, PartialEq, PartialOrd)]
struct Suhu {
    celsius: f64,
}

fn main() {
    let s1 = Suhu { celsius: 36.5 };
    let s2 = Suhu { celsius: 37.2 };

    println!("Equal: {}", s1 == s2);         // false
    println!("s1 < s2: {}", s1 < s2);       // true
    println!("Fever: {}", s2 > Suhu { celsius: 37.0 }); // true
}

Logical Operators #

Logical operators work on bool values and use short-circuit evaluation — the expression on the right side isn’t evaluated if the result can already be determined from the left side.

fn mahal() -> bool {
    println!("(expensive function called)");
    true
}

fn main() {
    // && short-circuit: if the left side is false, the right side isn't evaluated
    let hasil = false && mahal();  // "mahal" is NOT called
    println!("false && mahal() = {}", hasil);  // false

    // || short-circuit: if the left side is true, the right side isn't evaluated
    let hasil = true || mahal();   // "mahal" is NOT called
    println!("true || mahal() = {}", hasil);   // true

    // Both are evaluated when needed
    let hasil = true && mahal();   // "mahal" IS called
    println!("true && mahal() = {}", hasil);   // true

    // ! negation
    println!("!true = {}", !true);    // false
    println!("!false = {}", !false);  // true

    // Combining logical expressions
    let usia = 25;
    let punya_sim = true;
    let boleh_mengemudi = usia >= 17 && punya_sim;
    println!("Can drive: {}", boleh_mengemudi);  // true
}
Rust doesn’t have and, or, not keywords like Python. &&, ||, and ! are the only syntax for boolean logic. The & and | (bitwise) operators can also be used on bool but they are not short-circuit — both sides are always evaluated.

Bitwise Operators #

Bitwise operators manipulate the binary representation of integer values directly, bit by bit. Useful for flag manipulation, masking, encoding, and low-level protocols.

OperatorNameExampleResult
&Bitwise AND0b1100 & 0b10100b1000 (8)
|Bitwise OR0b1100 | 0b10100b1110 (14)
^Bitwise XOR0b1100 ^ 0b10100b0110 (6)
!Bitwise NOT!0b0000_1111u80b1111_0000 (240)
<<Left shift0b0001 << 30b1000 (8)
>>Right shift0b1000 >> 20b0010 (2)
fn main() {
    let a: u8 = 0b1100_1010;  // 202
    let b: u8 = 0b1010_0110;  // 166

    println!("a     = {:08b} ({})", a, a);
    println!("b     = {:08b} ({})", b, b);
    println!("a & b = {:08b} ({})", a & b, a & b);  // AND
    println!("a | b = {:08b} ({})", a | b, a | b);  // OR
    println!("a ^ b = {:08b} ({})", a ^ b, a ^ b);  // XOR
    println!("!a    = {:08b} ({})", !a, !a);         // NOT — flips all bits
    println!("a << 2 = {:08b} ({})", a << 2, a << 2); // left shift 2 positions
    println!("a >> 2 = {:08b} ({})", a >> 2, a >> 2); // right shift 2 positions
}

Practical Use: Bit Flags #

Bitwise operations are most often used to manage a set of flags in a single integer — an efficient technique in embedded systems, protocols, and file formats:

// Flag definitions as bit constants
const FLAG_BACA: u8    = 0b0000_0001;  // bit 0
const FLAG_TULIS: u8   = 0b0000_0010;  // bit 1
const FLAG_EKSEKUSI: u8 = 0b0000_0100; // bit 2

fn main() {
    let mut izin: u8 = 0;

    // Set a flag with OR
    izin |= FLAG_BACA;
    izin |= FLAG_TULIS;
    println!("After setting read+write: {:08b}", izin);  // 00000011

    // Check a flag with AND
    let bisa_baca = (izin & FLAG_BACA) != 0;
    let bisa_eksekusi = (izin & FLAG_EKSEKUSI) != 0;
    println!("Can read: {}", bisa_baca);       // true
    println!("Can execute: {}", bisa_eksekusi); // false

    // Remove a flag with AND NOT
    izin &= !FLAG_TULIS;
    println!("After removing write: {:08b}", izin);  // 00000001

    // Toggle a flag with XOR
    izin ^= FLAG_EKSEKUSI;
    println!("After toggling execute: {:08b}", izin);  // 00000101

    // Left shift = multiplication by a power of 2 (faster than *)
    let nilai = 1u32;
    println!("1 << 10 = {} (= 2^10 = 1024)", nilai << 10);

    // Right shift = division by a power of 2 (faster than /)
    let besar = 1024u32;
    println!("1024 >> 3 = {} (= 1024/8 = 128)", besar >> 3);
}

Assignment Operators #

= is basic assignment. Rust also provides compound assignment operators that combine an operation and assignment in one step, for all arithmetic and bitwise operators.

fn main() {
    let mut x: i32 = 10;

    x += 5;   println!("+=  : {}", x);  // 15
    x -= 3;   println!("-=  : {}", x);  // 12
    x *= 2;   println!("*=  : {}", x);  // 24
    x /= 4;   println!("/=  : {}", x);  // 6
    x %= 4;   println!("%=  : {}", x);  // 2

    let mut flags: u8 = 0b1111_0000;
    flags &= 0b1010_1010;  println!("&=  : {:08b}", flags);  // 10100000
    flags |= 0b0000_1111;  println!("|=  : {:08b}", flags);  // 10101111
    flags ^= 0b1111_1111;  println!("^=  : {:08b}", flags);  // 01010000
    flags <<= 1;           println!("<<= : {:08b}", flags);  // 10100000
    flags >>= 2;           println!(">>= : {:08b}", flags);  // 00101000
}

Rust Has No ++ and -- #

This differs from C, Java, and many other languages. Rust deliberately chose not to provide increment ++ and decrement -- because their semantics are ambiguous (prefix vs postfix) and they’re often a source of subtle bugs.

fn main() {
    let mut i = 0;

    // ANTI-PATTERN: trying to use ++ or --
    // i++;  // error: expected expression
    // i--;  // error: expected expression

    // CORRECT: use += 1 and -= 1
    i += 1;  // increment
    i -= 1;  // decrement
    println!("i = {}", i);  // 0
}

Range Operators #

Ranges are a unique construct in Rust — .. and ..= produce objects of type Range that can be iterated, used as slice indexes, and matched. Not just loop syntax, a range is a value that can be stored and operated on.

fn main() {
    // Exclusive: 0..5 produces 0, 1, 2, 3, 4
    for i in 0..5 {
        print!("{} ", i);
    }
    println!(); // 0 1 2 3 4

    // Inclusive: 0..=5 produces 0, 1, 2, 3, 4, 5
    for i in 0..=5 {
        print!("{} ", i);
    }
    println!(); // 0 1 2 3 4 5

    // Range as a slice index
    let data = [10, 20, 30, 40, 50];
    let tengah = &data[1..4];   // [20, 30, 40]
    let awal = &data[..3];      // [10, 20, 30]
    let akhir = &data[2..];     // [30, 40, 50]
    println!("{:?} {:?} {:?}", tengah, awal, akhir);

    // Range in match
    let skor = 85;
    let nilai = match skor {
        90..=100 => "A",
        80..=89  => "B",
        70..=79  => "C",
        60..=69  => "D",
        _        => "E",
    };
    println!("Score {} = Grade {}", skor, nilai);

    // Range as a value — can be stored in a variable
    let rentang = 1..=10;
    let jumlah: i32 = rentang.sum();
    println!("Sum of 1..=10 = {}", jumlah);  // 55

    // contains() — check whether a value is inside the range
    let valid = (18..=65).contains(&25);
    println!("25 within working-age range: {}", valid);  // true
}

Reference and Dereference Operators #

& creates a reference (borrowing a value without taking ownership), and * performs dereferencing (accessing the value behind a reference).

fn main() {
    let x = 5;
    let r = &x;       // r is a reference to x, of type &i32

    println!("x = {}", x);    // direct access
    println!("r = {}", r);    // auto-deref: println! dereferences automatically
    println!("*r = {}", *r);  // explicit deref — also 5

    // Mutable reference
    let mut y = 10;
    let rm = &mut y;
    *rm += 5;          // must deref to change the referenced value
    println!("y after modification: {}", y);  // 15

    // Deref in comparisons
    let a = 42;
    let ra = &a;
    println!("*ra == a : {}", *ra == a);   // true
    println!("ra == &a : {}", ra == &a);   // true — Rust auto-derefs here
}

Deref Coercion #

Rust performs deref coercion automatically in certain contexts — converting &String to &str, &Vec<T> to &[T], and so on, without an explicit deref:

fn cetak(s: &str) {
    println!("{}", s);
}

fn jumlahkan(data: &[i32]) -> i32 {
    data.iter().sum()
}

fn main() {
    let owned = String::from("halo");
    cetak(&owned);        // &String → &str automatically (deref coercion)

    let v = vec![1, 2, 3, 4, 5];
    let total = jumlahkan(&v);  // &Vec<i32> → &[i32] automatically
    println!("Total: {}", total);  // 15

    // Box<T> is also dereferenced automatically to T
    let boxed = Box::new(42);
    println!("Boxed: {}", *boxed);  // explicit deref
    println!("Boxed: {}", boxed);   // auto-deref also works
}

Casting Operator (as) #

Rust doesn’t perform type conversion implicitly — every conversion must be explicit using the as keyword. This forces you to be aware of the conversion happening and the possible data loss.

fn main() {
    // Integer to integer
    let besar: i64 = 1000;
    let kecil = besar as i32;  // i64 → i32: safe as long as the value fits
    println!("{} as i32 = {}", besar, kecil);

    // ANTI-PATTERN: unnoticed truncation
    let terlalu_besar: i32 = 300;
    let dipotong = terlalu_besar as u8;  // 300 % 256 = 44 — the value changed!
    println!("{} as u8 = {} (TRUNCATED!)", terlalu_besar, dipotong);

    // Float to integer: truncates (drops the decimal)
    let f = 3.99_f64;
    let i = f as i32;  // 3, not 4
    println!("{} as i32 = {}", f, i);

    // Integer to float
    let n: i32 = 42;
    let fp = n as f64;
    println!("{} as f64 = {}", n, fp);

    // char to integer and back
    let c = 'A';
    let kode = c as u32;
    println!("'{}' as u32 = {}", c, kode);  // 65

    let balik = 66u8 as char;
    println!("66u8 as char = '{}'", balik);  // 'B'

    // bool to integer
    println!("true as i32 = {}", true as i32);   // 1
    println!("false as i32 = {}", false as i32); // 0
}

Safe Casting with try_from / try_into #

For conversions that can fail (value outside the target range), use try_from / try_into which return a Result:

use std::convert::TryFrom;

fn main() {
    // as: silently truncates on overflow
    let besar: i32 = 300;
    let dipotong = besar as u8;  // 44 — no error, value lost

    // try_from: returns an error if it doesn't fit
    let aman = u8::try_from(besar);
    println!("{:?}", aman);  // Err(TryFromIntError(()))

    let kecil = u8::try_from(200i32);
    println!("{:?}", kecil);  // Ok(200)

    // Handle the result
    match u8::try_from(besar) {
        Ok(v) => println!("Success: {}", v),
        Err(_) => println!("Value {} doesn't fit in u8", besar),
    }
}

String Operators #

Rust provides two main ways to concatenate strings, each with different ownership semantics:

fn main() {
    // The + operator: moves the left string's ownership, borrows the right
    let s1 = String::from("Hello, ");
    let s2 = String::from("world!");
    let s3 = s1 + &s2;  // s1 is moved into s3; s2 stays valid because it's only borrowed

    // ANTI-PATTERN: using s1 after +
    // println!("{}", s1);  // error: s1 has been moved
    println!("{}", s2);  // ✓ s2 is still valid
    println!("{}", s3);  // ✓ "Hello, world!"

    // Concatenating many strings with + becomes verbose
    let a = String::from("tic");
    let b = String::from("tac");
    let c = String::from("toe");
    // let hasil = a + "-" + &b + "-" + &c;  // confusing, a gets moved

    // CORRECT: format! for many strings — nothing gets moved
    let a = String::from("tic");
    let b = String::from("tac");
    let c = String::from("toe");
    let hasil = format!("{}-{}-{}", a, b, c);
    println!("{}", hasil);  // tic-tac-toe
    println!("{} {} {}", a, b, c);  // all three are still valid ✓
}

Operator Precedence #

When several operators appear in one expression, Rust evaluates them based on precedence. Operators with higher precedence are evaluated first.

GroupOperatorsAssociativity
Unary-x, !x, *x, &x, &mut xRight to left
CastasLeft to right
Multiplication*, /, %Left to right
Addition+, -Left to right
Shift<<, >>Left to right
Bitwise AND&Left to right
Bitwise XOR^Left to right
Bitwise OR|Left to right
Comparison==, !=, <, >, <=, >=Non-associative
Logical AND&&Left to right
Logical OR||Left to right
Range.., ..=Non-associative
Assignment=, +=, -=, etc.Right to left
fn main() {
    // Multiplication before addition — same as math
    println!("{}", 2 + 3 * 4);    // 14, not 20

    // Comparison before logic
    println!("{}", 5 > 3 && 2 < 4);  // true

    // ANTI-PATTERN: relying on surprising bitwise precedence
    let a = 2;
    let b = 3;
    // This often surprises: & has lower precedence than ==
    let hasil = a & b == 2;  // interpreted as: a & (b == 2) = a & false = 0
    println!("{}", hasil);  // false — probably not what you wanted

    // CORRECT: use parentheses for clarity
    let hasil_jelas = (a & b) == 2;  // (2 & 3) == 2 = 2 == 2 = true
    println!("{}", hasil_jelas);  // true
}
The precedence of bitwise operators (&, |, ^) is lower than comparison operators (==, !=, <, >). This differs from C and is often a source of hidden bugs. Always use parentheses when mixing bitwise with comparisons.

Operator Overloading via Traits #

Rust lets you redefine operator behavior for custom types by implementing traits from the std::ops module. Every operator has a corresponding trait.

OperatorTraitOperatorTrait
+Add+=AddAssign
-Sub-=SubAssign
*Mul*=MulAssign
/Div/=DivAssign
%Rem%=RemAssign
- (unary)Neg!Not
&BitAnd|BitOr
^BitXor<<Shl
>>Shr== / !=PartialEq
< / > / <= / >=PartialOrd
use std::ops::{Add, Mul, Neg};

#[derive(Debug, Clone, Copy, PartialEq)]
struct Vektor2D {
    x: f64,
    y: f64,
}

impl Vektor2D {
    fn baru(x: f64, y: f64) -> Self {
        Vektor2D { x, y }
    }

    fn panjang(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }
}

impl Add for Vektor2D {
    type Output = Vektor2D;
    fn add(self, lain: Vektor2D) -> Vektor2D {
        Vektor2D::baru(self.x + lain.x, self.y + lain.y)
    }
}

impl Mul<f64> for Vektor2D {
    type Output = Vektor2D;
    fn mul(self, skalar: f64) -> Vektor2D {
        Vektor2D::baru(self.x * skalar, self.y * skalar)
    }
}

impl Neg for Vektor2D {
    type Output = Vektor2D;
    fn neg(self) -> Vektor2D {
        Vektor2D::baru(-self.x, -self.y)
    }
}

fn main() {
    let v1 = Vektor2D::baru(3.0, 4.0);
    let v2 = Vektor2D::baru(1.0, 2.0);

    let jumlah = v1 + v2;
    let skala = v1 * 2.0;
    let negatif = -v1;

    println!("v1 + v2 = {:?}", jumlah);    // Vektor2D { x: 4.0, y: 6.0 }
    println!("v1 * 2  = {:?}", skala);     // Vektor2D { x: 6.0, y: 8.0 }
    println!("-v1     = {:?}", negatif);   // Vektor2D { x: -3.0, y: -4.0 }
    println!("Length of v1: {}", v1.panjang());  // 5.0
    println!("v1 == v1: {}", v1 == v1);   // true
}

Summary #

  • No implicit conversion — mixing i32 with f64 in arithmetic operations is a compile error. Use as for explicit casts, or try_from/try_into for casts that can fail.
  • Integer division always truncates7 / 2 = 3, not 3.5 or 4. Cast to float first if you need a decimal result.
  • No ++ and -- — use += 1 and -= 1. This is a deliberate design choice to avoid prefix vs postfix ambiguity.
  • && and || short-circuit — the right side isn’t evaluated if the result can already be determined. & and | (bitwise) don’t short-circuit even when used on bool.
  • Bitwise &/| have lower precedence than == — always use parentheses when mixing the two, or you’ll get a bug that’s hard to track down.
  • Ranges .. and ..= are values — they can be stored in variables, summed with .sum(), and used as patterns in match.
  • as truncates without warning300i32 as u8 produces 44, not an error. Use try_from when you want a safe conversion with error handling.
  • Operators can be overloaded — implement traits from std::ops to define the behavior of +, -, *, and others on custom types.
  • Deref coercion works automatically&String is accepted where &str is needed, and &Vec<T> is accepted where &[T] is needed, without explicit deref.

← Previous: Data Types   Next: Conditionals →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact