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
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
-
Properties — stored, computed and observed properties on both structs and classes.
-
Methods and Subscripts —
mutatingmethods on value types, and how they interact with copy-on-write storage. -
Inheritance — class-only subclassing, overriding and dynamic dispatch.
-
Initialization and Deinitialization — two-phase initialization and
deinitin full. -
Automatic Reference Counting — how class reference sharing is kept alive and freed.
-
Memory Safety and Unsafe Pointers — exclusive access enforcement, which noncopyable ownership builds on.