Polyglot
Languages Swift reference types
Swift § reference-types

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) whenUse class (reference type) when
The data has value semanticsIdentity matters (objects must be uniquely identifiable)
Copies should be independentShared mutable state is intentional
The type is a simple data containerThe type controls a resource (file, network, etc.)
Inheritance is not neededInheritance is required
The type is small to mediumThe type is large and copying is expensive
Functional programming patternsObject-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:

  • let for the binding itself.
  • var for 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 struct for data.
  • Use class only when identity, inheritance, or shared mutation is genuinely needed.
  • Use let over var by default.
  • Use mutating on struct methods that modify self.
  • Mark classes final unless inheritance is intended.
  • Synthesise Equatable/Hashable on structs by conformance.
  • Use COW (manually) for substantial structs with reference-typed storage.
  • Use computed properties for derived values.
  • Use lazy for 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.