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.