Operators
Rust’s operator surface is similar to C’s at the level of arithmetic and comparison, with substantial differences: integer overflow is panic in debug, wrap in release (with wrapping_*/checked_*/saturating_* methods for explicit choice), boolean operators are &&/||/! (no word-spelt forms), the question-mark operator ? admits compact error propagation, and the range operators (.., ..=) admit slice and range syntax. Operator overloading is admitted through traits (Add, Sub, Mul, etc.); the type-system constraints prevent ambiguity.
Arithmetic operators
The principal arithmetic operators:
let a = 7;
let b = 2;
a + b // 9
a - b // 5
a * b // 14
a / b // 3 (integer division; truncates toward zero for both signs)
a % b // 1 (remainder)
-a // negation; only on signed types
let c = 7.5;
let d = 2.0;
c / d // 3.75 (floating-point division)
10_i32.pow(3); // 1000 (integer power)
2.0_f64.powi(10); // 1024.0 (integer exponent)
2.0_f64.powf(0.5); // 1.41... (floating exponent)
Several distinguishing features:
Integer overflow
In debug builds, integer arithmetic panics on overflow:
let n: u8 = 255;
let m = n + 1; // debug: panic!; release: wraps to 0
In release builds, overflow wraps (modular arithmetic). The behaviour is defined — no undefined behaviour — but differs between modes.
For explicit overflow handling:
let x: u8 = 200;
x.wrapping_add(100); // wraps: 44
x.checked_add(100); // Option<u8>; None on overflow
x.saturating_add(100); // saturates at u8::MAX: 255
x.overflowing_add(100); // (u8, bool); (44, true) on overflow
The four families admit explicit choice when overflow matters; they are conventional in numeric code that handles untrusted input.
No implicit numeric conversion
Rust does not implicitly convert between numeric types:
let a: i32 = 5;
let b: f64 = 3.14;
let sum = a + b; // ERROR: type mismatch
let sum = a as f64 + b; // OK: explicit cast
let sum = f64::from(a) + b; // OK: From trait
The strictness is one of Rust’s distinctive features; mixing types requires explicit conversion.
Comparison
a == b // equality
a != b // inequality
a < b
a <= b
a > b
a >= b
Comparison requires the PartialEq (or Eq) trait for equality and PartialOrd (or Ord) for ordering. Most primitive types and standard-library types implement these.
Floating-point types are PartialEq and PartialOrd but not Eq and Ord because of NaN — comparisons involving NaN return false:
let nan = f64::NAN;
nan == nan // false
nan < 1.0 // false
nan > 1.0 // false
For sorting floats, conventional patterns use partial_cmp with explicit NaN handling or the total_cmp method (since 1.62) for IEEE 754 total ordering.
Logical operators
true && false // false (short-circuiting)
true || false // true (short-circuiting)
!true // false
The logical operators && and || short-circuit. They operate only on bool; there is no truthiness coercion.
Bitwise operators
For integer types:
0b1010 & 0b1100 // 0b1000 (AND)
0b1010 | 0b1100 // 0b1110 (OR)
0b1010 ^ 0b1100 // 0b0110 (XOR)
!0b1010_u8 // 0b1111_0101 (bitwise NOT; one's complement)
0b1010_u8 << 2 // 0b101000
0b1010_u8 >> 1 // 0b0101
The ! operator is bitwise NOT on integers and logical NOT on booleans; the operator is overloaded by type.
Reference operators
Rust’s reference operators:
let x = 5;
let r = &x; // shared reference
let n = *r; // dereference
let mut y = 10;
let m = &mut y; // mutable reference
*m = 20; // assign through the mutable reference
The full treatment is in Ownership and borrowing.
Range operators
The .. and ..= admit ranges:
0..10 // 0, 1, ..., 9 (exclusive)
0..=10 // 0, 1, ..., 10 (inclusive)
..10 // 0, ..., 9 (start unspecified)
0.. // 0, 1, ... (end unspecified)
.. // all (rare)
// Iteration:
for i in 0..10 {
println!("{}", i);
}
// Slicing:
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..4]; // [2, 3, 4]
let slice = &arr[..3]; // [1, 2, 3]
let slice = &arr[2..]; // [3, 4, 5]
The .. operator is the conventional Rust range syntax. In pattern matching, ..= is for inclusive ranges (1..=10); the half-open .. is rare in patterns.
The ? operator
The ? operator admits compact error propagation:
fn read_file(path: &str) -> Result<String, std::io::Error> {
let mut f = File::open(path)?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
Ok(contents)
}
The ? is roughly:
let mut f = match File::open(path) {
Ok(f) => f,
Err(e) => return Err(e),
};
If the operand is Err(e), ? returns Err(e) from the enclosing function; if Ok(value), it unwraps to value. The mechanism admits substantial conciseness in error-handling chains; treated in Error handling.
The ? also works for Option<T>: if None, returns None.
The dot operator
The . operator is method call and field access:
let p = Point { x: 3.0, y: 4.0 };
p.x; // field access
p.distance(&q); // method call
let v = vec![1, 2, 3];
v.len(); // method
v.iter(); // method
v.iter().map(|n| n * 2).collect::<Vec<_>>();
The dot operator admits method resolution across the type’s methods, its traits’ default methods, and its parent traits’ methods. Rust’s deref coercion admits methods on T to be called on &T, &mut T, and Box<T> automatically:
let s = String::from("hello");
s.len(); // String's method
let r: &String = &s;
r.len(); // works through deref
Operator overloading
Operators are implemented through traits in std::ops. The principal traits:
| Operator | Trait |
|---|---|
+ | Add |
- (binary) | Sub |
* | Mul |
/ | Div |
% | Rem |
- (unary) | Neg |
! | Not |
& (bitwise) | BitAnd |
| (bitwise) | BitOr |
^ | BitXor |
<< | Shl |
>> | Shr |
+=, -=, etc. | AddAssign, SubAssign, etc. |
==, != | PartialEq |
<, <=, >, >= | PartialOrd |
[index] | Index, IndexMut |
use std::ops::Add;
#[derive(Clone, Copy)]
struct Money(i64);
impl Add for Money {
type Output = Money;
fn add(self, other: Money) -> Money {
Money(self.0 + other.0)
}
}
let a = Money(100);
let b = Money(50);
let total = a + b; // Money(150)
The trait-based mechanism admits user-defined types to participate in arithmetic; treated in Traits.
Assignment
Assignment uses =:
let mut x = 5;
x = 10; // reassignment (mut required)
let mut v = vec![1, 2, 3];
v[0] = 100; // through indexing
Assignment moves the value (or copies for Copy types). The full treatment is in Ownership and borrowing.
Compound assignments dispatch through the *Assign traits:
let mut n = 5;
n += 1; // AddAssign
n -= 1; // SubAssign
n *= 2;
n /= 2;
n %= 2;
n <<= 1;
n &= 0xff;
The conditional expression
Rust does not have a ?: ternary; if … else is itself the conditional expression:
let max = if a > b { a } else { b };
The two arms must have the same type; the if expression’s value is the value of the selected arm. Treated in Conditionals.
as and casting
The as operator admits explicit conversion between primitive types:
let n: i32 = 42;
let m: i64 = n as i64;
let f: f64 = n as f64;
let big: u32 = 1_000_000;
let small: u8 = big as u8; // truncates; 64 (low 8 bits)
let neg: i32 = -1;
let unsigned: u32 = neg as u32; // u32::MAX (two's complement)
The as is infallible; it always produces a result, possibly truncating or producing a “weird” value. For checked conversions, use TryFrom:
let n: i32 = -5;
let u: Result<u32, _> = u32::try_from(n); // Err(...)
Operator precedence
The full precedence table from highest to lowest:
| Level | Operators | Associativity |
|---|---|---|
| 1 | Paths, method calls, field access, function calls | left |
| 2 | Question mark ? | left |
| 3 | Unary -, !, * (deref), &, &mut | right |
| 4 | as | left |
| 5 | *, /, % | left |
| 6 | +, - (binary) | left |
| 7 | <<, >> | left |
| 8 | & (bitwise) | left |
| 9 | ^ | left |
| 10 | | (bitwise) | left |
| 11 | ==, !=, <, >, <=, >= | non-associative |
| 12 | && | left |
| 13 | || | left |
| 14 | .., ..= (range) | left |
| 15 | =, +=, etc. (assignment) | right |
| 16 | return, break (with value) | (n/a) |
Three precedence facts worth memorising:
&and|(bitwise) bind looser than comparison;if (x & MASK) == 0requires the parentheses.asbinds tightly;x as u32 + 1is(x as u32) + 1, notx as (u32 + 1).?binds very tightly;f()?is the conventional form, with?applied to the call result.
When in doubt, parenthesise. Rust does not penalise redundant parentheses; the alternative is to invite the bug.
A note on what Rust does not have
The operators that some other languages provide and Rust’s status:
| Operator | Available? |
|---|---|
Pre/post increment (++x, x++) | No. Use x += 1. |
&&=, ||= | No. Use explicit if. |
?: ternary | No. Use if … else (an expression). |
| Pointer arithmetic | No (without unsafe). |
| Operator overloading by class | Yes (through traits). |
<<< / unsigned right shift | No. Cast to unsigned first. |
** for exponentiation | No. Use pow / powi / powf methods. |
The combination — arithmetic operators with explicit overflow handling, the ? operator for error propagation, range operators for slicing and iteration, trait-based overloading — is the substance of Rust’s operator surface.