Introduction to Rust Operators
Operators in Rust are symbols that perform specific operations on one, two, or three operands and produce a result. Rust provides a rich set of operators that follow familiar patterns from C and C++ but with additional safety guarantees and some unique features. Understanding operators is fundamental to writing expressive and efficient Rust code.
Key Concepts
- Operator Precedence: Determines the order of evaluation
- Operator Associativity: Determines evaluation order for same-precedence operators
- Overloading: Many operators can be overloaded via traits
- Safety: Rust operators include bounds checking and overflow protection
- Zero-cost: Operators compile to efficient machine code
1. Arithmetic Operators
Basic Arithmetic
fn main() {
// Addition
let sum = 5 + 10;
let sum_float = 5.2 + 3.8;
println!("Sum: {} (int), {} (float)", sum, sum_float);
// Subtraction
let difference = 95.5 - 4.3;
let negative = 5 - 10;
println!("Difference: {}, Negative: {}", difference, negative);
// Multiplication
let product = 4 * 30;
let product_float = 2.5 * 4.0;
println!("Product: {} (int), {} (float)", product, product_float);
// Division
let quotient = 56.7 / 32.2;
let integer_division = 5 / 3; // Truncates to 1
let float_division = 5.0 / 3.0; // 1.666...
println!("Quotient: {}", quotient);
println!("Integer division: {}", integer_division);
println!("Float division: {}", float_division);
// Remainder (Modulo)
let remainder = 43 % 5;
let remainder_float = 43.5 % 5.0; // Works with floats
println!("Remainder: {} (int), {} (float)", remainder, remainder_float);
// Negation
let neg = -5;
let neg_float = -3.14;
println!("Negation: {} (int), {} (float)", neg, neg_float);
}
Compound Assignment
fn main() {
let mut x = 10;
// Add and assign
x += 5;
println!("x += 5: {}", x); // 15
// Subtract and assign
x -= 3;
println!("x -= 3: {}", x); // 12
// Multiply and assign
x *= 2;
println!("x *= 2: {}", x); // 24
// Divide and assign
x /= 4;
println!("x /= 4: {}", x); // 6
// Remainder and assign
x %= 4;
println!("x %= 4: {}", x); // 2
// Works with floats too
let mut y = 10.0;
y += 2.5;
y *= 2.0;
y /= 3.0;
println!("Float operations: {}", y);
}
Overflow Behavior
fn main() {
// In debug builds, arithmetic overflow panics
let mut x: u8 = 255;
// x = x + 1; // This would panic in debug mode
// In release builds, it wraps around
// Use explicit methods for controlled behavior
// Checked operations (returns Option)
let checked = 255u8.checked_add(1);
println!("Checked add: {:?}", checked); // None
// Wrapping operations
let wrapped = 255u8.wrapping_add(1);
println!("Wrapping add: {}", wrapped); // 0
// Saturating operations (clamps to bounds)
let saturated = 255u8.saturating_add(1);
println!("Saturating add: {}", saturated); // 255
// Overflowing operations (returns result and overflow flag)
let (result, overflowed) = 255u8.overflowing_add(1);
println!("Overflowing add: {}, overflowed: {}", result, overflowed);
// Similar methods for other operations
println!("Checked sub: {:?}", 0u8.checked_sub(1));
println!("Wrapping mul: {}", 255u8.wrapping_mul(2));
println!("Saturating pow: {}", 10u32.saturating_pow(10));
}
2. Comparison Operators
Equality and Inequality
fn main() {
let a = 5;
let b = 10;
let c = 5;
// Equal to
println!("a == b: {}", a == b); // false
println!("a == c: {}", a == c); // true
// Not equal to
println!("a != b: {}", a != b); // true
println!("a != c: {}", a != c); // false
// Works with different types? No, types must match
// println!("{}", 5 == 5.0); // Error: mismatched types
// Can compare with casting
println!("{}", 5 == 5.0 as i32); // true
// Comparing strings
let s1 = String::from("hello");
let s2 = String::from("world");
let s3 = String::from("hello");
println!("s1 == s2: {}", s1 == s2); // false
println!("s1 == s3: {}", s1 == s3); // true
println!("s1 != s2: {}", s1 != s2); // true
// Comparing structs (requires PartialEq)
#[derive(PartialEq)]
struct Point {
x: i32,
y: i32,
}
let p1 = Point { x: 10, y: 20 };
let p2 = Point { x: 10, y: 20 };
let p3 = Point { x: 5, y: 25 };
println!("p1 == p2: {}", p1 == p2); // true
println!("p1 == p3: {}", p1 == p3); // false
}
Ordering Comparisons
fn main() {
let a = 5;
let b = 10;
let c = 5;
// Greater than
println!("a > b: {}", a > b); // false
println!("b > a: {}", b > a); // true
// Less than
println!("a < b: {}", a < b); // true
println!("b < a: {}", b < a); // false
// Greater than or equal to
println!("a >= b: {}", a >= b); // false
println!("a >= c: {}", a >= c); // true
// Less than or equal to
println!("a <= b: {}", a <= b); // true
println!("a <= c: {}", a <= c); // true
// Chaining comparisons (not allowed in Rust)
// if 5 < a < 10 {} // Error
// Use && instead
if 5 < a && a < 10 {
println!("a is between 5 and 10");
}
// Comparing floats (be careful with precision)
let x = 0.1 + 0.2;
let y = 0.3;
// Direct comparison might fail due to precision
println!("x == y: {}", x == y); // false
// Use epsilon comparison
let epsilon = f64::EPSILON;
println!("Approximately equal: {}", (x - y).abs() < epsilon);
}
Comparing Custom Types
use std::cmp::Ordering;
#[derive(Debug, PartialEq, Eq)]
struct Person {
name: String,
age: u32,
}
// Implement PartialOrd for custom ordering
impl PartialOrd for Person {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other)) // Delegate to Ord implementation
}
}
impl Ord for Person {
fn cmp(&self, other: &Self) -> Ordering {
// First compare by age, then by name
match self.age.cmp(&other.age) {
Ordering::Equal => self.name.cmp(&other.name),
other => other,
}
}
}
fn main() {
let alice = Person {
name: "Alice".to_string(),
age: 30,
};
let bob = Person {
name: "Bob".to_string(),
age: 25,
};
let charlie = Person {
name: "Charlie".to_string(),
age: 30,
};
println!("Alice < Bob: {}", alice < bob); // false (Alice older)
println!("Bob < Alice: {}", bob < alice); // true
println!("Alice < Charlie: {}", alice < charlie); // true (Alice before Charlie)
// Using cmp for detailed ordering
match alice.cmp(&bob) {
Ordering::Less => println!("Alice is younger"),
Ordering::Equal => println!("Same age"),
Ordering::Greater => println!("Alice is older"),
}
}
3. Logical Operators
Boolean Logic
fn main() {
let t = true;
let f = false;
// Logical AND (&&)
println!("t && t: {}", t && t); // true
println!("t && f: {}", t && f); // false
println!("f && t: {}", f && t); // false
println!("f && f: {}", f && f); // false
// Logical OR (||)
println!("t || t: {}", t || t); // true
println!("t || f: {}", t || f); // true
println!("f || t: {}", f || t); // true
println!("f || f: {}", f || f); // false
// Logical NOT (!)
println!("!t: {}", !t); // false
println!("!f: {}", !f); // true
// Short-circuit evaluation
fn expensive_computation() -> bool {
println!("Computing...");
true
}
// Second operand not evaluated because first is false
let result = false && expensive_computation();
println!("Result: {}", result); // false, no "Computing..." printed
// Second operand not evaluated because first is true
let result = true || expensive_computation();
println!("Result: {}", result); // true, no "Computing..." printed
// Chaining logical operators
let x = 5;
let y = 10;
let z = 15;
if x < y && y < z && x > 0 {
println!("All conditions are true");
}
}
Bitwise Logical Operators
fn main() {
let a = 0b1100; // 12
let b = 0b1010; // 10
// Bitwise AND (&)
let and = a & b; // 0b1000 (8)
println!("a & b = {:04b} ({})", and, and);
// Bitwise OR (|)
let or = a | b; // 0b1110 (14)
println!("a | b = {:04b} ({})", or, or);
// Bitwise XOR (^)
let xor = a ^ b; // 0b0110 (6)
println!("a ^ b = {:04b} ({})", xor, xor);
// Bitwise NOT (!)
let not_a = !a & 0b1111; // Mask to 4 bits: 0b0011 (3)
println!("!a = {:04b} ({})", not_a, not_a);
// Compound bitwise assignment
let mut x = 0b1100;
x &= 0b1010; // x = x & 0b1010
println!("x &= 0b1010: {:04b}", x);
x |= 0b0011;
println!("x |= 0b0011: {:04b}", x);
x ^= 0b1111;
println!("x ^= 0b1111: {:04b}", x);
}
4. Bitwise Shift Operators
Shift Operations
fn main() {
let x: u8 = 0b0001_0000; // 16
// Left shift (<<)
let left = x << 2; // 0b0100_0000 (64)
println!("x << 2 = {:08b} ({})", left, left);
// Right shift (>>)
let right = x >> 2; // 0b0000_0100 (4)
println!("x >> 2 = {:08b} ({})", right, right);
// Shifting with different types
let y: i8 = -16; // 0b1111_0000 in two's complement
let right_signed = y >> 2; // Arithmetic shift preserves sign
println!("y >> 2 = {:08b} ({})", right_signed as u8, right_signed);
// Compound shift assignment
let mut z = 0b0001_0000;
z <<= 2;
println!("z <<= 2: {:08b}", z);
z >>= 1;
println!("z >>= 1: {:08b}", z);
// Practical example: extracting bits
let byte = 0b1010_1100;
// Get high nibble
let high = byte >> 4;
println!("High nibble: {:04b}", high);
// Get low nibble
let low = byte & 0b1111;
println!("Low nibble: {:04b}", low);
}
Shift with Overflow
fn main() {
// Shifting beyond bit width
let x: u8 = 1;
// In debug builds, shifting >= bit width panics
// let too_far = x << 8; // Panics in debug mode
// Use checked shifts
let checked = x.checked_shl(8);
println!("Checked shift: {:?}", checked); // None
// Wrapping shifts
let wrapped = x.wrapping_shl(8);
println!("Wrapping shift: {}", wrapped); // 1 (shifts modulo bit width)
// Saturating shifts (not available, but can implement)
fn saturating_shl(x: u8, shift: u32) -> u8 {
if shift >= 8 {
u8::MAX
} else {
x << shift
}
}
println!("Saturating shift: {}", saturating_shl(1, 8)); // 255
}
5. Range Operators
Range Types
fn main() {
// Range (start..end) - exclusive of end
let range = 1..5; // 1, 2, 3, 4
for i in range {
println!("Range: {}", i);
}
// Range inclusive (start..=end)
let inclusive = 1..=5; // 1, 2, 3, 4, 5
for i in inclusive {
println!("Inclusive: {}", i);
}
// Range from (start..)
let from = 3..;
for i in from.take(5) {
println!("From 3: {}", i);
}
// Range to (..end)
let to = ..5;
for i in to.start.unwrap_or(0)..5 {
println!("To 5: {}", i);
}
// Full range (..)
let full = ..;
// Using ranges for slicing
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..4]; // [2, 3, 4]
println!("Slice: {:?}", slice);
let slice_to = &arr[..3]; // [1, 2, 3]
println!("Slice to: {:?}", slice_to);
let slice_from = &arr[2..]; // [3, 4, 5]
println!("Slice from: {:?}", slice_from);
let slice_full = &arr[..]; // [1, 2, 3, 4, 5]
println!("Full slice: {:?}", slice_full);
}
Range Patterns
fn main() {
let score = 85;
// Using ranges in match patterns
match score {
0..=59 => println!("F"),
60..=69 => println!("D"),
70..=79 => println!("C"),
80..=89 => println!("B"),
90..=100 => println!("A"),
_ => println!("Invalid score"),
}
// Ranges in if let
let value = Some(42);
if let Some(x @ 1..=50) = value {
println!("Small number: {}", x);
}
// Ranges with characters
let c = 'e';
match c {
'a'..='z' => println!("Lowercase letter"),
'A'..='Z' => println!("Uppercase letter"),
'0'..='9' => println!("Digit"),
_ => println!("Other character"),
}
}
6. Type Testing Operators
Type Casting
fn main() {
// 'as' operator for primitive casting
let int_val: i32 = 42;
let float_val = int_val as f64;
let byte_val = int_val as u8;
println!("int: {}, float: {}, byte: {}", int_val, float_val, byte_val);
// Potential loss of data
let large = 1000;
let small = large as u8; // Wraps to 232 (1000 - 256*3)
println!("1000 as u8 = {}", small);
// Boolean to integer
let true_val = true as u8; // 1
let false_val = false as u8; // 0
println!("true as u8: {}, false as u8: {}", true_val, false_val);
// Character to integer
let char_val = 'A';
let ascii = char_val as u8; // 65
println!("'A' as u8: {}", ascii);
// Pointer casts (unsafe)
let x = 42;
let ptr = &x as *const i32;
let addr = ptr as usize;
println!("Pointer address: 0x{:x}", addr);
}
Type Inspection
use std::any::type_name;
fn type_of<T>(_: &T) -> &'static str {
type_name::<T>()
}
fn main() {
let x = 5;
let y = 3.14;
let z = "hello";
let w = vec![1, 2, 3];
println!("x is of type: {}", type_of(&x));
println!("y is of type: {}", type_of(&y));
println!("z is of type: {}", type_of(&z));
println!("w is of type: {}", type_of(&w));
// Using std::any for runtime type inspection
use std::any::Any;
let value: Box<dyn Any> = Box::new(42);
if let Some(number) = value.downcast_ref::<i32>() {
println!("It's an i32: {}", number);
}
if let Some(string) = value.downcast_ref::<String>() {
println!("It's a String: {}", string);
} else {
println!("Not a String");
}
}
7. Dereference Operators
Dereferencing
fn main() {
let x = 5;
let y = &x; // y is a reference to x
// Dereference operator (*)
println!("x = {}, *y = {}", x, *y);
// Mutable reference
let mut z = 10;
let w = &mut z;
*w += 5; // Modify through dereference
println!("z after modification: {}", z);
// Dereferencing with method calls (auto-deref)
let s = String::from("hello");
let s_ref = &s;
// These are equivalent due to auto-deref
println!("Length: {}", s_ref.len()); // Method call auto-derefs
println!("Length: {}", (*s_ref).len()); // Explicit deref
// Deref coercion
fn takes_str(s: &str) {
println!("Got: {}", s);
}
let string = String::from("hello");
takes_str(&string); // &String coerces to &str
// Box dereferencing
let boxed = Box::new(5);
println!("Boxed value: {}", *boxed);
}
Smart Pointer Dereferencing
use std::ops::Deref;
use std::rc::Rc;
// Custom smart pointer
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // *y works due to Deref impl
// Rc dereferencing
let rc = Rc::new(10);
println!("Rc value: {}", *rc);
// Chained deref
let rc_box = Rc::new(MyBox::new(15));
println!("Chained deref: {}", **rc_box);
}
8. Operator Overloading
Arithmetic Operator Overloading
use std::ops::{Add, Sub, Mul, Div};
#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
// Add operator (+)
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
// Add with different types
impl Add<i32> for Point {
type Output = Point;
fn add(self, scalar: i32) -> Point {
Point {
x: self.x + scalar,
y: self.y + scalar,
}
}
}
// Subtract operator (-)
impl Sub for Point {
type Output = Point;
fn sub(self, other: Point) -> Point {
Point {
x: self.x - other.x,
y: self.y - other.y,
}
}
}
// Multiply operator (*)
impl Mul<i32> for Point {
type Output = Point;
fn mul(self, scalar: i32) -> Point {
Point {
x: self.x * scalar,
y: self.y * scalar,
}
}
}
fn main() {
let p1 = Point { x: 10, y: 20 };
let p2 = Point { x: 5, y: 15 };
let p3 = p1 + p2;
println!("p1 + p2 = {:?}", p3);
let p4 = p1 - p2;
println!("p1 - p2 = {:?}", p4);
let p5 = p1 + 5;
println!("p1 + 5 = {:?}", p5);
let p6 = p1 * 2;
println!("p1 * 2 = {:?}", p6);
}
Bitwise Operator Overloading
use std::ops::{BitAnd, BitOr, BitXor, Not};
#[derive(Debug, Clone, Copy, PartialEq)]
struct Flags(u8);
impl BitAnd for Flags {
type Output = Flags;
fn bitand(self, other: Flags) -> Flags {
Flags(self.0 & other.0)
}
}
impl BitOr for Flags {
type Output = Flags;
fn bitor(self, other: Flags) -> Flags {
Flags(self.0 | other.0)
}
}
impl BitXor for Flags {
type Output = Flags;
fn bitxor(self, other: Flags) -> Flags {
Flags(self.0 ^ other.0)
}
}
impl Not for Flags {
type Output = Flags;
fn not(self) -> Flags {
Flags(!self.0)
}
}
fn main() {
let read = Flags(0b001);
let write = Flags(0b010);
let execute = Flags(0b100);
let read_write = read | write;
println!("read | write = {:03b}", (read_write).0);
let all = read | write | execute;
println!("all permissions = {:03b}", all.0);
let without_read = all & !read;
println!("all without read = {:03b}", without_read.0);
let toggle = read ^ write;
println!("read ^ write = {:03b}", toggle.0);
}
Index Operator Overloading
use std::ops::{Index, IndexMut};
struct Matrix {
data: Vec<Vec<i32>>,
rows: usize,
cols: usize,
}
impl Matrix {
fn new(rows: usize, cols: usize) -> Self {
Matrix {
data: vec![vec![0; cols]; rows],
rows,
cols,
}
}
}
// Immutable indexing
impl Index<usize> for Matrix {
type Output = Vec<i32>;
fn index(&self, row: usize) -> &Vec<i32> {
&self.data[row]
}
}
// Mutable indexing
impl IndexMut<usize> for Matrix {
fn index_mut(&mut self, row: usize) -> &mut Vec<i32> {
&mut self.data[row]
}
}
// Two-dimensional indexing
impl Index<(usize, usize)> for Matrix {
type Output = i32;
fn index(&self, index: (usize, usize)) -> &i32 {
let (row, col) = index;
&self.data[row][col]
}
}
impl IndexMut<(usize, usize)> for Matrix {
fn index_mut(&mut self, index: (usize, usize)) -> &mut i32 {
let (row, col) = index;
&mut self.data[row][col]
}
}
fn main() {
let mut mat = Matrix::new(3, 3);
// Use IndexMut to set values
mat[0][0] = 1;
mat[0][1] = 2;
mat[1][0] = 3;
mat[1][1] = 4;
// Use Index to get values
println!("mat[0][1] = {}", mat[0][1]);
// Use tuple indexing
mat[(2, 2)] = 5;
println!("mat[(2, 2)] = {}", mat[(2, 2)]);
// Print matrix
for i in 0..mat.rows {
for j in 0..mat.cols {
print!("{} ", mat[(i, j)]);
}
println!();
}
}
9. Assignment Operators
Basic Assignment
fn main() {
// Simple assignment (=)
let x = 5;
let y = x; // Copy (for Copy types)
// Move semantics for non-Copy types
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
// println!("{}", s1); // Error: s1 no longer valid
// Clone for explicit copy
let s3 = String::from("hello");
let s4 = s3.clone();
println!("s3: {}, s4: {}", s3, s4); // Both valid
// Destructuring assignment
let (a, b, c) = (1, 2, 3);
println!("a: {}, b: {}, c: {}", a, b, c);
// Struct destructuring
struct Point {
x: i32,
y: i32,
}
let p = Point { x: 10, y: 20 };
let Point { x, y } = p;
println!("x: {}, y: {}", x, y);
}
Compound Assignment Traits
use std::ops::{AddAssign, SubAssign, MulAssign, DivAssign};
#[derive(Debug, Clone, Copy)]
struct Vector {
x: i32,
y: i32,
}
impl AddAssign for Vector {
fn add_assign(&mut self, other: Vector) {
self.x += other.x;
self.y += other.y;
}
}
impl SubAssign for Vector {
fn sub_assign(&mut self, other: Vector) {
self.x -= other.x;
self.y -= other.y;
}
}
impl MulAssign<i32> for Vector {
fn mul_assign(&mut self, scalar: i32) {
self.x *= scalar;
self.y *= scalar;
}
}
fn main() {
let mut v1 = Vector { x: 10, y: 20 };
let v2 = Vector { x: 5, y: 15 };
v1 += v2;
println!("v1 after +=: {:?}", v1);
v1 -= v2;
println!("v1 after -=: {:?}", v1);
v1 *= 2;
println!("v1 after *=: {:?}", v1);
}
10. Operator Precedence and Associativity
Precedence Table
fn main() {
// Operator precedence (highest to lowest)
// 1. Path and method calls (., ::, ())
// 2. Unary operators (!, -, *, &, &mut)
// 3. As and .. (as, ..)
// 4. Multiplicative (*, /, %)
// 5. Additive (+, -)
// 6. Shift (<<, >>)
// 7. Bitwise AND (&)
// 8. Bitwise XOR (^)
// 9. Bitwise OR (|)
// 10. Comparison (==, !=, <, >, <=, >=)
// 11. Logical AND (&&)
// 12. Logical OR (||)
// 13. Range (.., ..=)
// 14. Assignment and compound assignment (=, +=, etc.)
let result = 5 + 3 * 4; // 5 + (3 * 4) = 17
println!("5 + 3 * 4 = {}", result);
let result = (5 + 3) * 4; // (5 + 3) * 4 = 32
println!("(5 + 3) * 4 = {}", result);
// Mixing bitwise and comparison
let x = 5;
let y = 10;
let z = 15;
// Bitwise AND has higher precedence than comparison
let result = x & y == 0; // x & (y == 0) - probably not what you want
println!("x & y == 0 = {}", result);
// Use parentheses for clarity
let result = (x & y) == 0;
println!("(x & y) == 0 = {}", result);
// Logical operators short-circuit
let result = expensive_check() || cheap_check(); // Short-circuits
}
fn expensive_check() -> bool {
println!("Expensive check");
false
}
fn cheap_check() -> bool {
println!("Cheap check");
true
}
Associativity Examples
fn main() {
// Left-associative operators (most)
let result = 10 - 5 - 2; // (10 - 5) - 2 = 3
println!("10 - 5 - 2 = {}", result);
let result = 10 / 5 / 2; // (10 / 5) / 2 = 1
println!("10 / 5 / 2 = {}", result);
// Right-associative operators
// Assignment is right-associative
let mut a = 5;
let mut b = 10;
let mut c = 15;
a = b = c; // a = (b = c)
println!("a = b = c: a = {}, b = {}, c = {}", a, b, c);
// .. range is also tricky
let range = 1..5..=10; // (1..5)..=10 - usually not what you want
// Use parentheses: 1..(5..=10) if needed
}
11. Special Operators
The ? Operator
use std::num::ParseIntError;
fn parse_number(s: &str) -> Result<i32, ParseIntError> {
let num = s.parse::<i32>()?; // Propagates error if any
Ok(num * 2)
}
fn process_numbers() -> Result<(), ParseIntError> {
let a = parse_number("10")?;
let b = parse_number("20")?;
let c = parse_number("30")?;
println!("Results: {}, {}, {}", a, b, c);
Ok(())
}
// With Option
fn find_first_even(numbers: &[i32]) -> Option<&i32> {
let first = numbers.get(0)?; // Returns None if index out of bounds
if first % 2 == 0 {
Some(first)
} else {
numbers.get(1) // May return None
}
}
fn main() {
match process_numbers() {
Ok(()) => println!("Success!"),
Err(e) => println!("Error: {}", e),
}
let nums = vec![1, 3, 5, 7];
match find_first_even(&nums) {
Some(x) => println!("Found even: {}", x),
None => println!("No even numbers"),
}
}
The ..= and .. Operators (Revisited)
fn main() {
// Inclusive range patterns in matches
match 42 {
0..=50 => println!("Between 0 and 50"),
51..=100 => println!("Between 51 and 100"),
_ => println!("Out of range"),
}
// Ranges in for loops
for i in (0..5).rev() {
println!("Reverse: {}", i);
}
// Step by (using step_by)
for i in (0..=10).step_by(2) {
println!("Even: {}", i);
}
// Ranges with custom types
#[derive(PartialEq, PartialOrd)]
struct Grade(char);
let grade = Grade('B');
// Can't use range with custom type without more traits
// if grade >= Grade('A') && grade <= Grade('C') {
// println!("Passing grade");
// }
}
12. Practical Examples
Bit Manipulation Utilities
// Bit manipulation helper
fn get_bit(value: u8, n: u8) -> bool {
(value & (1 << n)) != 0
}
fn set_bit(value: u8, n: u8) -> u8 {
value | (1 << n)
}
fn clear_bit(value: u8, n: u8) -> u8 {
value & !(1 << n)
}
fn toggle_bit(value: u8, n: u8) -> u8 {
value ^ (1 << n)
}
fn main() {
let mut flags: u8 = 0b0000_0000;
flags = set_bit(flags, 2);
flags = set_bit(flags, 5);
println!("Flags after setting: {:08b}", flags);
println!("Bit 2 is set: {}", get_bit(flags, 2));
println!("Bit 3 is set: {}", get_bit(flags, 3));
flags = clear_bit(flags, 2);
println!("After clearing bit 2: {:08b}", flags);
flags = toggle_bit(flags, 5);
println!("After toggling bit 5: {:08b}", flags);
}
Mathematical Operations
// Complex number operations
use std::ops::{Add, Mul};
#[derive(Debug, Clone, Copy)]
struct Complex {
real: f64,
imag: f64,
}
impl Complex {
fn new(real: f64, imag: f64) -> Self {
Complex { real, imag }
}
fn magnitude(&self) -> f64 {
(self.real * self.real + self.imag * self.imag).sqrt()
}
}
impl Add for Complex {
type Output = Complex;
fn add(self, other: Complex) -> Complex {
Complex {
real: self.real + other.real,
imag: self.imag + other.imag,
}
}
}
impl Mul for Complex {
type Output = Complex;
fn mul(self, other: Complex) -> Complex {
Complex {
real: self.real * other.real - self.imag * other.imag,
imag: self.real * other.imag + self.imag * other.real,
}
}
}
fn main() {
let c1 = Complex::new(3.0, 4.0);
let c2 = Complex::new(1.0, 2.0);
let sum = c1 + c2;
let product = c1 * c2;
println!("c1: {:?}, magnitude: {}", c1, c1.magnitude());
println!("c2: {:?}", c2);
println!("Sum: {:?}", sum);
println!("Product: {:?}", product);
}
Conclusion
Rust's operators provide a comprehensive set of tools for manipulating data:
Key Takeaways
- Arithmetic Operators:
+,-,*,/,%for basic math - Comparison Operators:
==,!=,<,>,<=,>=for ordering - Logical Operators:
&&,||,!for boolean logic - Bitwise Operators:
&,|,^,!,<<,>>for bit manipulation - Range Operators:
..,..=,..,..for sequences and slicing - Assignment Operators:
=,+=,-=, etc. for modifying variables - Dereference Operator:
*for accessing referenced values - Type Casting:
asfor explicit type conversions - Error Propagation:
?for concise error handling
Operator Overloading
- Implement traits from
std::opsto overload operators - Common traits:
Add,Sub,Mul,Div,Rem,Neg - Bitwise:
BitAnd,BitOr,BitXor,Not,Shl,Shr - Compound assignment:
AddAssign,SubAssign, etc. - Indexing:
Index,IndexMut
Safety Features
- Overflow checking in debug builds
- Checked, wrapping, and saturating methods
- Type safety prevents mixed-type operations
- Borrow checker ensures reference safety
Best Practices
- Use parentheses for complex expressions to improve readability
- Prefer checked operations when overflow is possible
- Implement operators only when semantically meaningful
- Use bitwise operators for flags and bit manipulation
- Leverage operator overloading for domain-specific types
- Be careful with floating-point comparisons due to precision issues
Rust's operator system balances expressiveness with safety, providing familiar syntax while preventing common programming errors through compile-time checks and runtime safety features.