Value and reference types
Swift admits two kinds of types: value types (struct, enum, tuple, all standard-library collections) and reference types (class, actor, closures). The principal distinction: value-type assignment copies the value; reference-type assignment copies the reference (the underlying object is shared). Swift’s standard library is built on value types — Array, Dictionary, Set, String are all structs. The conventional contemporary discipline favours value types for routine data; reference types are reserved for genuine identity-based abstractions (UI views, network connections, observable state). The standard collections use copy-on-write (COW) — sharing storage until mutation, admitting substantial efficiency for the value-semantics defaults. The combination — value types as the default, reference types when identity matters, COW for substantial performance, the conventional struct-first design — is the substance of Swift’s memory model.
Value types: structs
The conventional value type:
struct Point {
var x: Int
var y: Int
}
var p1 = Point(x: 1, y: 2)
var p2 = p1 // copy
p2.x = 99
print(p1.x) // 1 (unchanged)
print(p2.x) // 99
Each binding holds an independent copy.
The principal value types in the standard library:
- All numeric types (
Int,Double, etc.). Bool,String,Character.Array,Dictionary,Set.Optional,Result.- Tuples.
- Most enums.
- Most structs.
Reference types: classes
class Point {
var x: Int
var y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
var p1 = Point(x: 1, y: 2)
var p2 = p1 // share the reference
p2.x = 99
print(p1.x) // 99 (mutated through p2!)
print(p2.x) // 99
Both bindings refer to the same object. The mechanism admits identity-based semantics — useful for shared mutable state.
When to use struct vs class
The conventional Swift discipline:
| Use struct (value type) when | Use class (reference type) when |
|---|---|
| The data has value semantics | Identity matters (objects must be uniquely identifiable) |
| Copies should be independent | Shared mutable state is intentional |
| The type is a simple data container | The type controls a resource (file, network, etc.) |
| Inheritance is not needed | Inheritance is required |
| The type is small to medium | The type is large and copying is expensive |
| Functional programming patterns | Object-oriented patterns with mutation |
Examples:
// Value types (typical):
struct User { let id: UUID; let name: String; let email: String }
struct Configuration { var host: String; var port: Int }
struct Color { let r, g, b: Double }
// Reference types (typical):
class ViewController { /* ... */ } // UI controllers
class NetworkConnection { /* ... */ } // resources
class Observable { /* ... */ } // shared mutable state
Mutating methods
For struct methods that mutate self, the mutating keyword is required:
struct Counter {
var count: Int = 0
mutating func increment() { // mutating required
count += 1
}
func description() -> String { // not mutating
"count: \(count)"
}
}
var c = Counter() // var (not let) for mutation
c.increment()
print(c.count) // 1
The mutating is not required for class methods (classes admit mutation through references):
class Counter {
var count: Int = 0
func increment() { // no mutating needed
count += 1
}
}
let c = Counter() // let admits mutation through the reference
c.increment()
print(c.count) // 1
For classes, let constrains the binding, not the value — the reference cannot be reassigned, but the object’s properties may be modified.
let and var with reference types
class Box { var value: Int = 0 }
let b1 = Box()
b1.value = 42 // OK (mutating through let-bound reference)
b1 = Box() // ERROR (cannot reassign the binding)
var b2 = Box()
b2.value = 42 // OK
b2 = Box() // OK (reassign the binding)
The conventional discipline:
letfor the binding itself.varfor the binding to admit reassignment.- Properties on the class control what’s mutable through the reference.
Identity comparison
For reference types, === and !== admit identity comparison:
class Point {
var x: Int
init(x: Int) { self.x = x }
}
let a = Point(x: 1)
let b = Point(x: 1)
let c = a
print(a === b) // false (different objects)
print(a === c) // true (same object)
print(a !== b) // true
For value types, === is not admitted — value types do not have a separable identity.
For value comparison (==), the type must conform to Equatable.
Copy-on-write (COW)
The standard collections (Array, Dictionary, Set, String) use copy-on-write:
var a = [1, 2, 3, 4, 5]
var b = a // shares storage (no copy yet)
a[0] = 99 // copy now (b's view unchanged)
print(a) // [99, 2, 3, 4, 5]
print(b) // [1, 2, 3, 4, 5]
The mechanism admits substantial efficiency:
- Reading a collection — no copy.
- Mutating a collection that has multiple references — copies first.
- Mutating a collection with single reference — mutates in place.
For custom value types with reference-typed storage, manual COW is admitted:
struct CowArray<T> {
private final class Storage {
var items: [T]
init(_ items: [T]) { self.items = items }
}
private var storage: Storage
init() { storage = Storage([]) }
var items: [T] {
get { storage.items }
set {
if !isKnownUniquelyReferenced(&storage) {
storage = Storage(newValue)
} else {
storage.items = newValue
}
}
}
}
The isKnownUniquelyReferenced admits checking whether the storage has a unique owner.
Inheritance and classes
Classes admit inheritance:
class Animal {
let name: String
init(name: String) {
self.name = name
}
func makeSound() -> String {
"..."
}
}
class Dog: Animal {
override func makeSound() -> String { // override required
"Woof!"
}
}
The override keyword is required — explicitly admits intent.
For preventing inheritance, the final keyword:
final class Singleton { /* ... */ } // cannot be inherited
class Animal {
final func describe() { /* ... */ } // cannot be overridden
}
The conventional discipline marks classes final unless inheritance is genuinely intended.
Treated in Classes and OOP.
Structs cannot inherit
Structs do not admit inheritance — they have a single, fixed shape. For shared behaviour, the conventional substitutes:
- Composition — embed one struct in another.
- Protocols with default implementations — POP-style.
- Generics — abstract over types.
// Composition:
struct Address {
let street: String
let city: String
}
struct Person {
let name: String
let address: Address // composition (not inheritance)
}
// Protocol-oriented:
protocol Describable {
var description: String { get }
}
extension Describable {
func print() { Swift.print(description) }
}
struct Person: Describable {
let name: String
var description: String { "Person: \(name)" }
}
Equatable and Hashable for value types
Structs admit synthesised conformance to Equatable and Hashable if all their properties conform:
struct Point: Equatable, Hashable { // synthesised; no implementation needed
let x: Int
let y: Int
}
Point(x: 1, y: 2) == Point(x: 1, y: 2) // true (synthesised)
var set: Set<Point> = []
set.insert(Point(x: 1, y: 2)) // works (synthesised Hashable)
For classes, the conformance is not synthesised — must be implemented manually if value-equality is wanted (the default == compares identity).
Properties
Properties may be stored or computed:
struct Rectangle {
var width: Double
var height: Double
// Computed property:
var area: Double {
width * height
}
// Computed with get and set:
var perimeter: Double {
get { 2 * (width + height) }
set { /* would adjust width/height */ }
}
}
Property observers (willSet, didSet) admit hooking property changes:
class Account {
var balance: Double = 0 {
willSet {
print("about to change to \(newValue)")
}
didSet {
print("changed from \(oldValue)")
}
}
}
The observers are conventional in classes; value-typed structs admit them too but are less commonly used.
lazy properties
Stored properties that are computed on first access:
class DataLoader {
lazy var data: [Item] = {
return loadFromDisk() // runs only on first access
}()
}
The lazy admits substantial deferred initialisation; the property must be var (not let).
Common patterns
Value-type model
struct User {
let id: UUID
var name: String
var email: String
var settings: UserSettings
}
struct UserSettings {
var theme: Theme
var notifications: Bool
}
// Mutation:
var user = User(id: UUID(), name: "Alice", email: "a@b.c",
settings: UserSettings(theme: .light, notifications: true))
user.settings.theme = .dark // mutates the struct
The pattern admits substantial value semantics.
Reference-type controller
class GameController {
private(set) var score: Int = 0
private var lives: Int = 3
func incrementScore(_ delta: Int) {
score += delta
}
func loseLife() -> Bool {
lives -= 1
return lives > 0
}
}
let game = GameController()
game.incrementScore(100) // shared, mutable state
Hybrid: struct with class-typed reference
struct Cache {
private let storage = NSCache<NSString, NSObject>()
// NSCache is a class — enables copy-cheap value semantics with shared storage
}
Identity-based set
class Person {
let name: String
init(name: String) { self.name = name }
}
extension Person: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self)) // hash by identity
}
static func == (lhs: Person, rhs: Person) -> Bool {
lhs === rhs // identity-based equality
}
}
var people: Set<Person> = []
let p1 = Person(name: "Alice")
let p2 = Person(name: "Alice") // distinct object
people.insert(p1)
people.insert(p2)
print(people.count) // 2 (different identities)
Switching between struct and class
A conventional Swift question is “should this be a struct or class”. The discipline:
// Start with struct:
struct User {
let id: UUID
var name: String
}
// Promote to class only if:
// 1. Identity is required (not just equality)
// 2. Inheritance is needed
// 3. Apple framework requires class (e.g., NSObject, observable)
// 4. Shared mutable state is intentional
inout parameters for mutation
func incrementAll(_ values: inout [Int]) {
for i in values.indices {
values[i] += 1
}
}
var nums = [1, 2, 3]
incrementAll(&nums)
print(nums) // [2, 3, 4]
The inout admits modifying the caller’s value; treated in Functions and closures.
Computed property over stored
struct Circle {
var radius: Double
// Computed (no storage):
var area: Double { Double.pi * radius * radius }
var diameter: Double {
get { radius * 2 }
set { radius = newValue / 2 }
}
}
The computed properties admit substantial flexibility — change the underlying storage without breaking callers.
Property observers
class Counter {
var count: Int = 0 {
didSet {
if count != oldValue {
NotificationCenter.default.post(name: .countChanged, object: self)
}
}
}
}
The observers admit substantial reactive patterns; conventional in MVC/MVVM architectures.
Lazy initialisation
class ImageManager {
lazy var thumbnailCache: [String: UIImage] = {
loadCacheFromDisk() // expensive; run only when needed
}()
}
Static properties
struct Configuration {
static let `default` = Configuration(host: "localhost", port: 8080)
static var current = Configuration.default
let host: String
let port: Int
}
let config = Configuration.current
Final classes
public final class Logger { // public API; no subclassing
public static let shared = Logger()
private init() {}
}
The final admits substantial optimisations and prevents fragile subclassing.
A note on the conventional discipline
The contemporary Swift value-vs-reference advice:
- Default to
structfor data. - Use
classonly when identity, inheritance, or shared mutation is genuinely needed. - Use
letovervarby default. - Use
mutatingon struct methods that modifyself. - Mark classes
finalunless inheritance is intended. - Synthesise
Equatable/Hashableon structs by conformance. - Use COW (manually) for substantial structs with reference-typed storage.
- Use computed properties for derived values.
- Use
lazyfor substantial deferred initialisation. - Use property observers (
didSet) for reactive patterns.
The combination — value types as the default, reference types for identity, copy-on-write for efficiency, mutating-explicit value-type methods, the substantial type-system support for both kinds — is the substance of Swift’s memory model. The discipline produces clear, predictable, performant code with substantial flexibility for both functional and object-oriented patterns.