I picked up Rust in 2019, after nearly a decade writing software for Apple platforms. The motivation was half career, the Apple ecosystem felt increasingly closed and stagnant and I wanted somewhere else to invest my time, and half curiosity about what a real, modern systems language looked like in practice.
The first impression held up. Someone had quietly built Swift's more muscular sibling. The ergonomics were familiar enough to be productive within a week: enums with associated values, pattern matching, expression-oriented control flow, generics that read like Swift's, protocols renamed to traits, an option type, a result type. Underneath, the language is honest about what it's doing. A Vec<u8> is a length, a capacity, and a pointer, nothing else. There's no Data class hiding a buffer behind a copy-on-write boundary, no runtime making layout decisions on your behalf. What you write is what runs, and what runs is close to the hardware.
The claim this series is built on is narrower than the typical pitch for Rust: Swift's mental model overlaps with Rust's far more than most material acknowledges. Most existing material targets C, Java, or Python developers, and coming in from C alone the path is steep, because the type-system ergonomics Rust leans on (enums, generics, traits, pattern matching) all arrive at once. Coming from Swift, those ergonomics are already familiar, and the genuinely new ideas worth slowing down on are narrower: mostly the borrow checker, lifetimes, and how ownership extends into concurrency.
The type system is the natural place to start, because it's mostly familiar territory and lets us calibrate the Swift-to-Rust dictionary before any of the harder material. The differences are real, but they cluster around a small set of concepts: separate impl blocks, the orphan rule, monomorphisation, and the distinction between dynamic and opaque protocol types.
Structs
A Swift struct and a Rust struct look roughly the same on first reading:
struct Point {
var x: Double
var y: Double
func distance(from other: Point) -> Double {
let dx = x - other.x
let dy = y - other.y
return (dx * dx + dy * dy).squareRoot()
}
}
struct Point {
x: f64,
y: f64,
}
impl Point {
fn distance(&self, other: &Point) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
}
The differences are small but shape how the rest of the language reads.
Methods live in a separate impl block. The struct is data, the impl is behaviour, and the two are syntactically distinct. A type can have several impl blocks, including conditional ones (impl<T: Clone> Wrapper<T>), which lets capabilities grow along type-parameter constraints rather than being clustered inside the type declaration. Swift extensions cover some of the same ground, but impl is the primary way you write methods, not an afterthought.
Mutability lives on the binding, not the field. Rust has no var vs let per property. A struct is borrowed either immutably or mutably (let p vs let mut p), and all fields share that mutability. This is initially jarring coming from Swift, but field-level mutability is a leaky abstraction there too: any non-trivial computed property breaks it.
Constructors are functions, not initializers. Rust has no init keyword. You construct a struct with the literal syntax Point { x: 1.0, y: 2.0 } and, by convention, expose a new associated function for non-trivial construction:
impl Point {
fn new(x: f64, y: f64) -> Self {
Point { x, y }
}
}
The literal syntax replaces Swift's compiler-synthesized memberwise initializer.
Trait derivation replaces synthesized conformance. Swift synthesizes Equatable, Hashable, and Codable when the struct is trivially conformable. Rust gets the same effect with explicit derive macros:
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Point { x: i64, y: i64 }
The list of derivable traits is open. Third-party crates extend it with their own derives, for example serde::Serialize and serde::Deserialize for free JSON support.
Two minor variants are common enough to know about. Tuple structs have positional rather than named fields. They show up heavily in the newtype pattern, which we'll come back to under the orphan rule:
struct Meters(f64);
let height = Meters(1.8);
println!("{}", height.0);
Unit structs have no fields at all. They are used as marker types in trait implementations and as compile-time tags:
struct Initialized;
struct Uninitialized;
Swift has neither directly. The closest equivalents are a struct with a single named field, or an empty struct, but neither is idiomatic in the way tuple and unit structs are in Rust.
Enums and pattern matching
Swift's enum with associated values is among the cleanest features the language offers. Rust's enum is the same idea with the same shape, plus a match expression more rigorous than Swift's switch.
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case triangle(base: Double, height: Double)
}
func area(of shape: Shape) -> Double {
switch shape {
case .circle(let r):
return .pi * r * r
case .rectangle(let w, let h):
return w * h
case .triangle(let b, let h):
return 0.5 * b * h
}
}
enum Shape {
Circle { radius: f64 },
Rectangle { width: f64, height: f64 },
Triangle { base: f64, height: f64 },
}
fn area(shape: &Shape) -> f64 {
match shape {
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
Shape::Rectangle { width, height } => width * height,
Shape::Triangle { base, height } => 0.5 * base * height,
}
}
The mapping is essentially one-to-one: each variant carries its own payload, the only way to read it is to pattern-match, and exhaustiveness checking works the same way in both languages (with a catch-all available in both: default: in Swift, the wildcard _ in Rust). The genuine differences are in two places.
Variants come in three shapes. Tuple-style variants (Circle(f64)), struct-style variants (Circle { radius: f64 }), and unit variants (Empty). Tuple-style is more concise, struct-style is self-documenting, and unit variants serve as state-machine tags. Swift's enums map closest to the tuple-style form with argument labels.
Patterns are first-class everywhere a binding can appear. Rust reuses the same pattern grammar in match arms, if let, while let, plain let bindings, function parameters, and for loop variables. Swift has if case, guard case, and switch, but the syntax differs in each. In Rust, once you learn the pattern syntax, it applies everywhere. A function parameter, for example, can destructure its argument the same way let would:
fn translate(Point { x, y }: Point, dx: f64, dy: f64) -> Point {
Point { x: x + dx, y: y + dy }
}
fn midpoint((x1, y1): (f64, f64), (x2, y2): (f64, f64)) -> (f64, f64) {
((x1 + x2) / 2.0, (y1 + y2) / 2.0)
}
Call sites are unchanged. The caller passes a regular Point and regular tuples; the destructuring happens entirely in the function header:
let p = Point { x: 1.0, y: 2.0 };
let moved = translate(p, 3.0, 4.0); // Point { x: 4.0, y: 6.0 }
let m = midpoint((0.0, 0.0), (10.0, 10.0)); // (5.0, 5.0)
Function-parameter patterns must be irrefutable (the function cannot fail to match its own argument), which rules out enum variants but covers structs, tuples, references, and any combination of these.
Refutable patterns
if let and while let are direct ports of the Swift constructs:
if let Some(name) = lookup(id) {
println!("found {}", name);
}
while let Some(item) = queue.pop() {
process(item);
}
There is no direct equivalent of Swift's guard let. The idiomatic stand-in is a match arm with an early return on the failure path:
let name = match lookup(id) {
Some(name) => name,
None => return,
};
println!("found {}", name);
In functions that themselves return Option or Result, the ? operator (covered in the error-handling post) collapses the match into a single character.
Guards and bindings
Match arms can be filtered by an arbitrary boolean expression, the same as Swift's where clause on a switch case:
match value {
Shape::Rectangle { width, height } if width == height => println!("square"),
Shape::Rectangle { width, height } => println!("{}x{} rectangle", width, height),
_ => {}
}
The @ operator (read "at") binds a name to a value while simultaneously checking that value against a sub-pattern. In n @ 0..=100, Rust first checks that the value lies in the inclusive range 0..=100, and if it matches, the value is also captured in n for the arm body to use. Without @, the choice is binary: write 0..=100 => to check without binding, or n => to bind without checking. @ lets you do both at once.
match temperature {
n @ 0..=100 => println!("ok: {}", n),
n => println!("out of range: {}", n),
}
The same idea composes inside larger patterns. Inside a struct destructure, width: w @ 0..=1024 matches windows whose width fits the range and binds that width to w:
struct Window { width: u32, height: u32 }
match window {
Window { width: w @ 0..=1024, height } => println!("narrow: {}x{}", w, height),
Window { width, height } => println!("wide: {}x{}", width, height),
}
0..=100 is a range pattern, also available for char and integer types. Swift has no equivalent in pattern position; the same effect requires a guard expression.
Option<T> and Result<T, E>
Two standard-library enums deserve mention here, because they are the spine of every Rust API:
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Option<T> is Swift's T? written longhand, with all the same use cases. Result<T, E> is Swift's standard-library Result<Success, Failure>, but it's the universal shape Rust uses for fallible operations rather than the throws mechanism. The error-handling post covers the rest. For now, treat Result as the primary citizen and ? as the moral equivalent of Swift's try.
Traits and protocols
Traits are the closest thing Rust has to Swift's protocols, and the mapping is generally one-to-one. A trait declares a set of methods (and optionally associated types and constants) that a type can provide; an impl block makes a type conform.
protocol Greet {
var name: String { get }
func hello() -> String
}
extension Greet {
func hello() -> String {
return "Hello, \(name)!"
}
}
struct Person: Greet {
let name: String
}
trait Greet {
fn name(&self) -> &str;
fn hello(&self) -> String {
format!("Hello, {}!", self.name())
}
}
struct Person {
name: String,
}
impl Greet for Person {
fn name(&self) -> &str {
&self.name
}
}
The shape is the same: a contract, a default method, a type that satisfies it. Three differences shape how Rust idioms read.
Conformance always lives in a separate impl block. Swift can declare conformance inline on the type (struct Person: Greet { ... } with the methods inside the struct body) or in an extension. Rust only has the second form. Every conformance is its own impl Trait for Type block, deliberately cordoned off from the type definition. Visually noisier at first, but in practice it makes capability boundaries obvious at a glance, and it lets you split conformances across modules.
Default methods are inline, not in extensions. Swift requires putting default implementations inside a protocol extension. Rust puts them inside the trait body, with a function body following the signature. The mechanics are identical, just the placement differs.
Associated types use type, not associatedtype. Swift's associatedtype Element becomes Rust's type Item;, and where clauses constraining associated types work the same way as Swift's where Item: ...:
trait Container {
type Item;
fn get(&self, index: usize) -> Option<&Self::Item>;
}
trait CloneableContainer: Container
where
Self::Item: Clone,
{
fn duplicate(&self, index: usize) -> Option<Self::Item> {
self.get(index).cloned()
}
}
CloneableContainer extends Container only for implementations whose Item is Clone. The where clause carries the constraint, and the default duplicate method body relies on it (Option::cloned requires the inner type to be Clone).
So far, mostly cosmetic. The next four points have no direct Swift equivalent and reshape how you organise code in Rust.
The orphan rule
Rust enforces a constraint called the orphan rule: when you write impl Trait for Type, either the trait or the type (or both) must be local to your crate. You can't, for example, implement the standard library's Display trait for the standard library's Vec<T> from your own crate. The compiler refuses on the grounds that two crates could each add their own implementation, and the linker would have no way to choose.
Swift allows retroactive conformance freely. You can extend any type to conform to any protocol from anywhere, and the collision risk is left as a runtime concern.
The orphan rule is the price of coherence: every (Trait, Type) pair has at most one implementation in the entire program, and the compiler can prove this statically. The cost is that you sometimes need a workaround.
The newtype pattern
The standard workaround for the orphan rule is wrapping the foreign type in a tuple struct of your own:
struct MyVec(Vec<i32>);
impl std::fmt::Display for MyVec {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let parts: Vec<String> = self.0.iter().map(i32::to_string).collect();
write!(f, "[{}]", parts.join(", "))
}
}
MyVec is local to your crate, so the orphan rule is satisfied and you can implement any trait you like on it. The wrapper is zero-cost (the layout is identical to the inner Vec), and you forward methods you want to expose either by hand or via the Deref trait.
The newtype pattern shows up far more often than Swift devs expect. Beyond the orphan-rule workaround, it's the idiomatic way to express type-level distinctions that Swift would handle with phantom-typed wrappers or distinct structs: Meters(f64) vs Feet(f64), UserId(u64) vs OrderId(u64). The compiler refuses to mix them.
Blanket impls
Rust traits support blanket implementations: a single impl block that implements a trait for every type satisfying some constraint.
impl<T: Display> ToString for T {
fn to_string(&self) -> String {
format!("{}", self)
}
}
Read this as: every type T that implements Display automatically implements ToString. The standard library uses blanket impls heavily for cross-trait conversions. Swift cannot express this directly. The closest analogue is a constrained protocol extension (extension P where Self: Q), which is similar in effect but only adds methods to P; it doesn't grant Q-conformance to anyone.
Object safety
A trait can usually be used in two ways: as a generic constraint (fn f<T: Trait>(x: T)) or as a runtime-polymorphism mechanism (fn f(x: &dyn Trait)). The first is monomorphised at compile time; the second uses dynamic dispatch through a virtual method table (vtable). The next section unpacks the dichotomy.
Not every trait can be used in the second form. A trait is object-safe if and only if a vtable can be constructed for it. The rules are roughly:
- No generic methods. A vtable would need one entry per instantiation, which is unbounded.
Selfonly in receiver position.&self,&mut self,self: Box<Self>are fine. Methods returningSelfor takingSelfby value are not object-safe, becauseSelfhas an unknown size at the call site through adynreference.- No associated constants.
If a trait violates these, the compiler refuses dyn Trait and points at the method responsible.
The Swift parallel is the rule that protocols with Self or associated type requirements cannot be used as existential types. The error message is Protocol 'X' can only be used as a generic constraint because it has Self or associated type requirements. Different name, same underlying problem: the existential's size and operations cannot be determined ahead of time.
No class inheritance
Rust has no classes and no inheritance. There is nothing equivalent to class Subclass: Superclass. Behaviour reuse goes through three channels instead. Composition: a struct has another struct as a field, and methods delegate. Trait inheritance: trait A: B means "every type that implements A must also implement B". This is a constraint on conformance, not an "is-a" relationship in the OOP (object-oriented programming) sense, and there is no method overriding because there are no virtual methods to override. Default methods on traits: reusable behaviour gets attached to a trait once and is inherited by every type that implements the trait.
Swift developers who already lean on protocol-oriented programming feel at home. Those who lean on class hierarchies need to switch idioms; Rust will not meet them halfway.
No method overloading
Rust does not allow two methods with the same name and different argument types on the same type. fn foo(x: i32) and fn foo(x: String) cannot both exist on the same impl. The idiomatic alternative is a single generic method bounded by a trait, where the trait is implemented for each accepted argument type:
trait Foo {
fn foo(self);
}
impl Foo for i32 {
fn foo(self) { /* ... */ }
}
impl Foo for String {
fn foo(self) { /* ... */ }
}
This is more code than Swift's overloading, but it surfaces in the type signature exactly which argument types are accepted, and the trait can be extended later by other crates without modifying the original.
Generics
Generic functions and types in Rust are visually familiar. The syntax for type parameters is the same. The substantial differences are how bounds compose, when to use associated types versus generic parameters, and what happens at the generic call site.
func max<T: Comparable>(_ a: T, _ b: T) -> T {
return a > b ? a : b
}
fn max<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
Bounds
Multiple bounds on a single parameter use +, mirroring Swift's &:
func describe<T: Equatable & CustomStringConvertible>(_ a: T, _ b: T) -> String {
a == b ? "same: \(a)" : "\(a) and \(b)"
}
fn describe<T: PartialEq + std::fmt::Display>(a: T, b: T) -> String {
if a == b {
format!("same: {}", a)
} else {
format!("{} and {}", a, b)
}
}
For more than two or three bounds, the inline form gets noisy. The where clause handles longer constraints out-of-line, identical in role to Swift's:
fn describe<T>(a: T, b: T) -> String
where
T: PartialEq + std::fmt::Display,
{
// ...
}
Associated types vs generic parameters
Rust traits can be parametrised in two distinct ways: with associated types (which we saw under traits) or with generic type parameters. The two are not interchangeable, and the choice carries meaning.
A generic type parameter is an input type. The implementer picks it, and a single type can have many implementations:
trait From<T> {
fn from(value: T) -> Self;
}
impl From<&str> for String { /* ... */ }
impl From<char> for String { /* ... */ }
String implements From<T> separately for each conversion source.
An associated type is an output type. It is determined by the implementer, and a type implements the trait at most once:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
You cannot make Vec<i32> an iterator producing both i32s and &strs. Pick one. The associated type forces a single answer.
Swift protocols only have associated types, not generic parameters. The closest Swift idiom for From<T> is a family of distinct protocols (ExpressibleByStringLiteral, ExpressibleByIntegerLiteral, and so on), one per source type. Rust collapses the family into a single generic trait.
Monomorphisation
The largest performance-relevant difference between Rust and Swift generics lives here. Rust generics are monomorphised, literally "made into a single form per use" (from Greek monos "one" and morphē "form"). At compile time, each instantiation (Vec<i32>, Vec<String>, Vec<MyStruct>) produces its own specialised machine code, with the size, layout, and operations of that specific type baked in. There is no runtime cost. Method calls on a generic type are direct calls, not table lookups, and inlining works as it does for any concrete code.
This is the structural reason a Vec<u8> is (ptr, len, cap) and nothing else, as the introduction noted. The compiler knows the type at every instantiation site, so there's no value-witness machinery to thread through and no runtime discovery to make. The Swift side of this story is the witness-table mechanism that runs through unspecialised generics across module boundaries; Rust simply does not have it.
The trade-offs are real. Compile times scale with the number of instantiations, so a heavily generic crate compiles slower than the same code written against concrete types. Binary size grows with the number of distinct monomorphisations, since each gets its own machine code in the output.
Both pressures encourage keeping generic functions small and pushing the bulk of work into non-generic helpers. The pattern is common enough to have a name in the community: write a thin generic wrapper that converts to a concrete type and delegates to a non-generic implementation. For example, a function that reads a file from any path-like argument:
pub fn read_file<P: AsRef<Path>>(path: P) -> std::io::Result<String> {
fn inner(path: &Path) -> std::io::Result<String> {
std::fs::read_to_string(path)
}
inner(path.as_ref())
}
Path here is a standard-library struct from std::path, the borrowed form of a filesystem path (the same relationship &str has to String). AsRef<Path> is the trait implemented by anything that can be borrowed as a &Path, including &str, String, PathBuf, and &Path itself.
More broadly, AsRef<T> is a general-purpose conversion trait used wherever a function wants to accept "anything cheaply borrowable as &T". The standard library provides AsRef<str> for string-like things, AsRef<[u8]> for byte slices, AsRef<[T]> for general slices, and many more. The trait body is a single method that returns &T, and the implicit contract is that the conversion is O(1) and allocation-free. For conversions with real cost, the analogous trait is Into<T>.
The outer read_file is generic over AsRef<Path>, but its body is two lines: convert and delegate. The compiler monomorphises this trivial wrapper for each distinct argument type, while the inner function (where the actual file reading happens) is concrete and compiled exactly once. The cost of generic flexibility is paid only at the conversion boundary.
Turbofish
When type inference cannot determine a generic argument, Rust requires the type to be provided explicitly. The syntax is the turbofish, ::<>:
let n = "42".parse::<i32>().unwrap();
let v = Vec::<i32>::new();
Swift rarely needs explicit type arguments, because constructor and method-return inference resolve most cases. Rust hits ambiguous instantiations more often, and the turbofish is the canonical disambiguator.
dyn Trait vs impl Trait
A trait can be used in two distinct positions in the type system. Inside angle brackets it acts as a constraint on a type parameter (fn f<T: Trait>(x: T)), which we covered above. Outside angle brackets a trait can also stand in for a type, and Rust offers two forms with different semantics: dyn Trait and impl Trait. Both have direct Swift analogues.
dyn Trait
dyn Trait is a trait object: a value where the concrete type has been erased, leaving only the trait conformance visible. A dyn Animal could be a Dog, a Cat, or anything else implementing Animal, and the caller cannot tell the difference at compile time.
What a vtable is
To make method calls work without compile-time type information, the compiler builds a virtual method table, almost always shortened to vtable. A vtable is a small static table of function pointers, one slot per method the trait declares, each slot pointing to that type's specific implementation of that method. The compiler generates one vtable per (type, trait) pair and embeds it in the binary as ordinary read-only data.
For example, given a trait
trait Animal {
fn speak(&self) -> &str;
fn legs(&self) -> u32;
}
and Dog and Cat types implementing it, the compiler emits two vtables that look conceptually like this:
| Slot | Dog's vtable | Cat's vtable |
|---|---|---|
drop | drops a Dog | drops a Cat |
| size, alignment | of Dog | of Cat |
speak | Dog::speak | Cat::speak |
legs | Dog::legs | Cat::legs |
A &dyn Animal value is then a fat pointer: a pair of pointers, where a normal reference would be one. The first points to the actual Dog or Cat data, the second points to that type's Animal vtable. Calling animal.speak() becomes "look up the speak slot in the vtable, jump to whatever is there". The cost is one memory load plus one indirect branch per call. CPUs predict this cheaply when the same call site sees the same type repeatedly, more expensively when types vary widely.
The structure is the same as how C++ implements virtual methods on classes and how Java implements interface dispatch. The Rust-specific point is that vtables are opt-in: they exist only for dyn Trait values. Generic code (T: Trait) and impl Trait are dispatched statically and have no vtable.
Putting it together
Because the size of dyn Animal itself is unknown at compile time (it could be a Dog or a Cat, with different layouts), you only ever encounter dyn Trait behind a pointer: a &dyn Trait reference, a Box<dyn Trait> heap allocation, or similar. We come back to this under Dynamically Sized Types (DSTs) below.
trait Animal {
fn speak(&self) -> &str;
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn speak(&self) -> &str { "woof" }
}
impl Animal for Cat {
fn speak(&self) -> &str { "meow" }
}
fn announce(animal: &dyn Animal) {
println!("{}", animal.speak());
}
let pets: Vec<Box<dyn Animal>> = vec![Box::new(Dog), Box::new(Cat)];
for pet in &pets {
announce(pet.as_ref());
}
The Swift equivalent is the existential form of a protocol, the type you get by writing the protocol name as a type itself:
protocol Animal {
func speak() -> String
}
let pets: [Animal] = [Dog(), Cat()]
for pet in pets {
print(pet.speak())
}
The mechanics are the same: heterogeneous storage, runtime dispatch, type information carried alongside the value. Swift uses witness tables (the same idea by a different name) and an existential container that holds either the value inline or a heap pointer, but the underlying story is structurally the same.
impl Trait
impl Trait is an opaque type. The function returns a single concrete type that implements the trait, but the caller only knows the trait, not the type itself. There is no erasure, no vtable, no runtime indirection. In return position, it hides a complicated concrete type (often a chain of iterator adapters or a closure) behind the trait it implements:
fn even_squares(n: u32) -> impl Iterator<Item = u32> {
(0..n).filter(|i| i % 2 == 0).map(|i| i * i)
}
The caller knows the return value is "some Iterator over u32" and nothing more. The actual type, a deeply nested generic combinator, is invisible. In argument position, impl Trait is sugar for an unnamed generic parameter:
fn print_all(items: impl Iterator<Item = u32>) {
for x in items { println!("{}", x); }
}
// equivalent to:
fn print_all<I: Iterator<Item = u32>>(items: I) { /* ... */ }
The Swift equivalent is some Protocol, stabilised in Swift 5.1, which behaves identically: a single hidden concrete type, monomorphised at compile time, no dynamic dispatch:
func evenSquares(n: Int) -> some Sequence {
return stride(from: 0, to: n, by: 2).lazy.map { $0 * $0 }
}
Why dyn Trait needs indirection
A natural question after meeting dyn Trait: why do we need &dyn Trait or Box<dyn Trait>? Why not just dyn Trait on its own?
Because dyn Trait is a Dynamically Sized Type (DST): a type whose size isn't known at compile time. A Dog is one size, a Cat is another, and dyn Animal could be either or something else altogether. Without a fixed size, the compiler can't place a value on the stack, pass it by value, or store it directly in a struct field. The fix is to put it behind a pointer of a known size, which is what &dyn Trait (a fat pointer: data pointer plus vtable pointer) and Box<dyn Trait> (heap-allocated, same fat-pointer shape) do.
Other DSTs you have already met include str (the borrowed slice form of a string, always seen as &str) and [T] (the slice type, always seen as &[T]). The same rule applies in each case: you only ever interact with them through a pointer.
Swift solves the size problem differently. Existentials in Swift are boxed: every existential is the same fixed-size container (five words on 64-bit, around 40 bytes), holding either the value inline if it fits or a heap pointer if it doesn't. Type metadata and witness tables sit alongside. The size question is therefore implicit at the language level. Rust instead pushes the choice onto the caller: &dyn Trait borrows in place, Box<dyn Trait> allocates on the heap, and the cost is visible in the signature.
Choosing between them
The default in Rust is impl Trait. Static dispatch is faster, the binary is no larger than equivalent concrete code (no vtables, no fat pointers), and the type system carries more information. Reach for dyn Trait when you need a heterogeneous collection (Vec<Box<dyn Trait>>), when you need to store or return a value whose concrete type is decided at runtime, when the trait is large or rarely called and you want to avoid the binary-size cost of monomorphisation, or when you explicitly want runtime polymorphism in the API surface.
The Swift heuristic carries over: prefer some Protocol over the existential form, and reach for the existential only when heterogeneity or runtime polymorphism is genuinely needed.
What's missing in either direction
The previous sections handled the structurally important gaps (no class inheritance in Rust, no method overloading, no retroactive conformance without a newtype, the orphan rule, blanket impls). A few smaller items are worth flagging before closing.
Computed properties. Swift's var area: Double { width * height } becomes a method in Rust: fn area(&self) -> f64 { self.width * self.height }. Rust doesn't distinguish stored from computed at the type level; if it has parentheses at the call site, it's a method.
Property observers (willSet, didSet). No direct equivalent. The idiomatic Rust pattern is to make the field private and expose a setter method that performs the side effects explicitly.
Subscripts. Rust expresses indexing through the Index and IndexMut traits:
struct Matrix { rows: Vec<Vec<f64>> }
impl std::ops::Index<(usize, usize)> for Matrix {
type Output = f64;
fn index(&self, (i, j): (usize, usize)) -> &f64 {
&self.rows[i][j]
}
}
let m: Matrix = /* ... */;
let value = m[(2, 3)];
Operator overloading in general works this way: every operator has a corresponding trait (Add, Sub, Mul, Deref, and so on) that you implement.
Default arguments and argument labels. Swift's func greet(name: String = "world") and call-site labels (greet(name: "Alice")) aren't in Rust. The substitutes are constructor functions (Person::new(name) plus Person::default()), the builder pattern for richer cases, Option<T> parameters, and descriptive function names that compensate for the absence of labels (read_to_string rather than read(format: ...)).
The builder pattern shows up so often in idiomatic Rust libraries that it's worth seeing in concrete form:
struct Window { title: String, width: u32, height: u32 }
struct WindowBuilder { title: String, width: u32, height: u32 }
impl WindowBuilder {
fn new(title: impl Into<String>) -> Self {
Self { title: title.into(), width: 800, height: 600 }
}
fn size(mut self, w: u32, h: u32) -> Self {
self.width = w;
self.height = h;
self
}
fn build(self) -> Window {
Window {
title: self.title,
width: self.width,
height: self.height,
}
}
}
let w = WindowBuilder::new("My App").size(1024, 768).build();
Each setter takes mut self by value and returns Self, which is what makes chaining work: each call consumes the builder, mutates fields in place, and hands the same value back so the next method has something to be called on. The final build consumes the builder one last time and returns the configured target type. Defaults live in the constructor (new); you only call setters for fields you want to override.
Key paths and reflective access (\Person.name, Mirror). Rust has nothing analogous in stable code. The closest practical approximation is closure-based field access (|p: &Person| &p.name) or a code-generation mechanism such as serde's field-level attributes; neither is runtime reflection.
In the other direction, two type-system features that have surfaced in passing deserve being named explicitly, since they will return throughout the series.
The Drop trait. Every type can implement Drop to run destructor code when a value goes out of scope (fn drop(&mut self)). This is how files are closed, locks are released, and heap allocations are freed in Rust. Swift's class deinit is the nearest parallel, but Drop runs deterministically at scope exit, not when a refcount hits zero.
The Sized trait and ?Sized. Every type parameter is implicitly Sized, meaning its size is known at compile time. To accept a DST in a generic context, you opt out with T: ?Sized. You see ?Sized mostly on standard-library wrappers like Box<T: ?Sized> and Arc<T: ?Sized>, which is what lets them hold DSTs such as dyn Trait.
A handful of larger Rust topics are deliberately outside this post: unsafe, declarative and procedural macros, lifetimes, and the standard collection traits. Each gets its own treatment later in the series.
The type system is the half of Rust that translates most readily from Swift. The next post turns to error handling, where Result<T, E> and the ? operator together replace the throws mechanism. The post after that is memory and ownership, where the genuinely new ideas begin.