Swift & Objective-C

Swift Class vs Struct: What a Copy Still Shares

Compare a value, a class reference, and a struct containing a reference to see which mutations propagate and why struct does not imply a deep copy.

2 min read
Cover image for Swift Class vs Struct: What a Copy Still Shares

Start with a struct for a model representing a value; choose a class when identity or shared object state is required. Wrapping an object in a struct does not make the object independent or automatically thread-safe.

Mechanism and design choice

Assigning a struct copies its stored values. If a property holds a class reference, the reference is copied while the referenced object remains shared. Separate this from deep copying and thread safety. Inheritance and Objective-C object APIs can also influence the choice. Primary source

Reproduce the experiment

The experiment copies an integer in Point, a Box reference, and an Envelope holding a Box. It separates changing the object’s property from replacing the copied envelope’s box with a new object. Preconditions check both original and copied values at each step.

This standalone example was run on 2026-10-23 with Apple Swift 6.4 targeting arm64 macOS, in Swift 6 language mode. It needs no network or additional packages.

Download the complete Swift file

Run the command in the download directory. Preconditions stop execution if a check fails. Do not use -Ounchecked: it removes these checks.

swift -swift-version 6 swift-class-vs-struct.swift
struct Point { var x: Int }
final class Box {
    var x: Int
    init(_ x: Int) { self.x = x }
}
struct Envelope { var box: Box }

let point = Point(x: 0)
var copiedPoint = point
copiedPoint.x = 10
precondition(point.x == 0 && copiedPoint.x == 10)

let box = Box(0)
let copiedReference = box
copiedReference.x = 10
precondition(box === copiedReference && box.x == 10)

let envelope = Envelope(box: Box(0))
var copiedEnvelope = envelope
copiedEnvelope.box.x = 20
precondition(envelope.box.x == 20)
copiedEnvelope.box = Box(30)
precondition(envelope.box.x == 20 && copiedEnvelope.box.x == 30)

print("struct=0,10 class=10,10 nested-reference=20,20 replacement=20,30")

Recorded output and interpretation

struct=0,10 class=10,10 nested-reference=20,20 replacement=20,30

Point diverges into 0 and 10; the Box references share 10. Mutating the object through the copied Envelope makes both observe 20. Replacing that envelope’s box with Box(30) leaves the original at 20. Property independence and shared object identity are distinct.

Scope and limitations

This sequential experiment checks copy semantics, not races or performance. Sharing a mutable variable or a nested reference still requires isolation or synchronization. Swift 6 Sendable checking is not equivalent to merely declaring a struct.

What to check next

List which state should change together after copying a model. For unintended reference sharing, prefer immutable values or define an explicit cloning policy. For an efficiently copied large backing store, continue with the CoW experiment.

Related experiment

Sources and verification