Complete Guide to Rust Operators

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

  1. Arithmetic Operators: +, -, *, /, % for basic math
  2. Comparison Operators: ==, !=, <, >, <=, >= for ordering
  3. Logical Operators: &&, ||, ! for boolean logic
  4. Bitwise Operators: &, |, ^, !, <<, >> for bit manipulation
  5. Range Operators: .., ..=, .., .. for sequences and slicing
  6. Assignment Operators: =, +=, -=, etc. for modifying variables
  7. Dereference Operator: * for accessing referenced values
  8. Type Casting: as for explicit type conversions
  9. Error Propagation: ? for concise error handling

Operator Overloading

  • Implement traits from std::ops to 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

  1. Use parentheses for complex expressions to improve readability
  2. Prefer checked operations when overflow is possible
  3. Implement operators only when semantically meaningful
  4. Use bitwise operators for flags and bit manipulation
  5. Leverage operator overloading for domain-specific types
  6. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper