PONY λ M2 Modula-2

Rust.CodeCompared.To/Swift

An interactive executable cheatsheet comparing Rust and Swift

Rust 1.95 Swift 6.3
Hello World & Tooling
Hello, World
fn main() { println!("Hello, World!"); }
print("Hello, World!")
Swift scripts need no main function — top-level statements execute directly. print() is the equivalent of Rust's println! macro; both append a trailing newline. Swift files compiled as scripts run top-to-bottom, while a fully structured Swift app would use a @main entry point.
Build tools & project structure
// Cargo (built into Rust): // cargo new myproject — create a project (src/main.rs + Cargo.toml) // cargo build --release — compile to a native binary // cargo run — compile + run // cargo test — run tests // cargo add serde — add a dependency // Dependencies live in Cargo.toml: // [dependencies] // serde = { version = "1", features = ["derive"] }
// Swift Package Manager (part of the Swift toolchain): // swift package init --name MyProject --type executable // swift build — compile (binary in .build/debug/) // swift build -c release — release build // swift run — compile + run // swift test — run tests // swift package add https://github.com/apple/swift-log.git --from 1.0.0 // Dependencies live in Package.swift: // .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0")
Both Cargo and the Swift Package Manager are first-class tools bundled with the language. Cargo uses a TOML manifest (Cargo.toml); Swift Package Manager uses a Swift source file (Package.swift). Both support workspaces for multi-package monorepos.
Formatted output
fn main() { let name = "Alice"; let score = 42; println!("Player: {name}, score: {score}"); println!("Padded: {:>10}", name); println!("Float: {:.2}", 3.14159_f64); }
import Foundation let name = "Alice" let score = 42 print("Player: \(name), score: \(score)") print(String(format: "Padded: %10@", name)) print(String(format: "Float: %.2f", 3.14159))
Rust uses format macros (format!, println!) with {} placeholders. Swift uses string interpolation (\(expression)) for simple cases and String(format:) (Foundation) for C-style printf formatting. Swift uses %@ (not %s) for string arguments, and the call requires import Foundation.
Variables & Types
let vs. var — the role reversal
fn main() { let immutable = 42; // Rust: let is immutable let mut mutable = 10; // Rust: let mut is mutable mutable += 5; println!("{immutable}"); println!("{mutable}"); }
let immutable = 42 // Swift: let is also immutable var mutable = 10 // Swift: var is mutable (not "let mut") mutable += 5 print(immutable) print(mutable)
Both languages default to immutability via let. The difference is mutation: Rust adds mut to the existing let keyword, while Swift uses a separate keyword var. The compiler in both languages enforces immutability and warns when a var/let mut is never actually mutated.
Type inference & annotations
fn main() { let count = 10_i32; // inferred as i32 let ratio: f64 = 0.75; // explicit annotation let label: &str = "hello"; println!("{count} {ratio} {label}"); }
let count: Int32 = 10 // explicit annotation let ratio = 0.75 // inferred as Double let label = "hello" // inferred as String print(count, ratio, label)
Both languages infer types from context. Type annotations in Rust use a colon after the name (let ratio: f64), which is identical to Swift's syntax. Rust defaults integer literals to i64 and floats to f64; Swift defaults integer literals to Int (pointer-sized) and floats to Double.
Integer types
fn main() { let signed: i64 = -9_000_000_000; let unsigned: u32 = 4_294_967_295; let small: i8 = 127; let platform: isize = 42; // pointer-sized println!("{signed} {unsigned} {small} {platform}"); }
let signed: Int64 = -9_000_000_000 let unsigned: UInt32 = 4_294_967_295 let small: Int8 = 127 let platform: Int = 42 // pointer-sized (like isize) print(signed, unsigned, small, platform)
The integer types map directly: Rust's i8/i16/i32/i64/i128 become Swift's Int8/Int16/Int32/Int64; Rust's u* types become Swift's UInt*. Both use underscore digit separators in literals. Rust's isize/usize correspond to Swift's Int/UInt.
Tuples
fn main() { let point: (f64, f64) = (3.0, 4.0); let (x, y) = point; // destructure println!("x={x} y={y}"); println!("first={}", point.0); // index access }
let point: (Double, Double) = (3.0, 4.0) let (x, y) = point // destructure print("x=\(x) y=\(y)") print("first=\(point.0)") // index access // Swift also supports named tuple elements: let named = (x: 1.0, y: 2.0) print("named.x=\(named.x)")
Tuples work almost identically in both languages — positional access with .0, .1, and destructuring assignment. Swift additionally supports named tuple elements ((x: 1.0, y: 2.0)), which Rust lacks; Swift's named elements are accessed by label rather than index.
Type casting
fn main() { let n: i32 = 1000; let wider = n as i64; // widening — always safe let narrowed = n as i8; // narrowing — wraps silently let from_float = 3.99_f64 as i32; // truncates toward zero println!("{wider} {narrowed} {from_float}"); }
let n: Int32 = 1000 let wider = Int64(n) // widening via initializer let narrowed = Int8(truncatingIfNeeded: n) let fromFloat = Int(3.99) // truncates toward zero print(wider, narrowed, fromFloat)
Rust uses the as keyword for numeric casts; Swift uses type initializers (Int64(n)). Swift's approach makes narrowing explicit — you choose between init(truncatingIfNeeded:), init(exactly:) (returns Optional), or the checked variants. Rust's as narrows silently by wrapping/truncating, which can be surprising.
Strings
String types
fn main() { let owned: String = String::from("hello"); // heap-allocated, owned let borrowed: &str = "world"; // string slice (fat pointer) let combined = format!("{owned} {borrowed}"); println!("{combined}"); println!("{}", owned.len()); // byte length }
let greeting: String = "hello" // always heap-allocated let world: String = "world" // no separate slice type needed let combined = "\(greeting) \(world)" print(combined) print(greeting.count) // character count (not byte count)
Rust has two string types: String (owned, heap-allocated) and &str (a borrowed slice). Swift has only one — String is always the owned type, and Swift copies on write (COW) for efficiency. Swift's .count returns the number of Unicode grapheme clusters, not bytes; Rust's .len() returns byte count.
String interpolation
fn main() { let name = "Alice"; let age = 30_u32; let greeting = format!("Hello, {name}! You are {age} years old."); println!("{greeting}"); println!("Double age: {}", age * 2); }
let name = "Alice" let age: UInt32 = 30 let greeting = "Hello, \(name)! You are \(age) years old." print(greeting) print("Double age: \(age * 2)")
Rust uses format! with {name} capture syntax for string building; Swift uses \(expression) directly inside any double-quoted string. Both can embed arbitrary expressions — Swift's interpolation can call methods and perform arithmetic inline without a separate format! call.
Common string operations
fn main() { let s = String::from("Hello, Rust!"); println!("{}", s.to_uppercase()); println!("{}", s.to_lowercase()); println!("{}", s.contains("Rust")); println!("{}", s.replace("Rust", "Swift")); let parts: Vec<&str> = s.split(", ").collect(); println!("{:?}", parts); }
import Foundation let s = "Hello, Swift!" print(s.uppercased()) print(s.lowercased()) print(s.contains("Swift")) print(s.replacingOccurrences(of: "Swift", with: "Rust")) let parts = s.components(separatedBy: ", ") print(parts)
The common string operations exist in both languages but with different naming conventions. Rust uses verb suffixes like to_uppercase() and replace(); Swift uses longer, more English-readable names like uppercased() and replacingOccurrences(of:with:). Swift's argument labels make multi-parameter methods read like sentences.
Multiline strings
fn main() { let poem = "Roses are red, Violets are blue, Rust has no GC, And neither does Swift."; println!("{poem}"); // Raw strings with no escaping needed: let raw = r"No escape here"; println!("{raw}"); }
let poem = """ Roses are red, Violets are blue, Rust has no GC, And neither does Swift. """ print(poem) // Extended string delimiters — quotes need no escaping: let raw = #"She said "hello" without any escaping"# print(raw)
Rust multiline strings are plain double-quoted strings; raw strings use r"..." or r#"..."#. Swift uses triple-quote ("""...""") for multiline literals — the closing """ sets the indentation baseline, stripping leading whitespace. Swift's extended string delimiter (#"..."#) is exactly analogous to Rust's r#"..."#.
Collections
Vectors and arrays
fn main() { let fixed: [i32; 3] = [1, 2, 3]; // fixed-size array let mut growable: Vec<i32> = vec![4, 5, 6]; growable.push(7); println!("{:?}", fixed); println!("{:?}", growable); println!("len={}", growable.len()); }
let fixed: [Int32] = [1, 2, 3] // Array<Int32> — always heap-allocated var growable: [Int] = [4, 5, 6] growable.append(7) print(fixed) print(growable) print("len=\(growable.count)")
Rust distinguishes fixed-size arrays ([T; N], stack-allocated) from heap-allocated growable vectors (Vec). Swift has only one array type (Array, written [T]) that is always heap-allocated and grows dynamically. Swift arrays use copy-on-write: assigning one array to another is cheap until a mutation occurs.
Hash maps and dictionaries
use std::collections::HashMap; fn main() { let mut scores: HashMap<String, u32> = HashMap::new(); scores.insert(String::from("Alice"), 95); scores.insert(String::from("Bob"), 87); println!("{:?}", scores.get("Alice")); // Some(95) scores.entry(String::from("Carol")).or_insert(80); println!("{}", scores.len()); }
var scores: [String: UInt32] = [:] scores["Alice"] = 95 scores["Bob"] = 87 print(scores["Alice"] as Any) // Optional(95) if scores["Carol"] == nil { scores["Carol"] = 80 } print(scores.count)
Rust's HashMap requires an explicit import from std::collections; Swift's Dictionary is available as [Key: Value] without any import. Both return an optional on lookup (Option<&V> in Rust, V? in Swift). Rust's entry().or_insert() is a powerful upsert pattern; Swift requires a nil-check.
Sets
use std::collections::HashSet; fn main() { let mut primes: HashSet<u32> = [2, 3, 5, 7].into_iter().collect(); primes.insert(11); println!("{}", primes.contains(&7)); let evens: HashSet<u32> = [2, 4, 6].into_iter().collect(); let common: HashSet<_> = primes.intersection(&evens).collect(); println!("{:?}", common); }
var primes: Set<UInt32> = [2, 3, 5, 7] primes.insert(11) print(primes.contains(7)) let evens: Set<UInt32> = [2, 4, 6] let common = primes.intersection(evens) print(common)
Rust's HashSet requires an import; Swift's Set is a built-in type. Both support set operations (intersection, union, subtracting). The main syntax difference: Rust's set operations return iterators and require .collect(); Swift's return a new Set directly.
Iterator operations
fn main() { let numbers = vec![1, 2, 3, 4, 5, 6]; let evens: Vec<i32> = numbers.iter() .filter(|&&x| x % 2 == 0) .map(|&x| x * x) .collect(); let total: i32 = numbers.iter().sum(); println!("{:?}", evens); println!("sum={total}"); }
let numbers = [1, 2, 3, 4, 5, 6] let evens = numbers .filter { $0 % 2 == 0 } .map { $0 * $0 } print(evens) let total = numbers.reduce(0, +) print("sum=\(total)")
Both languages have lazy, chainable iterator/sequence operations. Rust iterators are lazy by default and require .collect() to materialize results; Swift's array methods are eager and return new arrays directly. Swift uses $0 shorthand for closure parameters in simple cases. Rust closures use double-reference patterns (&&x) when iterating over slices — a common gotcha.
Control Flow
if / else
fn main() { let temperature = 22_i32; if temperature > 30 { println!("Hot"); } else if temperature > 15 { println!("Comfortable"); } else { println!("Cold"); } // if as expression: let label = if temperature > 20 { "warm" } else { "cool" }; println!("{label}"); }
let temperature = 22 if temperature > 30 { print("Hot") } else if temperature > 15 { print("Comfortable") } else { print("Cold") } // Ternary operator (Swift has ? :, Rust does not): let label = temperature > 20 ? "warm" : "cool" print(label)
Rust's if is an expression that can return a value; Swift uses the traditional ternary operator (condition ? a : b) for inline conditionals (Rust deliberately omits ?:). Neither language requires parentheses around the condition. Swift adds the ternary because if is a statement, not an expression, in most Swift contexts.
for loops
fn main() { // Range — exclusive: for i in 0..5 { print!("{i} "); } println!(); // Range — inclusive: for i in 1..=3 { print!("{i} "); } println!(); // Enumerate: for (index, value) in ["a", "b", "c"].iter().enumerate() { println!("{index}: {value}"); } }
// Range — exclusive (half-open): for i in 0..<5 { print("\(i) ", terminator: "") } print("") // Range — inclusive (closed): for i in 1...3 { print("\(i) ", terminator: "") } print("") // Enumerate: for (index, value) in ["a", "b", "c"].enumerated() { print("\(index): \(value)") }
Ranges use different syntax: Rust uses .. (exclusive) and ..= (inclusive); Swift uses ..< (half-open) and ... (closed). Both support enumerate/enumerated to pair an index with each element. Swift's print adds a newline by default — use terminator: "" to suppress it, analogous to Rust's print! (without the ln).
while / repeat-while
fn main() { let mut count = 0_u32; while count < 3 { println!("count={count}"); count += 1; } // loop is Rust's infinite-loop with break-to-return: let result = loop { count += 1; if count >= 6 { break count * 2; } }; println!("result={result}"); }
var count: UInt32 = 0 while count < 3 { print("count=\(count)") count += 1 } // repeat-while runs the body at least once (do-while in many languages): repeat { count += 1 } while count < 6 print("count=\(count)")
Rust adds a loop construct (infinite loop that can break with a value) on top of standard while. Swift replaces Rust's do-while with repeat-while and has no value-returning loop; break values require a different pattern in Swift. Both languages support labeled loops (outer: for ...) for breaking out of nested loops.
Pattern matching (match vs. switch)
fn main() { let score = 85_u32; let grade = match score { 90..=100 => "A", 80..=89 => "B", 70..=79 => "C", _ => "F", }; println!("Grade: {grade}"); // Guard conditions: let x = 42_i32; match x { n if n < 0 => println!("negative"), 0 => println!("zero"), n => println!("positive: {n}"), } }
let score: UInt32 = 85 let grade: String switch score { case 90...100: grade = "A" case 80..<90: grade = "B" case 70..<80: grade = "C" default: grade = "F" } print("Grade: \(grade)") // Where clauses (equivalent to Rust guard conditions): let x = 42 switch x { case let n where n < 0: print("negative") case 0: print("zero") case let n: print("positive: \(n)") }
Rust's match and Swift's switch are both exhaustive pattern matchers that require handling all cases. Key differences: Rust's arms use => and a comma to separate arms; Swift's use : and no comma. Swift cases don't fall through by default (unlike C switch). Rust's if n < 0 guard becomes Swift's where n < 0.
Functions & Closures
Function definition
fn add(x: i32, y: i32) -> i32 { x + y // last expression is the implicit return value } fn greet(name: &str) -> String { format!("Hello, {name}!") } fn main() { println!("{}", add(3, 4)); println!("{}", greet("Alice")); }
func add(x: Int32, y: Int32) -> Int32 { return x + y // explicit return required (no implicit last-expr) } func greet(name: String) -> String { "Hello, \(name)!" // single-expression: return is implicit } print(add(x: 3, y: 4)) print(greet(name: "Alice"))
Rust uses fn; Swift uses func. Rust functions return their last expression implicitly; Swift requires an explicit return except in single-expression functions. The crucial difference: Swift callers must write argument labels (add(x: 3, y: 4)), while Rust callers use positional arguments (add(3, 4)).
Argument labels
// Rust: parameter names appear only in the function body. // Callers always use positional arguments. fn move_to(x: f64, y: f64, z: f64) -> String { format!("moved to ({x}, {y}, {z})") } fn main() { println!("{}", move_to(1.0, 2.0, 3.0)); }
// Swift: each parameter has an external label (for callers) and // an internal name (for the body). Use _ to suppress the label. func moveTo(x: Double, y: Double, z: Double) -> String { "moved to (\(x), \(y), \(z))" } func echo(_ text: String, times count: Int) -> String { Array(repeating: text, count: count).joined() } print(moveTo(x: 1.0, y: 2.0, z: 3.0)) print(echo("ha", times: 3))
Argument labels are one of Swift's most distinctive features. Each parameter can have a separate external label (what callers write) and internal name (what the body uses): times count: Int means the caller writes times: 3 while the body uses count. Use _ to suppress a label entirely. This makes call sites read like natural language.
Closures
fn apply<F: Fn(i32) -> i32>(value: i32, transform: F) -> i32 { transform(value) } fn main() { let double = |x: i32| x * 2; let add_ten = |x| x + 10; // type inferred from context println!("{}", apply(5, double)); println!("{}", apply(5, add_ten)); println!("{}", apply(5, |x| x * x)); }
func apply(value: Int, transform: (Int) -> Int) -> Int { transform(value) } let double: (Int) -> Int = { x in x * 2 } let addTen: (Int) -> Int = { $0 + 10 } // $0 = first argument print(apply(value: 5, transform: double)) print(apply(value: 5, transform: addTen)) print(apply(value: 5) { $0 * $0 }) // trailing closure syntax
Rust closures use pipe syntax (|x| body); Swift closures use braces with in ({ x in body }) or the $0/$1 shorthand arguments. When a closure is the last argument, Swift allows trailing closure syntax — the closure moves outside the parentheses. The Rust equivalent is a trailing closure argument but written inside the argument list.
Higher-order functions
fn main() { let words = vec!["apple", "banana", "cherry"]; let long: Vec<_> = words.iter().filter(|w| w.len() > 5).collect(); let upper: Vec<_> = words.iter().map(|w| w.to_uppercase()).collect(); let joined = words.join(", "); println!("{:?}", long); println!("{:?}", upper); println!("{joined}"); }
let words = ["apple", "banana", "cherry"] let long = words.filter { $0.count > 5 } let upper = words.map { $0.uppercased() } let joined = words.joined(separator: ", ") print(long) print(upper) print(joined)
Both languages have map, filter, reduce, and sort/sorted. In Rust these are iterator adapters — they are lazy and chained before a terminal like .collect(). Swift's sequence methods are eager and return new arrays directly. Swift's trailing closure syntax makes chained transforms particularly readable.
Structs
Struct definition & initialization
#[derive(Debug)] struct Point { x: f64, y: f64, } fn main() { let origin = Point { x: 0.0, y: 0.0 }; let moved = Point { x: 3.0, y: 4.0 }; println!("{:?}", origin); println!("x={} y={}", moved.x, moved.y); }
struct Point: CustomStringConvertible { var x: Double var y: Double var description: String { "Point(\(x), \(y))" } } let origin = Point(x: 0.0, y: 0.0) let moved = Point(x: 3.0, y: 4.0) print(origin) print("x=\(moved.x) y=\(moved.y)")
Both structs are value types — assignment copies the value. Rust derives Debug for printing; Swift conforms to CustomStringConvertible with a description computed property (or use CustomDebugStringConvertible for debugDescription). Swift auto-generates a memberwise initializer that uses argument labels; Rust uses field-name syntax.
Methods on structs
struct Circle { radius: f64, } impl Circle { fn new(radius: f64) -> Self { Circle { radius } } fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } fn scale(&mut self, factor: f64) { self.radius *= factor; } } fn main() { let mut circle = Circle::new(5.0); println!("{:.2}", circle.area()); circle.scale(2.0); println!("{:.2}", circle.area()); }
import Foundation struct Circle { var radius: Double func area() -> Double { Double.pi * radius * radius } mutating func scale(by factor: Double) { radius *= factor } } var circle = Circle(radius: 5.0) print(String(format: "%.2f", circle.area())) circle.scale(by: 2.0) print(String(format: "%.2f", circle.area()))
Rust defines methods in a separate impl block; Swift defines them inside the struct braces. Rust distinguishes &self (read) from &mut self (mutate) at the method signature level. Swift uses the mutating keyword on methods that modify the struct's stored properties — identical semantics, different placement. Rust uses :: for associated functions (constructors); Swift uses the memberwise initializer.
Computed properties
struct Rectangle { width: f64, height: f64, } impl Rectangle { fn area(&self) -> f64 { self.width * self.height } fn perimeter(&self) -> f64 { 2.0 * (self.width + self.height) } fn is_square(&self) -> bool { self.width == self.height } } fn main() { let rect = Rectangle { width: 4.0, height: 3.0 }; println!("area={} perimeter={} square={}", rect.area(), rect.perimeter(), rect.is_square()); }
struct Rectangle { var width: Double var height: Double var area: Double { width * height } var perimeter: Double { 2 * (width + height) } var isSquare: Bool { width == height } } let rect = Rectangle(width: 4.0, height: 3.0) print("area=\(rect.area) perimeter=\(rect.perimeter) square=\(rect.isSquare)")
Rust has no computed properties — methods with no parameters serve the same role and are called with parentheses (rect.area()). Swift computed properties look like stored properties at the call site (rect.area, no parentheses), which can make APIs feel more natural for values that are logically a property rather than an action.
Reference types: classes
// Rust has no classes. Reference semantics come from Box<T>, Rc<T>, or Arc<T>. use std::rc::Rc; use std::cell::RefCell; fn main() { let shared = Rc::new(RefCell::new(vec![1, 2, 3])); let clone = Rc::clone(&shared); // same heap allocation clone.borrow_mut().push(4); println!("{:?}", shared.borrow()); // [1, 2, 3, 4] }
// Swift has classes for reference semantics: class Counter { var count = 0 func increment() { count += 1 } } let first = Counter() let second = first // same instance — reference copy second.increment() print(first.count) // 1 — mutation visible via first
Rust achieves shared mutable state through explicit wrapper types (Rc<RefCell<T>> for single-threaded, Arc<Mutex<T>> for multi-threaded). Swift has first-class class types with reference semantics — assignment copies the reference, not the value. Swift manages object lifetimes via ARC (automatic reference counting) rather than a borrow checker.
Enums
Basic enums
#[derive(Debug)] enum Direction { North, South, East, West, } fn main() { let heading = Direction::North; println!("{:?}", heading); match heading { Direction::North => println!("Going north"), Direction::South => println!("Going south"), Direction::East => println!("Going east"), Direction::West => println!("Going west"), } }
enum Direction { case north, south, east, west } let heading = Direction.north print(heading) switch heading { case .north: print("Going north") case .south: print("Going south") case .east: print("Going east") case .west: print("Going west") }
Swift enums use a case keyword for each variant; Rust uses bare names in the enum body. Accessing a variant in Rust requires the type prefix (Direction::North); in Swift, when the type is already known from context, you can use the dot shorthand (.north) — the compiler fills in the type. Both match and switch are exhaustive for enums.
Enums with associated values
#[derive(Debug)] enum Shape { Circle(f64), Rectangle(f64, f64), Triangle { base: f64, height: f64 }, } fn area(shape: &Shape) -> f64 { match shape { Shape::Circle(r) => std::f64::consts::PI * r * r, Shape::Rectangle(w, h) => w * h, Shape::Triangle { base, height } => 0.5 * base * height, } } fn main() { println!("{:.2}", area(&Shape::Circle(5.0))); println!("{:.2}", area(&Shape::Rectangle(4.0, 3.0))); }
import Foundation enum Shape { case circle(Double) case rectangle(Double, Double) case triangle(base: Double, height: Double) } func area(_ shape: Shape) -> Double { switch shape { case .circle(let r): return Double.pi * r * r case .rectangle(let w, let h): return w * h case .triangle(let base, let height): return 0.5 * base * height } } print(String(format: "%.2f", area(.circle(5.0)))) print(String(format: "%.2f", area(.rectangle(4.0, 3.0))))
Enums with associated values are nearly identical in Rust and Swift — both provide true algebraic sum types. The main syntactic difference: Rust patterns use Shape::Circle(r), Swift uses case .circle(let r). Swift labels associated values (triangle(base:height:)) to make patterns more readable. Both compilers enforce exhaustive matching.
Methods on enums
enum Coin { Penny, Nickel, Dime, Quarter, } impl Coin { fn value_cents(&self) -> u32 { match self { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, } } } fn main() { let coins = [Coin::Penny, Coin::Quarter, Coin::Dime]; let total: u32 = coins.iter().map(|c| c.value_cents()).sum(); println!("{total} cents"); }
enum Coin { case penny, nickel, dime, quarter var valueCents: Int { switch self { case .penny: return 1 case .nickel: return 5 case .dime: return 10 case .quarter: return 25 } } } let coins: [Coin] = [.penny, .quarter, .dime] let total = coins.reduce(0) { $0 + $1.valueCents } print("\(total) cents")
Both Rust and Swift enums can carry methods. The pattern is the same: match/switch on self to dispatch on the variant. Swift computed properties (var valueCents: Int { ... }) look particularly clean here because they appear as properties at the call site (coin.valueCents) rather than method calls.
Optionals
Option / Optional
fn find_user(id: u32) -> Option<String> { if id == 1 { Some(String::from("Alice")) } else { None } } fn main() { let found = find_user(1); let not_found = find_user(99); println!("{:?}", found); // Some("Alice") println!("{:?}", not_found); // None println!("{}", found.is_some()); }
func findUser(id: UInt32) -> String? { id == 1 ? "Alice" : nil } let found = findUser(id: 1) let notFound = findUser(id: 99) print(found as Any) // Optional("Alice") print(notFound as Any) // nil print(found != nil)
Rust's Option<T> and Swift's optional type (T?) are the same concept with different syntax. Rust's Some(x) wraps a value; Swift's optional is any non-nil value. Rust's None is Swift's nil. The key difference: Swift integrates optionals into the language syntax so deeply that T? feels like a primitive, while Rust's Option<T> is just a standard library enum.
Unwrapping optionals
fn main() { let maybe: Option<i32> = Some(42); // if let — the Rust and Swift equivalent: if let Some(value) = maybe { println!("Got {value}"); } // unwrap_or for a default: let result = maybe.unwrap_or(0); println!("{result}"); // ? operator (propagate None as early return): // fn double(opt: Option<i32>) -> Option<i32> { Some(opt? * 2) } }
let maybe: Int? = 42 // if let — identical concept, nearly identical syntax: if let value = maybe { print("Got \(value)") } // nil-coalescing operator ?? (like unwrap_or): let result = maybe ?? 0 print(result) // guard let — unwrap or early-return: func doubled(_ opt: Int?) -> Int? { guard let value = opt else { return nil } return value * 2 } print(doubled(maybe) as Any)
The patterns are nearly identical: Rust's if let Some(x) = opt maps to Swift's if let x = opt. Rust's unwrap_or(default) maps to Swift's ?? nil-coalescing operator. Rust's ? operator for propagating None in a function corresponds to Swift's guard let ... else { return nil }. Swift adds guard let to invert the flow and continue in the success branch.
Optional chaining
struct Address { city: String } struct Person { name: String, address: Option<Address> } fn main() { let person = Person { name: String::from("Alice"), address: Some(Address { city: String::from("Paris") }), }; // Manual chaining with and_then: let city = person.address.as_ref().map(|addr| addr.city.as_str()); println!("{:?}", city); // Some("Paris") }
struct Address { var city: String } struct Person { var name: String; var address: Address? } let person = Person( name: "Alice", address: Address(city: "Paris") ) // Optional chaining with ?. — short-circuits on nil: let city = person.address?.city print(city as Any) // Optional("Paris") // Chain multiple levels — any nil short-circuits the whole chain: let uppercase = person.address?.city.uppercased() print(uppercase as Any)
Swift's optional chaining operator (?.) short-circuits the entire chain and returns an optional if any intermediate value is nil — similar to Rust's .as_ref().map(|x| ...) chain but far more concise. The result of any optional chain is itself an optional. Rust lacks this syntax; the equivalent requires explicit .map() or .and_then() calls.
Option combinators
fn main() { let score: Option<i32> = Some(85); // map — transform the inner value: let grade = score.map(|s| if s >= 90 { "A" } else { "B" }); println!("{:?}", grade); // and_then (flatMap) — chain fallible operations: let doubled = score.and_then(|s| if s > 0 { Some(s * 2) } else { None }); println!("{:?}", doubled); // filter — keep Some only if predicate holds: let high = score.filter(|&s| s >= 90); println!("{:?}", high); // None }
let score: Int? = 85 // map — transform the inner value (same name as Rust): let grade = score.map { $0 >= 90 ? "A" : "B" } print(grade as Any) // flatMap (like Rust's and_then) — chain fallible operations: let doubled = score.flatMap { $0 > 0 ? $0 * 2 : nil } print(doubled as Any) // Swift Optional has no filter; use flatMap with a condition: let high: Int? = score.flatMap { $0 >= 90 ? $0 : nil } print(high as Any) // nil
Both languages provide map and flatMap/and_then on optionals. One difference: Rust's Option has a filter method that returns None when the predicate fails; Swift's Optional has no filter — use flatMap { condition ? $0 : nil } instead. The naming diverges: Rust uses and_then where Swift uses flatMap.
Protocols & Traits
Defining protocols / traits
trait Drawable { fn draw(&self); fn bounding_box(&self) -> (f64, f64, f64, f64); // Default implementation: fn describe(&self) { println!("A drawable shape"); } }
protocol Drawable { func draw() func boundingBox() -> (Double, Double, Double, Double) // Default implementation via extension (see protocol extensions): } // Default implementations go in extensions, not in the protocol body: extension Drawable { func describe() { print("A drawable shape") } }
Rust traits and Swift protocols serve the same purpose — defining shared behavior that multiple types can conform to. The syntax is nearly identical. A key structural difference: Rust default method implementations live inside the trait block; Swift default implementations live in a separate extension on the protocol. Both are idiomatic in their respective ecosystems.
Implementing protocols / traits
trait Greet { fn hello(&self) -> String; } struct Person { name: String } impl Greet for Person { fn hello(&self) -> String { format!("Hello, I am {}", self.name) } } fn main() { let alice = Person { name: String::from("Alice") }; println!("{}", alice.hello()); }
protocol Greet { func hello() -> String } struct Person { var name: String } extension Person: Greet { func hello() -> String { "Hello, I am \(name)" } } let alice = Person(name: "Alice") print(alice.hello())
Rust implements a trait with impl TraitName for TypeName { ... }; Swift conforms a type to a protocol with extension TypeName: ProtocolName { ... }. Both allow implementing protocols/traits for types defined elsewhere — this is called retroactive conformance (Rust: orphan rules apply; Swift: same restriction). Swift often places conformances in separate extension blocks for clarity.
Dynamic dispatch
trait Animal { fn sound(&self) -> &str; } struct Dog; struct Cat; impl Animal for Dog { fn sound(&self) -> &str { "woof" } } impl Animal for Cat { fn sound(&self) -> &str { "meow" } } fn main() { // dyn Animal — dynamic dispatch via trait object (fat pointer): let animals: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)]; for animal in &animals { println!("{}", animal.sound()); } }
protocol Animal { func sound() -> String } struct Dog: Animal { func sound() -> String { "woof" } } struct Cat: Animal { func sound() -> String { "meow" } } // Swift uses existential types (any Animal) for dynamic dispatch: let animals: [any Animal] = [Dog(), Cat()] for animal in animals { print(animal.sound()) }
Rust uses dyn Trait (a fat pointer = data pointer + vtable pointer) for runtime polymorphism. Swift uses existentials — spelled any Protocol in Swift 5.7+ — which work similarly. Rust requires explicit boxing (Box<dyn Trait>) for heap allocation; Swift handles this automatically. For static dispatch, both use generics with trait/protocol bounds instead.
Associated types
trait Container { type Item; fn first(&self) -> Option<&Self::Item>; fn len(&self) -> usize; fn is_empty(&self) -> bool { self.len() == 0 } } struct Stack<T>(Vec<T>); impl<T> Container for Stack<T> { type Item = T; fn first(&self) -> Option<&T> { self.0.first() } fn len(&self) -> usize { self.0.len() } } fn main() { let stack = Stack(vec![1, 2, 3]); println!("{:?}", stack.first()); println!("{}", stack.len()); }
protocol Container { associatedtype Item func first() -> Item? var count: Int { get } var isEmpty: Bool { get } } struct Stack<T>: Container { private var items: [T] = [] typealias Item = T mutating func push(_ item: T) { items.append(item) } func first() -> T? { items.first } var count: Int { items.count } var isEmpty: Bool { items.isEmpty } } var stack = Stack<Int>() stack.push(1); stack.push(2); stack.push(3) print(stack.first() as Any) print(stack.count)
Rust's type Item; in a trait corresponds exactly to Swift's associatedtype Item in a protocol. Both let you write generic protocols/traits without specifying the concrete type upfront. The conforming type specifies the concrete type: Rust uses type Item = T;, Swift uses typealias Item = T (or infers it from usage).
Generics
Generic functions
fn largest<T: PartialOrd>(list: &[T]) -> &T { let mut max = &list[0]; for item in list { if item > max { max = item; } } max } fn main() { let numbers = vec![34, 50, 25, 100, 65]; let chars = vec!['y', 'm', 'a', 'q']; println!("{}", largest(&numbers)); println!("{}", largest(&chars)); }
func largest<T: Comparable>(_ list: [T]) -> T { var max = list[0] for item in list { if item > max { max = item } } return max } let numbers = [34, 50, 25, 100, 65] let chars: [Character] = ["y", "m", "a", "q"] print(largest(numbers)) print(largest(chars))
Generic syntax is nearly identical: <T: Bound> in both languages. Rust's PartialOrd maps to Swift's Comparable. Rust also requires PartialEq for ==, while Swift's Equatable implies equality. One difference: Rust allows multiple bounds with + (T: Debug + Clone); Swift uses commas or where clauses. Rust passes slices (&[T]); Swift passes arrays ([T]).
Generic structs
struct Pair<T> { first: T, second: T, } impl<T: std::fmt::Display> Pair<T> { fn new(first: T, second: T) -> Self { Pair { first, second } } fn display(&self) { println!("({}, {})", self.first, self.second); } } fn main() { let pair = Pair::new(5, 10); pair.display(); let words = Pair::new("hello", "world"); words.display(); }
struct Pair<T: CustomStringConvertible> { var first: T var second: T func display() { print("(\(first), \(second))") } } let pair = Pair(first: 5, second: 10) let words = Pair(first: "hello", second: "world") pair.display() words.display()
Generic structs use the same <T> parameter syntax in both languages. In Rust, method implementations that need bounds on T are written in a separate impl<T: Bound> TypeName<T> block. In Swift, the constraint goes on the type parameter directly (struct Pair<T: Comparable>). Bounds can be deferred to the protocol conformance declaration in Swift using extensions.
Where clauses
use std::fmt::{Debug, Display}; fn print_pair<T, U>(first: T, second: U) where T: Display + Debug, U: Display + Clone, { println!("first={first} second={second}"); } fn main() { print_pair(42, "hello"); print_pair(3.14, "world"); }
func printPair<T, U>(first: T, second: U) where T: CustomStringConvertible, U: CustomStringConvertible { print("first=\(first) second=\(second)") } printPair(first: 42, second: "hello") printPair(first: 3.14, second: [1, 2, 3])
where clauses exist in both languages with nearly identical syntax — place them after the signature to list constraints on type parameters. Both support multiple constraints per type parameter. Rust uses + to chain multiple trait bounds (T: Display + Debug); Swift uses commas only in where clauses and & for protocol composition in type positions.
Error Handling
Result type
#[derive(Debug)] enum ParseError { InvalidInput(String) } fn parse_age(input: &str) -> Result<u32, ParseError> { input.trim().parse::<u32>() .map_err(|_| ParseError::InvalidInput(input.to_string())) } fn main() { println!("{:?}", parse_age("25")); // Ok(25) println!("{:?}", parse_age("oops")); // Err(InvalidInput("oops")) }
enum ParseError: Error { case invalidInput(String) } func parseAge(_ input: String) -> Result<UInt32, ParseError> { guard let value = UInt32(input) else { return .failure(.invalidInput(input)) } return .success(value) } print(parseAge("25")) // success(25) print(parseAge("oops")) // failure(invalidInput("oops"))
Swift has both a Result<Success, Failure> type (identical in purpose to Rust's) and a throws mechanism. The Result type uses .success and .failure cases (Swift dot-shorthand) vs. Rust's Ok and Err. Both let you pattern-match on the result or chain operations with .map and .flatMap.
Error propagation: ? vs. try
use std::num::ParseIntError; fn double_first(vec: &[&str]) -> Result<i32, ParseIntError> { let first = vec.first().ok_or("empty".parse::<i32>().unwrap_err())?; let parsed = first.parse::<i32>()?; // ? propagates error Ok(parsed * 2) } fn main() { println!("{:?}", double_first(&["5", "10"])); println!("{:?}", double_first(&["oops"])); }
import Foundation enum MathError: Error { case emptyInput, notANumber(String) } func doubleFirst(_ items: [String]) throws -> Int { guard let first = items.first else { throw MathError.emptyInput } guard let value = Int(first) else { throw MathError.notANumber(first) } return value * 2 } do { print(try doubleFirst(["5", "10"])) } catch { print("Error: \(error)") }
Rust's ? operator propagates Err values as early returns from functions returning Result. Swift's try keyword marks a call that might throw, and Swift automatically propagates the thrown error to the caller. Swift's throws functions don't encode the error type in the signature (unlike Rust's Result<T, E>) — typed throws (throws(MyError)) were added in Swift 6.
do-catch / match on errors
#[derive(Debug)] enum AppError { Network(String), Parse(String) } fn fetch(url: &str) -> Result<String, AppError> { if url.starts_with("http") { Ok(format!("data from {url}")) } else { Err(AppError::Network(format!("bad url: {url}"))) } } fn main() { match fetch("https://example.com") { Ok(data) => println!("Got: {data}"), Err(AppError::Network(msg)) => println!("Network error: {msg}"), Err(AppError::Parse(msg)) => println!("Parse error: {msg}"), } }
enum AppError: Error { case network(String) case parse(String) } func fetch(url: String) throws -> String { guard url.hasPrefix("http") else { throw AppError.network("bad url: \(url)") } return "data from \(url)" } do { let data = try fetch(url: "https://example.com") print("Got: \(data)") } catch AppError.network(let msg) { print("Network error: \(msg)") } catch AppError.parse(let msg) { print("Parse error: \(msg)") } catch { print("Unknown error: \(error)") }
Rust's match on a Result and Swift's do-catch both provide exhaustive error handling with pattern matching. A key difference: in Rust you match on the return value; in Swift the error is thrown through a separate channel (similar to exceptions, but required to be explicitly handled). Swift's catch patterns use the same syntax as switch patterns.
Custom error types
use std::fmt; #[derive(Debug)] struct ValidationError { field: String, message: String, } impl fmt::Display for ValidationError { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "Validation error on '{}': {}", self.field, self.message) } } impl std::error::Error for ValidationError {} fn main() { let error = ValidationError { field: String::from("age"), message: String::from("must be positive") }; println!("{error}"); println!("{error:?}"); }
struct ValidationError: Error, CustomStringConvertible { var field: String var message: String var description: String { "Validation error on '\(field)': \(message)" } } let error = ValidationError(field: "age", message: "must be positive") print(error) print(error.description)
Rust requires implementing std::fmt::Display and std::error::Error for a custom error type; Swift requires conforming to Error (a protocol with no required methods) and optionally CustomStringConvertible for readable messages. Swift's approach is more concise; Rust's is more explicit about the formatting contract. The thiserror crate reduces Rust boilerplate significantly.
Memory & Ownership
Ownership model
fn main() { let data = String::from("hello"); // Move: ownership transfers, original is no longer valid: let moved = data; // data is moved, not copied // println!("{data}"); // ❌ compile error — value moved println!("{moved}"); // Clone for an explicit deep copy: let original = String::from("world"); let clone = original.clone(); println!("{original} {clone}"); }
var data = "hello" // Swift String is a value type with copy-on-write: var moved = data // logical copy — data remains valid moved.append("!") print(data) // "hello" — unchanged print(moved) // "hello!" // Classes are reference types — assignment copies the reference: class Container { var value = "original" } let first = Container() let second = first // same object second.value = "changed" print(first.value) // "changed"
Rust's borrow checker enforces single ownership at compile time — moving a value invalidates the original. Swift uses value semantics with copy-on-write for structs and strings: copies are efficient (no actual copy until mutation). For reference semantics, Swift uses class types with ARC. The practical difference: Rust prevents memory bugs at compile time; Swift prevents them at runtime via ARC.
Borrowing and inout parameters
fn add_one(value: &mut i32) { *value += 1; } fn print_value(value: &i32) { println!("{value}"); } fn main() { let mut number = 5_i32; print_value(&number); // immutable borrow — read only add_one(&mut number); // mutable borrow — write println!("{number}"); }
func addOne(_ value: inout Int) { value += 1 } func printValue(_ value: Int) { print(value) } var number = 5 printValue(number) // pass by value — copy addOne(&number) // inout — pass by reference (& at call site) print(number)
Rust's &T (immutable borrow) and &mut T (mutable borrow) correspond to Swift's pass-by-value (the default) and inout parameters. Both use & at the call site for mutable/inout arguments. The borrow checker enforces that only one mutable borrow or any number of immutable borrows can exist at a time; Swift has no equivalent compile-time enforcement.
Reference counting
use std::sync::Arc; use std::thread; fn main() { // Arc: atomically-reference-counted, safe to share across threads: let shared = Arc::new(vec![1, 2, 3]); let clone1 = Arc::clone(&shared); let handle = thread::spawn(move || { println!("thread sees: {:?}", clone1); }); handle.join().unwrap(); println!("main sees: {:?}", shared); }
// Swift uses ARC automatically — no explicit Arc::new needed. // Classes are always reference-counted; just use them: class SharedData { var values: [Int] init(_ values: [Int]) { self.values = values } } let shared = SharedData([1, 2, 3]) let alias = shared // same object, ARC count = 2 print(alias.values) // [1, 2, 3] shared.values.append(4) print(alias.values) // [1, 2, 3, 4] — same object
Rust's Arc<T> (atomically reference-counted shared pointer) and Rc<T> (single-thread) are explicit opt-in wrappers. Swift applies ARC transparently to all class instances — there is no Arc::new() call. Swift's ARC increments the count on assignment and decrements on scope exit, freeing memory when the count reaches zero. Rust achieves the same effect manually; the borrow checker prevents the retain-cycle bugs that plague Swift (though Swift has weak/unowned to break cycles).
Value semantics vs. reference semantics
#[derive(Clone)] struct Counter { count: u32 } fn increment(counter: &mut Counter) { counter.count += 1; } fn main() { let mut original = Counter { count: 0 }; let mut copy = original.clone(); // explicit deep copy increment(&mut original); println!("original={} copy={}", original.count, copy.count); // copy is unaffected because it is an independent value copy.count = 99; println!("original={} copy={}", original.count, copy.count); }
// Struct: value semantics — assignment always copies struct Counter { var count: Int } var original = Counter(count: 0) var copy = original // implicit copy — independent value original.count += 1 print("original=\(original.count) copy=\(copy.count)") // Class: reference semantics — assignment copies the reference class RefCounter { var count = 0 } let refA = RefCounter() let refB = refA // same object refA.count += 1 print("refA=\(refA.count) refB=\(refB.count)")
Swift makes the value vs. reference distinction explicit at the type-definition level: struct always has value semantics (assignment copies), class always has reference semantics. Rust's types are all value types unless you opt into heap allocation (Box, Rc, Arc). Swift's copy-on-write optimization means the actual copy of large data is deferred until a write happens.
Async & Concurrency
async / await
// Rust async requires a runtime (e.g., tokio): // [dependencies] // tokio = { version = "1", features = ["full"] } // #[tokio::main] // async fn main() { // let result = fetch_data("https://api.example.com").await; // println!("{:?}", result); // } // async fn fetch_data(url: &str) -> Result<String, reqwest::Error> { // reqwest::get(url).await?.text().await // } // Rust async is zero-cost: no heap allocation until .await points.
// Swift async uses structured concurrency built into the language: // async func greet(name: String) async -> String { // return "Hello, \(name)!" // } // @main struct App { // static func main() async { // let message = await greet(name: "Swift") // print(message) // } // } // Swift async functions run on a cooperative thread pool managed by the runtime.
Both Rust and Swift use async/await syntax. Rust async is zero-cost and requires an external runtime (tokio, async-std); Swift async is built into the language runtime using a cooperative thread pool. The key architectural difference: Rust uses Send + Sync traits to enforce data-race safety across thread boundaries; Swift uses the actor model and the Sendable protocol. These examples are shown as comments because the execution environment does not support async entry points.
Actors (Swift) vs. Mutex (Rust)
use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0_u32)); let mut handles = vec![]; for _ in 0..5 { let counter = Arc::clone(&counter); handles.push(thread::spawn(move || { let mut value = counter.lock().unwrap(); *value += 1; })); } for handle in handles { handle.join().unwrap(); } println!("count = {}", counter.lock().unwrap()); }
// Swift actor — the compiler enforces exclusive access to mutable state: // actor BankAccount { // private var balance: Double = 0 // // func deposit(amount: Double) { // balance += amount // safe — only one task accesses actor at a time // } // // func getBalance() -> Double { balance } // } // // Task { // let account = BankAccount() // await account.deposit(amount: 100) // print(await account.getBalance()) // } // Shown as comments: actors require async context to run.
Rust protects shared mutable state with Mutex<T> (explicit locking) combined with Arc for shared ownership. Swift's actor is a built-in language construct that automatically serializes access to its mutable state — no explicit locks needed. The compiler enforces that you await every actor method call, making the isolation boundary visible at call sites. This is conceptually similar to Rust's ownership rules, but enforced at the language level for concurrency.
Structured concurrency
// Rust with tokio: // use tokio::task; // #[tokio::main] // async fn main() { // let handle1 = task::spawn(async { compute(1).await }); // let handle2 = task::spawn(async { compute(2).await }); // let (result1, result2) = tokio::join!(handle1, handle2); // println!("{:?} {:?}", result1, result2); // } // async fn compute(n: u32) -> u32 { n * n } // tokio::join! runs tasks concurrently and waits for all to complete.
// Swift structured concurrency with async let: // async let result1 = compute(1) // async let result2 = compute(2) // let (r1, r2) = await (result1, result2) // print(r1, r2) // // func compute(_ n: Int) async -> Int { n * n } // // Or use a TaskGroup for a dynamic number of tasks: // await withTaskGroup(of: Int.self) { group in // for number in 1...5 { // group.addTask { await compute(number) } // } // for await result in group { print(result) } // } // Shown as comments: requires async entry point.
Rust's tokio::join! macro runs multiple futures concurrently and collects results — analogous to Swift's async let syntax. Swift's structured concurrency (async let, withTaskGroup) is built into the language; Rust's equivalent is provided by runtime libraries (tokio, async-std). Both approaches ensure that spawned tasks are completed or canceled before the enclosing scope exits.