Basics & Syntax
Hello, World
print("Hello, World!") println!("Hello, World!"); The exclamation mark is the first thing to notice:
println! is a macro, not a function, so the format string is checked against its arguments at compile time. Rust programs start at fn main(), and the examples on this page show just its body — Swift, uniquely among compiled languages, lets top-level statements be the program, so the left column needs no wrapper either.let and var become let and let mut
Same two ideas, one keyword. Rust marks mutability on the binding rather than choosing a different keyword for it.
let fixed = 1
var counter = 0
counter += 1
let explicit: Int64 = 42
let inferred = "a string"
print(fixed + counter, explicit, inferred) let fixed = 1;
let mut counter = 0; // 'mut' is the var
counter += 1;
let explicit: i64 = 42;
let inferred = "a string";
println!("{} {} {}", fixed + counter, explicit, inferred);
// Shadowing: 'let' again with the same name is a NEW binding, and it may even
// change the type. This is idiomatic, not a mistake.
let inferred = inferred.len();
println!("{}", inferred); Immutable by default in both, which is one of the many places these two languages agree. The new habit is shadowing: re-declaring a name with
let creates a fresh binding that may have a different type, which Rust programmers use constantly to convert a value through a pipeline (let input = input.trim(); let input: u32 = input.parse()?;) without inventing three names. Swift has nothing like it — a second let input in the same scope is an error.Functions, and the missing argument labels
// Argument labels are part of the name: greet(name:salutation:)
func greet(name: String, salutation: String = "Hello") -> String {
return "\(salutation), \(name)!"
}
// _ removes the label at the call site.
func double(_ number: Int) -> Int {
number // an implicit return, for a single expression
}
print(greet(name: "Ada"))
print(greet(name: "Ada", salutation: "Hi"))
print(double(21) * 2) // No argument labels, and NO DEFAULT ARGUMENTS. Rust has neither.
fn greet(name: &str, salutation: &str) -> String {
format!("{}, {}!", salutation, name) // the last expression is the return
}
// The two-arity convention: a short name for the common case, wrapping the full one.
fn greet_default(name: &str) -> String {
greet(name, "Hello")
}
fn double(number: i32) -> i32 {
number * 2 // no semicolon = this is the return value
}
println!("{}", greet_default("Ada"));
println!("{}", greet("Ada", "Hi"));
println!("{}", double(21)); Two Swift conveniences are simply gone. There are no argument labels, so a call is positional and
connect(host, 8080, true) tells the reader nothing (the community answer is a builder or an options struct — the same pattern Java reaches for). And there are no default arguments, so you write the extra function. What Rust gives back is the expression-oriented body: the final expression without a semicolon is the return value, and a stray semicolon turns it into a statement returning () — which is the confusing early compile error you will hit within an hour.Ownership: Copies Become Moves
let second = first — the most different line on this page
Swift structs are value types: assignment copies (lazily, via copy-on-write). Rust moves: assignment transfers ownership, and the original name becomes unusable.
var first = [1, 2, 3]
var second = first // a COPY (deferred until one of them is written to)
second.append(4)
print(first) // [1, 2, 3] — untouched
print(second) // [1, 2, 3, 4]
// Both names are still valid. Copy-on-write means the copy cost was only paid
// because we mutated one of them.
print(first.count, second.count) let first = vec![1, 2, 3];
let second = first; // a MOVE: ownership of the heap buffer transfers
// println!("{:?}", first);
// ^ will not compile: "borrow of moved value: 'first'"
// There is exactly one owner, and it is now 'second'.
println!("{:?}", second);
// To keep both, ask for the copy explicitly. It is never implicit and never
// hidden, so an allocation always appears in the source.
let original = vec![1, 2, 3];
let mut duplicate = original.clone();
duplicate.push(4);
println!("{:?} {:?}", original, duplicate);
// Small Copy types (integers, bools, chars, and structs of them) are copied
// implicitly, because copying them is trivial.
let number = 5;
let another = number;
println!("{} {}", number, another); This is the line where the two languages part company, and everything else in Rust follows from it. A value has exactly one owner; assignment (and passing to a function, and returning) transfers that ownership, and using the old name afterwards is a compile error. Swift reaches the same safety by copying value types and reference-counting the rest, at run time. Rust does it by proving ownership at compile time — so the buffer is freed exactly once, at exactly the point the owner goes out of scope, with no retain, no release, no atomic, and no deferred cost. What it takes from you is the freedom to say
let second = first and keep using first. When you want that, you say .clone(), and the cost is right there in the source where a reviewer can see it.Passing a value gives it away
func describe(_ items: [Int]) -> Int {
// 'items' is a copy (COW: no allocation unless it is mutated).
items.count
}
var numbers = [1, 2, 3]
print(describe(numbers))
print(numbers) // still ours, still usable
// inout passes a mutable reference, and the write is visible to the caller.
func append(_ items: inout [Int], _ value: Int) {
items.append(value)
}
append(&numbers, 4)
print(numbers) // Taking the value BY VALUE consumes it: the caller can no longer use it.
fn consume(items: Vec<i32>) -> usize {
items.len()
}
// Borrowing (&) reads it without taking it — this is what you usually want.
fn describe(items: &Vec<i32>) -> usize {
items.len()
}
// &mut is Swift's 'inout': a temporary, exclusive, mutable loan.
fn append(items: &mut Vec<i32>, value: i32) {
items.push(value);
}
let mut numbers = vec![1, 2, 3];
println!("{}", describe(&numbers)); // borrowed: numbers is still ours
append(&mut numbers, 4); // borrowed mutably, then given back
println!("{:?}", numbers);
println!("{}", consume(numbers)); // MOVED: numbers is gone after this
// println!("{:?}", numbers); ← will not compile The signature tells you what the function does with your value, which is information a Swift signature simply does not carry.
fn consume(items: Vec<i32>) takes the vector — the caller loses it. &Vec<i32> borrows it to read. &mut Vec<i32> borrows it to write, and is exactly Swift's inout (right down to the & at the call site). The rule of thumb when writing Rust: borrow by default, and take ownership only when you genuinely need to keep or destroy the value.Borrowing & Lifetimes
Many readers, or one writer — never both
Swift allows any amount of aliasing: several references may point at the same object and all of them may write to it. Rust does not, and this rule is the borrow checker.
class Counter {
var count = 0
}
let counter = Counter()
let alias = counter // two references to the same object
counter.count += 1
alias.count += 1 // both can write; nothing objects
print(counter.count) // 2
// The danger this permits: mutating a collection while iterating it is a
// runtime trap (or worse, in a language without Swift's checks).
var numbers = [1, 2, 3]
for number in numbers {
print(number)
// numbers.append(number) ← would loop forever in many languages
} let mut count = 0;
// Any number of SHARED borrows (&) may exist at once — they can only read.
let first_reader = &count;
let second_reader = &count;
println!("{} {}", first_reader, second_reader);
// Or ONE exclusive borrow (&mut) — which can write, and excludes all others.
let writer = &mut count;
*writer += 1;
println!("{}", writer);
// The two cannot overlap:
// let reader = &count;
// let writer = &mut count; ← "cannot borrow as mutable because it is
// println!("{}", reader); also borrowed as immutable"
println!("{}", count);
// And so this is a compile error, not a runtime surprise:
let mut numbers = vec![1, 2, 3];
for number in &numbers { // the loop holds a shared borrow...
println!("{}", number);
// numbers.push(*number); ← ...so pushing (a &mut borrow) will not compile
}
numbers.push(4); // fine here: the loop's borrow has ended
println!("{:?}", numbers); One writer or many readers, checked at compile time. That single rule buys you the elimination of an entire class of bugs — iterator invalidation, use-after-free, and data races are all the same mistake (mutation through one alias while another alias is live), and none of them can be written. Swift catches the memory-safety half of this with ARC and exclusivity checks at run time, and simply permits the rest. The cost is the one everybody complains about: the borrow checker rejects programs that are actually fine, and learning to restructure code so it agrees is the real Rust learning curve. It gets shorter than you expect.
Lifetimes: the idea you have never needed
The genuinely new concept. Rust needs to know that a reference does not outlive what it points at — and where it cannot infer that, you tell it.
// Swift never asks. ARC keeps the object alive as long as any strong reference
// exists, so returning a reference into a value you were handed is always safe:
// the referent's lifetime is managed at run time.
func longest(_ first: String, _ second: String) -> String {
first.count > second.count ? first : second
}
let winner = longest("hello", "hi")
print(winner) // 'a is a LIFETIME PARAMETER. It says: the returned reference lives at least as
// long as BOTH inputs — so the compiler can prove the caller never holds a
// dangling pointer. It is a constraint, not an allocation: nothing runs at all.
fn longest<'a>(first: &'a str, second: &'a str) -> &'a str {
if first.len() > second.len() { first } else { second }
}
let first = String::from("hello");
let winner;
{
let second = String::from("hi");
winner = longest(first.as_str(), second.as_str());
println!("{}", winner); // fine: 'second' is still alive here
}
// println!("{}", winner);
// ^ will NOT compile: 'second' was dropped at the brace, and 'winner' may
// point at it. Swift would have kept the string alive; Rust proves you
// cannot look at freed memory instead.
// Most of the time you write no lifetimes at all — the compiler infers them.
fn first_word(text: &str) -> &str {
text.split(' ').next().unwrap_or("")
}
println!("{}", first_word("hello world")); A lifetime is not a thing that exists at run time; it is a constraint the compiler checks and then erases.
<'a> on the signature above says "the reference I return is valid for as long as the shorter of my two inputs", which is exactly the fact ARC establishes dynamically by keeping the object alive. Rust establishes it statically and frees the memory at a point it can prove is safe — which is why there is no reference counting, no atomic increment, and no deallocation happening at an unpredictable moment. The good news for a Swift developer: lifetime elision means you almost never write one. The bad news: when you do have to, the error message that sent you there is the hardest one in the language.ARC vs Ownership
ARC becomes Rc — and it is opt-in
Every Swift class instance is reference-counted, automatically, whether you need sharing or not. In Rust, reference counting is a type you choose.
class Node {
let name: String
init(name: String) { self.name = name }
deinit { print("freeing \(name)") }
}
func scope() {
let first = Node(name: "shared") // refcount 1
let second = first // refcount 2 — an atomic increment
print(second.name)
} // both go out of scope, refcount hits 0, deinit runs
scope()
print("done") use std::rc::Rc;
struct Node {
name: String,
}
// Drop is deinit.
impl Drop for Node {
fn drop(&mut self) {
println!("freeing {}", self.name);
}
}
{
// Rc = Reference Counted. You asked for it explicitly, so you know the cost.
let first = Rc::new(Node { name: String::from("shared") });
let second = Rc::clone(&first); // refcount 2 — a non-atomic increment
println!("{} (count {})", second.name, Rc::strong_count(&first));
} // both dropped, count hits 0, Drop runs
// Most values need NO reference counting at all: a single owner, freed at the
// closing brace, with no counter anywhere.
{
let owned = Node { name: String::from("owned") };
println!("{}", owned.name);
}
println!("done"); The default is inverted. Every Swift class pays for reference counting — atomic increments and decrements, on every assignment, forever — because the compiler cannot know whether you will share it. Rust's default is a single owner with zero runtime cost, and shared ownership is a type you opt into:
Rc for one thread (a plain counter) or Arc for several (an atomic one, the exact cost Swift always pays). deinit becomes the Drop trait, and it runs at a point the compiler determined statically rather than when a counter happens to hit zero.weak var becomes Weak — and shared mutation gets loud
class Parent {
var child: Child?
deinit { print("parent freed") }
}
class Child {
// Without 'weak', parent and child would retain each other and NEITHER
// would ever be freed. Nothing in the type system warns you: it is a
// silent leak, and finding it means running Instruments.
weak var parent: Parent?
deinit { print("child freed") }
}
func build() {
let parent = Parent()
let child = Child()
parent.child = child
child.parent = parent
}
build()
print("done") use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Parent {
child: RefCell<Option<Rc<Child>>>,
}
struct Child {
// Weak is Swift's 'weak': it does not keep the parent alive.
parent: RefCell<Weak<Parent>>,
}
impl Drop for Parent {
fn drop(&mut self) { println!("parent freed"); }
}
impl Drop for Child {
fn drop(&mut self) { println!("child freed"); }
}
{
let parent = Rc::new(Parent { child: RefCell::new(None) });
let child = Rc::new(Child { parent: RefCell::new(Weak::new()) });
*parent.child.borrow_mut() = Some(Rc::clone(&child));
*child.parent.borrow_mut() = Rc::downgrade(&parent); // the weak link
println!("parent still reachable from child: {}", child.parent.borrow().upgrade().is_some());
}
println!("done"); Rust has the same escape hatch (
Weak, obtained with Rc::downgrade, and turned back into a real reference with .upgrade(), which returns an Option because the target may be gone). It also has the same hole: an Rc cycle leaks, and the compiler will not catch it. What is different is how much noise Rust makes about shared mutable state on the way there — Rc<RefCell<T>> is deliberately ugly, because RefCell moves the borrow check from compile time to run time (it panics if you take two mutable borrows at once). That ugliness is a signal, and the usual Rust response is to redesign the data structure — an arena with indices, or a tree that owns its children one way — rather than to reach for Rc at all. A Swift graph of mutually referencing classes is often the wrong shape for Rust entirely.Optionals & Option
Optional<T> becomes Option<T> — the same idea, without the sugar
func findName(_ id: Int) -> String? {
id == 1 ? "Ada" : nil
}
// Optional chaining, nil-coalescing, and the force unwrap.
print(findName(1)?.uppercased() ?? "(none)")
print(findName(99)?.uppercased() ?? "(none)")
print(findName(1)!)
// if let / guard let bind the unwrapped value.
if let name = findName(1) {
print("found \(name)")
}
func shout(_ id: Int) -> String {
guard let name = findName(id) else {
return "(nobody)"
}
return name.uppercased()
}
print(shout(99)) fn find_name(id: i32) -> Option<String> {
if id == 1 { Some(String::from("Ada")) } else { None }
}
// There is no '?.' and no '??'. They are methods, because Option is an ordinary
// enum rather than a compiler feature.
println!("{}", find_name(1).map(|name| name.to_uppercase()).unwrap_or(String::from("(none)")));
println!("{}", find_name(99).map(|name| name.to_uppercase()).unwrap_or(String::from("(none)")));
println!("{}", find_name(1).unwrap()); // the force unwrap: panics on None
println!("{}", find_name(99).unwrap_or_default());
// if let is if let.
if let Some(name) = find_name(1) {
println!("found {}", name);
}
// let ... else is guard let, right down to the requirement that the else block
// diverges (return / break / panic).
fn shout(id: i32) -> String {
let Some(name) = find_name(id) else {
return String::from("(nobody)");
};
name.to_uppercase()
}
println!("{}", shout(99)); Almost a straight port:
Optional<T> is Option<T>, if let is if let, and guard let … else is let … else — including the rule that the else branch must diverge. The difference is that Swift's optionals come with dedicated syntax (?., ??, !, ?!), while Rust's Option is a plain enum from the standard library, so the syntax becomes methods: map for optional chaining, unwrap_or for ??, unwrap() for the force unwrap (and expect("why"), which is the same thing with a message — always prefer it). Being an ordinary type has a real payoff: Option composes with generic code, and you can write your own types that behave exactly like it.Errors: throws vs Result
throws becomes Result, and try becomes ?
Swift already made errors part of the signature. Rust goes one step further and makes them part of the return type — a failure is a value you hold, not a side channel you catch.
enum ParseError: Error {
case notANumber(String)
case notPositive(Int)
}
func parsePositive(_ text: String) throws -> Int {
guard let number = Int(text) else {
throw ParseError.notANumber(text)
}
guard number > 0 else {
throw ParseError.notPositive(number)
}
return number
}
// 'try' marks each call that can fail; do/catch handles it.
do {
print(try parsePositive("42"))
print(try parsePositive("-1"))
} catch ParseError.notPositive(let number) {
print("not positive: \(number)")
} catch {
print("failed: \(error)")
}
// try? turns it into an Optional.
print(try? parsePositive("nope") ?? -1) #[derive(Debug)]
enum ParseError {
NotANumber(String),
NotPositive(i32),
}
// The failure is IN the return type. No 'throws', and no invisible side channel.
fn parse_positive(text: &str) -> Result<i32, ParseError> {
let number: i32 = text
.parse()
.map_err(|_| ParseError::NotANumber(String::from(text)))?; // ? propagates
if number <= 0 {
return Err(ParseError::NotPositive(number));
}
Ok(number)
}
// The ? operator is Swift's 'try': on Err it returns early, on Ok it unwraps.
fn run() -> Result<(), ParseError> {
println!("{}", parse_positive("42")?);
println!("{}", parse_positive("-1")?); // returns Err from here
Ok(())
}
// And handling it is a match — because a Result is just a value.
match run() {
Ok(()) => println!("all good"),
Err(ParseError::NotPositive(number)) => println!("not positive: {}", number),
Err(other) => println!("failed: {:?}", other),
}
// .ok() turns a Result into an Option — Swift's 'try?'.
println!("{}", parse_positive("nope").ok().unwrap_or(-1)); The mapping is direct —
throws becomes -> Result<T, E>, try becomes the postfix ?, try? becomes .ok(), and do/catch becomes a match — and the consequences of a failure being an ordinary value run deeper than the syntax. A Result can be stored in a struct, collected into a Vec, mapped over, or returned from a closure, none of which a thrown Swift error can do. It also means the error type is part of the signature (Swift only got typed throws recently, and it is rarely used), so a caller knows precisely what can go wrong. The idiomatic escape from writing that type by hand is a library — thiserror to define errors, anyhow when you just want one type that swallows everything.fatalError becomes panic!
func divide(_ numerator: Int, by denominator: Int) -> Int {
precondition(denominator != 0, "denominator must not be zero")
return numerator / denominator
}
print(divide(10, by: 2))
let numbers = [1, 2, 3]
// print(numbers[10]) ← a runtime trap: "Index out of range"
print(numbers.indices.contains(10) ? numbers[10] : -1)
// fatalError is the unrecoverable exit — and it cannot be caught.
func unreachable() -> Never {
fatalError("this should never happen")
} fn divide(numerator: i32, denominator: i32) -> i32 {
assert!(denominator != 0, "denominator must not be zero");
numerator / denominator
}
println!("{}", divide(10, 2));
let numbers = vec![1, 2, 3];
// println!("{}", numbers[10]); ← panics: "index out of bounds"
// The non-panicking accessor returns an Option. This is the Rust reflex: the
// safe version hands you a value you must deal with.
println!("{}", numbers.get(10).copied().unwrap_or(-1));
// panic! is fatalError. It unwinds the thread and, by default, aborts the
// program — it is for a broken invariant (a BUG), never for expected failure.
let result = std::panic::catch_unwind(|| {
panic!("this should never happen");
});
println!("panicked, and we caught it: {}", result.is_err()); The distinction is the same one Swift draws and states more plainly:
Result is for failure you expect (bad input, missing file), panic! is for a bug (a broken invariant, an index that cannot be out of range). panic! is fatalError, assert! is precondition, and unreachable!() exists too. catch_unwind can technically catch a panic, but using it as an exception handler is a well-known anti-pattern — a panicked thread may have left data in a half-updated state, which is precisely what the type system was preventing everywhere else. Note the .get(10) reflex in the right column: for almost every panicking operation, Rust also offers a version that returns an Option or a Result, and idiomatic code reaches for that one.Enums & Pattern Matching
Enums with associated values — nearly identical
The place these two languages agree most completely. Both have real sum types, and both make you handle every case.
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case point
}
func area(_ shape: Shape) -> Double {
// Exhaustive: omit a case and it will not compile.
switch shape {
case .circle(let radius):
return 3.14159 * radius * radius
case .rectangle(let width, let height):
return width * height
case .point:
return 0
}
}
print(area(.circle(radius: 1)))
print(area(.rectangle(width: 2, height: 3)))
print(area(.point)) enum Shape {
Circle { radius: f64 }, // named fields, like Swift's labels
Rectangle { width: f64, height: f64 },
Point, // a case with no payload
}
fn area(shape: &Shape) -> f64 {
// Exhaustive: omit an arm and it will not compile.
match shape {
Shape::Circle { radius } => 3.14159 * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Point => 0.0,
}
}
println!("{}", area(&Shape::Circle { radius: 1.0 }));
println!("{}", area(&Shape::Rectangle { width: 2.0, height: 3.0 }));
println!("{}", area(&Shape::Point)); This is a rename, not a translation:
switch is match, case .circle(let radius) is Shape::Circle { radius }, and both compilers reject a missing case. Two small differences to absorb. Rust variants must be qualified with the enum name (Shape::Circle, not .circle) unless you use Shape::* — the leading-dot shorthand does not exist. And Option and Result are ordinary enums declared in the standard library, not language features, which is why everything you learn here applies to them directly.Patterns: guards, bindings, and ranges
enum Message {
case text(String)
case number(Int)
case pair(Int, Int)
}
func describe(_ message: Message) -> String {
switch message {
case .number(let value) where value > 100:
return "big number \(value)"
case .number(let value) where (0...100).contains(value):
return "small number \(value)"
case .number:
return "negative"
case .text(let text) where text.isEmpty:
return "empty text"
case .text(let text):
return "text of \(text.count)"
case .pair(let first, let second):
return "pair summing to \(first + second)"
}
}
print(describe(.number(1000)))
print(describe(.number(7)))
print(describe(.text("swift")))
print(describe(.pair(2, 3))) enum Message {
Text(String), // a tuple-style variant: positional, no field names
Number(i32),
Pair(i32, i32),
}
fn describe(message: &Message) -> String {
match message {
// 'if' is Swift's 'where'.
Message::Number(value) if *value > 100 => format!("big number {}", value),
// Range patterns, matched directly.
Message::Number(0..=100) => String::from("small number"),
Message::Number(_) => String::from("negative"),
Message::Text(text) if text.is_empty() => String::from("empty text"),
Message::Text(text) => format!("text of {}", text.len()),
// | matches several patterns; @ binds the whole thing while matching it.
Message::Pair(first @ 1..=9, second) => format!("small pair {} {}", first, second),
Message::Pair(first, second) => format!("pair summing to {}", first + second),
}
}
println!("{}", describe(&Message::Number(1000)));
println!("{}", describe(&Message::Number(7)));
println!("{}", describe(&Message::Text(String::from("rust"))));
println!("{}", describe(&Message::Pair(2, 3)));
println!("{}", describe(&Message::Pair(20, 3))); The pattern language is a superset of what you know. Swift's
where clause is Rust's if guard, and Rust adds range patterns (0..=100, matched inline rather than through contains), alternation with |, and the @ binding, which captures a value while testing it (first @ 1..=9). The one thing to get used to is the * in *value > 100: because message is a reference, the bindings are references too, and comparing one to a number means dereferencing it. That is the borrow checker showing up somewhere you did not expect it, and it is the most common paper cut in a Rust match.Structs, Classes & Traits
struct and class collapse into one struct
Swift makes you choose between a value type and a reference type. Rust has only structs — sharing is expressed by how you pass them, not by what they are.
// A value type: copied on assignment.
struct Point {
var x: Int
var y: Int
func distanceFromOrigin() -> Double {
Double(x * x + y * y).squareRoot()
}
// 'mutating' is required to change self on a value type.
mutating func moveRight() {
x += 1
}
}
// A reference type: shared on assignment, and reference-counted.
class Counter {
var count = 0
func increment() { count += 1 }
}
var point = Point(x: 3, y: 4)
point.moveRight()
print(point, point.distanceFromOrigin())
let counter = Counter()
let alias = counter
alias.increment()
print(counter.count) // 1 — the same object #[derive(Debug, Clone, Copy, PartialEq)] // opt into the behaviors you want
struct Point {
x: i32,
y: i32,
}
// Methods live in an impl block, separate from the data.
impl Point {
// An associated function (no self) is the initializer — 'new' is a convention,
// not a keyword.
fn new(x: i32, y: i32) -> Self {
Point { x, y }
}
// &self borrows immutably — this is a plain Swift method.
fn distance_from_origin(&self) -> f64 {
(((self.x * self.x + self.y * self.y) as f64)).sqrt()
}
// &mut self is 'mutating'.
fn move_right(&mut self) {
self.x += 1;
}
}
let mut point = Point::new(3, 4);
point.move_right();
println!("{:?} {}", point, point.distance_from_origin());
// There is no 'class'. Shared MUTABLE state must be spelled out — and the fact
// that it is verbose is the point.
use std::cell::RefCell;
use std::rc::Rc;
let counter = Rc::new(RefCell::new(0));
let alias = Rc::clone(&counter);
*alias.borrow_mut() += 1;
println!("{}", counter.borrow()); // 1 — the same value Two ideas collapse into one. Every Rust struct is a value; whether it is shared depends on whether you pass it by value, by reference, or inside an
Rc. So mutating func becomes &mut self, an ordinary method becomes &self, and a Swift class becomes a struct plus the sharing strategy written out at the use site. Two things to note in the right column: methods live in an impl block outside the struct (so you can add several, and implement traits, without touching the data definition), and the behaviors Swift gives you for free — printing, copying, equality — are opted into with #[derive(Debug, Clone, Copy, PartialEq)]. If you forget to derive Debug, you cannot even print the thing, which is the most common five-second compile error in Rust.Protocols become traits
protocol Describable {
var name: String { get }
func describe() -> String
}
// A protocol extension: a default implementation for every conformer.
extension Describable {
func describe() -> String {
"I am \(name)"
}
}
struct Dog: Describable {
let name: String
}
struct Robot: Describable {
let name: String
func describe() -> String { "BEEP. I AM \(name.uppercased())" }
}
// Generic over the protocol (static dispatch)...
func announce<T: Describable>(_ value: T) {
print(value.describe())
}
// ...or an existential (dynamic dispatch, boxed).
let things: [any Describable] = [Dog(name: "Rex"), Robot(name: "R2")]
for thing in things {
print(thing.describe())
}
announce(Dog(name: "Rex")) trait Describable {
fn name(&self) -> String;
// A default method — Swift's protocol extension.
fn describe(&self) -> String {
format!("I am {}", self.name())
}
}
struct Dog { name: String }
struct Robot { name: String }
// The impl is SEPARATE from the type — so a trait can be implemented for a type
// you did not define (impl Describable for i32 is legal here).
impl Describable for Dog {
fn name(&self) -> String { self.name.clone() }
}
impl Describable for Robot {
fn name(&self) -> String { self.name.clone() }
fn describe(&self) -> String { format!("BEEP. I AM {}", self.name.to_uppercase()) }
}
// Generic: monomorphized, statically dispatched, no allocation.
fn announce<T: Describable>(value: &T) {
println!("{}", value.describe());
}
// dyn Trait is Swift's 'any Protocol': dynamic dispatch through a vtable, and it
// must be behind a pointer (Box) because the size is not known at compile time.
let things: Vec<Box<dyn Describable>> = vec![
Box::new(Dog { name: String::from("Rex") }),
Box::new(Robot { name: String::from("R2") }),
];
for thing in &things {
println!("{}", thing.describe());
}
announce(&Dog { name: String::from("Rex") }); The correspondence is unusually tight: a protocol is a trait, a protocol extension is a default method,
<T: Describable> is <T: Describable>, and any Describable is dyn Describable. Two differences with real consequences. Rust's impl block is separate from the type, so you can implement your trait for someone else's type (impl Describable for i32) — the coherence rules only require that either the trait or the type be yours. And dyn Trait must live behind a pointer (Box<dyn Trait>, &dyn Trait), because a trait object has no size known at compile time; Swift hides that boxing inside any, and Rust makes you type it, which is the same philosophy as clone() — the allocation is visible in the source.There is no class inheritance
class Animal {
let name: String
init(name: String) { self.name = name }
func speak() -> String { "..." }
func introduce() -> String {
"\(name) says \(speak())"
}
}
class Dog: Animal {
override func speak() -> String { "Woof" }
}
class Cat: Animal {
override func speak() -> String { "Meow" }
}
let animals: [Animal] = [Dog(name: "Rex"), Cat(name: "Tom")]
for animal in animals {
print(animal.introduce())
} // No classes, so no subclassing, no 'super', no overriding. Shared BEHAVIOR is a
// trait (with defaults); shared DATA is a field you compose in.
trait Speaker {
fn name(&self) -> &str;
fn speak(&self) -> String;
// The "base class method" becomes a default method on the trait.
fn introduce(&self) -> String {
format!("{} says {}", self.name(), self.speak())
}
}
// The "base class stored properties" become a struct you embed.
struct Animal {
name: String,
}
struct Dog { animal: Animal }
struct Cat { animal: Animal }
impl Speaker for Dog {
fn name(&self) -> &str { &self.animal.name }
fn speak(&self) -> String { String::from("Woof") }
}
impl Speaker for Cat {
fn name(&self) -> &str { &self.animal.name }
fn speak(&self) -> String { String::from("Meow") }
}
let animals: Vec<Box<dyn Speaker>> = vec![
Box::new(Dog { animal: Animal { name: String::from("Rex") } }),
Box::new(Cat { animal: Animal { name: String::from("Tom") } }),
];
for animal in &animals {
println!("{}", animal.introduce());
} Rust has no inheritance at all — no base class, no
super, no override. Polymorphism is traits, and code reuse is composition: the shared behavior moves into a trait's default methods, and the shared data becomes a struct you embed as a field (and then re-expose, which is the boilerplate this design costs you). Swift already pushes you this way — protocol-oriented programming is the house style, and structs are the default — so the change is less violent than it sounds. It is worth being honest about the cost: a deep class hierarchy does not port, and trying to recreate one with traits and Box<dyn> fights the language the whole way.Collections & Iterators
Array and Dictionary become Vec and HashMap
var numbers = [1, 2, 3]
numbers.append(4)
print(numbers, numbers.count)
print(numbers[0])
print(numbers.first ?? -1) // Optional — safe
var ages = ["Ada": 36, "Alan": 41]
ages["Grace"] = 45
print(ages["Ada"] ?? 0) // subscripting a Dictionary gives an Optional
print(ages["Nobody"] ?? 0)
for (name, age) in ages.sorted(by: { $0.key < $1.key }) {
print(name, age)
} use std::collections::HashMap;
let mut numbers = vec![1, 2, 3];
numbers.push(4);
println!("{:?} {}", numbers, numbers.len());
println!("{}", numbers[0]); // panics if out of bounds
println!("{}", numbers.first().copied().unwrap_or(-1)); // the Option version
let mut ages: HashMap<&str, i32> = HashMap::new();
ages.insert("Ada", 36);
ages.insert("Alan", 41);
ages.insert("Grace", 45);
// .get returns an Option — the same safety Swift's subscript gives you.
println!("{}", ages.get("Ada").copied().unwrap_or(0));
println!("{}", ages.get("Nobody").copied().unwrap_or(0));
// The entry API is the get-or-insert idiom, and it has no Swift equivalent.
*ages.entry("Ada").or_insert(0) += 1;
println!("{}", ages["Ada"]);
let mut sorted: Vec<_> = ages.iter().collect();
sorted.sort_by_key(|(name, _)| *name);
for (name, age) in sorted {
println!("{} {}", name, age);
} A
Vec<T> is an Array and a HashMap<K, V> is a Dictionary, with two things worth flagging. Indexing a Vec out of bounds panics (Swift traps too, so this is familiar), while .get() returns an Option — reach for that one. And HashMap's entry API (*map.entry(key).or_insert(0) += 1) does a lookup-and-insert in a single hash, which Swift needs two operations for; it looks strange and it is the idiom to learn. Note also that Rust's HashMap is unordered and randomly seeded, so anything you print must be sorted first — as above.map and filter are lazy, and must be collected
let names = ["Ada", "Grace", "Alan", "Barbara"]
// Eager: each step allocates a new Array.
let shouted = names
.filter { $0.count > 3 }
.map { $0.uppercased() }
print(shouted)
print(names.map { $0.count }.reduce(0, +))
print(names.joined(separator: ", "))
// $0 is the shorthand argument; laziness is opt-in with .lazy.
let firstLong = names.lazy.filter { $0.count > 3 }.first
print(firstLong ?? "(none)") let names = vec!["Ada", "Grace", "Alan", "Barbara"];
// .iter() opens a LAZY pipeline. Nothing runs until .collect() (or another
// consuming call) forces it — so the whole chain is fused into one pass.
let shouted: Vec<String> = names
.iter()
.filter(|name| name.len() > 3)
.map(|name| name.to_uppercase())
.collect(); // forget this and you have a lazy iterator,
println!("{:?}", shouted); // not a Vec — and a confusing type error
println!("{}", names.iter().map(|name| name.len()).sum::<usize>());
println!("{}", names.join(", "));
// Laziness is the default, so short-circuiting is free: this stops at the first
// match and never looks at "Barbara".
let first_long = names.iter().find(|name| name.len() > 3);
println!("{}", first_long.unwrap_or(&"(none)"));
// The closure argument has no $0: it is always named.
println!("{:?}", names.iter().map(|name| name.len()).collect::<Vec<_>>()); The vocabulary is the same, but the defaults are swapped: Swift's
map/filter are eager and each allocates an array (with .lazy as the opt-in), while Rust's iterator adapters are lazy and do nothing at all until a consumer — collect(), sum(), find(), a for loop — drives them. That means one pass over the data with no intermediate allocation, and it means forgetting .collect() is the classic beginner error (the compiler complains that a Map<Filter<…>> is not a Vec, which is bewildering until it is obvious). The other daily adjustment: there is no $0 — every closure names its parameter.Strings
String and &str — two types where Swift has one
Both languages take Unicode seriously and refuse to let you index a string by integer. Rust adds a distinction Swift does not have: owned versus borrowed.
let name = "Ada" // a String
let greeting = "Hello, \(name)!" // interpolation
print(greeting)
var builder = "abc"
builder += "def" // Strings are mutable value types
print(builder, builder.count)
// No integer subscript: you index with String.Index, because a Character is a
// grapheme cluster of variable width.
print(greeting.first!)
print(Array(greeting)[7])
// A Character is what a human calls a character — even if it is several scalars.
let flag = "🇳🇴"
print(flag.count) // 1
print(flag.unicodeScalars.count) // 2 let name = "Ada"; // &str — a BORROWED string slice
let greeting = format!("Hello, {}!", name); // String — OWNED, heap-allocated
println!("{}", greeting);
let mut builder = String::from("abc");
builder.push_str("def"); // push_str takes a &str
println!("{} {}", builder, builder.len()); // len() is BYTES, not characters!
// No integer subscript either — and for the same reason.
println!("{}", greeting.chars().next().unwrap());
println!("{}", greeting.chars().nth(7).unwrap());
// A 'char' is a Unicode SCALAR, not a grapheme cluster. This is the one place
// Swift is genuinely more correct out of the box.
let flag = "🇳🇴";
println!("{}", flag.chars().count()); // 2 — two regional indicators
println!("{}", flag.len()); // 8 — bytes
// Grapheme clusters need the 'unicode-segmentation' crate.
// The everyday rule: take &str as a parameter, return String when you own it.
fn shout(text: &str) -> String {
text.to_uppercase()
}
println!("{}", shout(&greeting)); The
String / &str split is ownership applied to text: String is a heap-allocated buffer you own (Swift's String), while &str is a borrowed view into one (a slice — closer to Swift's Substring). The rule that resolves ninety percent of the confusion: take &str as a parameter, return String when you produce something new. Two traps for a Swift developer. len() returns bytes, not characters — .chars().count() is what you meant. And a Rust char is a Unicode scalar, not a grapheme cluster, so an emoji flag counts as two; Swift's Character is grapheme-correct out of the box, and matching it in Rust needs an external crate. This is the one section where Swift is unambiguously ahead.Concurrency
Threads, and a compiler that checks them
Swift 6 has strict concurrency checking and
Sendable; Rust has Send and Sync. The ideas are cousins — but Rust's have been load-bearing since 1.0, and there is no runtime to hide behind.import Foundation
// A shared, mutable counter, protected by a lock you must remember to take.
final class Counter: @unchecked Sendable {
private let lock = NSLock()
private var count = 0
func increment() {
lock.lock()
count += 1
lock.unlock()
}
var value: Int {
lock.lock()
defer { lock.unlock() }
return count
}
}
let counter = Counter()
let group = DispatchGroup()
for _ in 1...4 {
DispatchQueue.global().async(group: group) {
counter.increment()
}
}
group.wait()
print(counter.value) use std::sync::{Arc, Mutex};
use std::thread;
// The lock OWNS the data: there is no way to read 'count' without taking it,
// because the value lives INSIDE the Mutex.
let counter = Arc::new(Mutex::new(0));
let mut handles = Vec::new();
for _ in 0..4 {
let counter = Arc::clone(&counter); // each thread gets its own handle
handles.push(thread::spawn(move || { // 'move' gives the closure ownership
let mut count = counter.lock().unwrap();
*count += 1;
})); // the lock is released HERE, at the
} // end of scope — no unlock() to forget
for handle in handles {
handle.join().unwrap();
}
println!("{}", *counter.lock().unwrap());
// Try to share it WITHOUT the Arc/Mutex and the program does not compile:
// the closure would need to move a value into several threads at once, and
// the Send/Sync bounds on thread::spawn reject it. A data race is a TYPE ERROR. The structural difference is that in Rust the lock owns the data. There is no way to read the counter without going through
lock(), because the integer lives inside the Mutex — so "forgot to take the lock" is not a bug you can write, and the guard releases at end of scope, so "forgot to unlock" is not either. Arc is the thread-safe Rc, and move hands the closure ownership of what it captures. Behind it all, the Send (safe to transfer between threads) and Sync (safe to share by reference) marker traits are what let the compiler reject a data race as a type error — the guarantee Swift 6 is now reaching for with Sendable, arrived at from a very different direction.async/await — with no runtime in the box
import Foundation
func fetch(_ id: Int) async -> String {
try? await Task.sleep(for: .milliseconds(10))
return "user-\(id)"
}
// Structured concurrency: the task group owns its children and waits for them.
func fetchAll() async -> [String] {
await withTaskGroup(of: String.self) { group in
for id in 1...3 {
group.addTask { await fetch(id) }
}
var results: [String] = []
for await result in group {
results.append(result)
}
return results.sorted()
}
}
// An actor serializes access to its state — no lock, no data race.
actor Counter {
private var count = 0
func increment() { count += 1 }
func value() -> Int { count }
}
let results = await fetchAll()
print(results)
let counter = Counter()
await counter.increment()
print(await counter.value()) // Rust has async/await in the LANGUAGE — and no executor in the standard library.
// An async fn returns a Future that does NOTHING until something polls it, so you
// bring a runtime: tokio, async-std, smol. That is the single biggest surprise
// coming from Swift, where the concurrency runtime ships with the language.
//
// #[tokio::main]
// async fn main() {
// let results = futures::future::join_all((1..=3).map(fetch)).await;
// }
//
// async fn fetch(id: i32) -> String {
// tokio::time::sleep(Duration::from_millis(10)).await;
// format!("user-{}", id)
// }
//
// The Playground has no tokio, so here is the same shape with real threads —
// which is often the right answer anyway: Rust threads are OS threads, and
// blocking one is fine.
use std::thread;
use std::time::Duration;
fn fetch(id: i32) -> String {
thread::sleep(Duration::from_millis(10));
format!("user-{}", id)
}
let handles: Vec<_> = (1..=3)
.map(|id| thread::spawn(move || fetch(id)))
.collect();
let mut results: Vec<String> = handles
.into_iter()
.map(|handle| handle.join().unwrap()) // join is 'await' on a thread
.collect();
results.sort();
println!("{:?}", results); Rust has
async/await as syntax, and then stops: an async fn returns a Future, a Future does nothing until it is polled, and nothing in the standard library polls it. You choose an executor — tokio, in practice — and that decision colors your whole dependency tree. Swift, by contrast, ships the runtime, the cooperative thread pool, and structured concurrency in the language. Two more gaps: there are no actors in Rust (an actor is Arc<Mutex<T>>, or a task owning the state and receiving messages over a channel), and there is no task tree — Swift's withTaskGroup cancels its children automatically, while a Rust JoinHandle is detached unless you go out of your way. What Rust has instead is Send/Sync, which means the compiler proves the whole thing is race-free rather than asking you to adopt an actor.