Structures and Classes

This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Structures and classes are Swift’s two general-purpose, named building blocks for defining new types: both can declare stored and computed properties, methods, initializers, subscripts, and protocol conformances (see Properties, Methods and Subscripts and Protocols). What sets them apart — value semantics vs. reference semantics — is the subject of this page.

Definition Syntax, Instances and Property Access

struct Resolution {
    var width = 0
    var height = 0
}

class VideoMode {
    var resolution = Resolution()
    var interlaced = false
    var frameRate = 0.0
    var name: String?
}

let someResolution = Resolution()          // () calls the memberwise/default initializer
let someVideoMode = VideoMode()

print("The width of someResolution is \(someResolution.width)")   // dot syntax for property access
someVideoMode.resolution.width = 1280       // reaching through a class instance to a nested struct's property
print("The width is now \(someVideoMode.resolution.width)")

Both are defined with a body of { } following their keyword and name, in UpperCamelCase by convention, and both are instantiated with initializer syntax (TypeName()); members are accessed and set with dot syntax on either kind of instance.

Memberwise Initializers

struct Resolution2 {
    var width = 0
    var height = 0
}

let vga = Resolution2(width: 640, height: 480)   // the compiler-synthesized memberwise initializer

// class VideoMode2 {
//     var resolution = Resolution2()
//     // let m = VideoMode2(resolution: vga)   // error: classes get no memberwise initializer
// }

A struct that declares no initializer of its own automatically receives a memberwise initializer, with one parameter per stored property, in declaration order; a class never receives one, since a class is expected to fully control how its (possibly inherited) storage is set up — see Initialization and Deinitialization for the full initializer-delegation rules this feeds into.

Value Types vs. Reference Types

Assigning a struct copies its value into a second independent variable; assigning a class instance shares one reference between two variables; a copy-on-write struct defers the actual copy until a mutation happens
struct Size { var width = 0 }
var a = Size(width: 10)
var b = a                          // b is a fully independent copy
b.width = 20
print(a.width, b.width)            // 10 20 -- a is untouched

class Box { var width = 0; init(width: Int) { self.width = width } }
let c = Box(width: 10)
let d = c                          // d refers to the same Box instance as c
d.width = 20
print(c.width, d.width)            // 20 20 -- both see the same instance

Structs and enums are value types: assigning an instance to a new variable, or passing it as a function argument, copies its value (conceptually — Swift avoids the actual copy until it would otherwise be observable; see Copy-on-Write below). Classes are reference types: assignment and argument passing copy only a reference to one shared instance, so a mutation through any one reference is visible through every other reference to that same instance — exactly why d.width = 20 above also changes what c.width reports.

Identity Operators === / !==

let e = c                 // c and d and e all point at the very same Box instance
if c === e {
    print("c and e refer to the same Box instance")
}
if c !== d {
    print("this never prints -- c and d are also the same instance")
} else {
    print("c and d are the same instance too")
}

===/!== test whether two class references point to the exact same instance in memory (identity), which is a different question from ==/!= (equality, requiring Equatable and comparing values a type considers equal). Value types have no notion of identity at all — two struct values are simply equal or not, never "the same instance."

Choosing Between Structures and Classes

Apple’s official guidance: prefer a structure by default. Reach for a class specifically when you need Objective-C interoperability, controlled sharing of a single mutable instance across your program (identity that matters), or inheritance from an existing class hierarchy (e.g. UIViewController). Otherwise, structures — along with enums — give value semantics (no unexpected sharing, easy Equatable/Hashable synthesis, safe default use across concurrency domains) at a lower cost, since copies are only conceptual until a mutation forces one (Copy-on-Write, next).

Copy-on-Write and isKnownUniquelyReferenced

final class Storage<Element> {
    var elements: [Element]
    init(_ elements: [Element]) { self.elements = elements }
}

struct MyArray<Element> {
    private var storage: Storage<Element>

    init(_ elements: [Element]) { storage = Storage(elements) }

    var elements: [Element] { storage.elements }

    mutating func append(_ element: Element) {
        if !isKnownUniquelyReferenced(&storage) {          // another MyArray shares this Storage -- copy first
            storage = Storage(storage.elements)
        }
        storage.elements.append(element)
    }
}

var m1 = MyArray([1, 2, 3])
var m2 = m1                     // cheap: only the Storage reference is copied so far
m2.append(4)                    // triggers the real copy here, since storage is shared
print(m1.elements, m2.elements) // [1, 2, 3] [1, 2, 3, 4]

Array, Dictionary, Set and String all implement copy-on-write this way internally: a value type wraps a private reference to some heap storage, sharing that storage across copies until a mutating operation is about to run, at which point isKnownUniquelyReferenced(_:) checks whether the wrapping value is the storage’s only owner. If another copy still shares it, the storage is duplicated first; otherwise the existing storage is mutated in place, safely, since nothing else can observe the change.

Noncopyable Types, consume/borrowing/consuming, and deinit

struct FileHandle: ~Copyable {                 // this type may never be implicitly copied
    private let descriptor: Int32

    init(descriptor: Int32) { self.descriptor = descriptor }

    consuming func close() {                    // takes ownership and ends the value's lifetime
        print("closing descriptor \(descriptor)")
    }

    deinit {                                     // runs if close() was never called
        print("descriptor \(descriptor) leaked -- closing in deinit")
    }
}

func borrow(_ handle: borrowing FileHandle) {    // reads without taking ownership
    print("using descriptor without consuming it")
}

let handle = FileHandle(descriptor: 3)
borrow(handle)
let moved = consume handle                       // explicitly ends `handle`'s lifetime here...
// borrow(handle)                                // ...so using it again is a compile error
moved.close()

A type marked ~Copyable ("noncopyable") gives up implicit copying entirely: the compiler tracks each value’s single owner and enforces, at compile time, that it is used exactly once along any path — passed on, or explicitly ended. borrowing and consuming are parameter modifiers describing how a function uses its noncopyable argument: borrowing reads it temporarily without ending its lifetime; consuming takes ownership and ends it, which is also what a consuming method (close() above) does to self. The consume operator forces a value’s lifetime to end at that point explicitly. A noncopyable struct can declare deinit, unlike an ordinary (copyable) struct — since with a single enforced owner, Swift always knows the exact moment a noncopyable value’s lifetime ends, the same guarantee a class relies on for its own deinit (see Initialization and Deinitialization). Noncopyable types were introduced in Swift 5.9 and are covered further by SE-0390.

See Also