When to Choose a Swift Struct Over a Class: 10 Performance and Safety Benefits

Prefer a Swift struct over a class when you need value semantics, zero ARC overhead, thread-safe data sharing, and predictable memory layout without sacrificing performance through copy-on-write optimization.

Swift provides two fundamental data modeling tools: value types (struct, enum) and reference types (class). While classes enable inheritance and shared mutable state, the Swift standard library and compiler are architected to make structs the default choice for most data containers. According to the apple/swift repository documentation and implementation, structs provide specific performance and safety guarantees that classes cannot match.

Copy-on-Write Value Semantics

Structs in Swift exhibit value semantics, meaning each variable holds an independent copy of the data. However, the compiler implements copy-on-write (COW) optimization to avoid expensive deep copies until mutation actually occurs.

As documented in [docs/OwnershipManifesto.md](https://github.com/apple/swift/blob/main/docs/OwnershipManifesto.md), COW allows large value types like Array and String to share underlying storage between copies, performing the actual copy only when one instance modifies the data. This gives you the safety of value semantics with the performance characteristics of reference semantics.

Zero ARC Overhead and Predictable Memory

Unlike classes, which are allocated on the heap and managed by Automatic Reference Counting (ARC), structs typically live on the stack or inline within other values. This eliminates the runtime cost of retain and release operations.

The [docs/SIL/Types.md](https://github.com/apple/swift/blob/main/docs/SIL/Types.md) documentation notes that value types enable more aggressive compiler optimizations, including monomorphization of generic code and better cache locality due to predictable memory layout. When the compiler knows the exact size and alignment of a struct, it can inline storage directly into parent containers rather than requiring pointer indirection.

Thread Safety by Default

Because each copy of a struct is independent, value types are inherently thread-safe without requiring locks or synchronization primitives. When you pass a struct to another thread, you pass a copy of the data, not a reference to shared mutable state.

This aligns with the Law of Exclusivity described in [docs/OwnershipManifesto.md](https://github.com/apple/swift/blob/main/docs/OwnershipManifesto.md), which guarantees that memory accessed for writing cannot be simultaneously accessed for reading or writing by any other code. This static enforcement prevents data races at the language level.

Identity-Free Semantics and Hashable Conformance

Structs have no object identity—two instances are equal if and only if their stored properties are equal. This simplifies equality comparisons and makes structs natural candidates for use as dictionary keys or set elements.

The standard library implements Set and Dictionary to work optimally with value types. As shown in [stdlib/public/core/SetStorage.swift](https://github.com/apple/swift/blob/main/stdlib/public/core/SetStorage.swift), these collections rely on the stable hash values and value semantics of their elements to provide deterministic behavior and efficient lookups.

Reduced Boilerplate and Generic Performance

Structs require less ceremony than classes. You do not need to write initializers for stored properties unless you need custom logic, and there is no inheritance hierarchy to manage. As noted in [docs/StandardLibraryProgrammersManual.md](https://github.com/apple/swift/blob/main/docs/StandardLibraryProgrammersManual.md), the standard library guidelines encourage keeping struct definitions short and focused.

Additionally, generic code operating on structs can be monomorphized—the compiler generates specialized machine code for each concrete type used, eliminating the overhead of dynamic dispatch. This is significantly harder to achieve with reference types that rely on virtual method tables.

Practical Code Examples

Value Type with Copy-on-Write

struct Point {
    var x: Double
    var y: Double
    
    mutating func translate(byX dx: Double, byY dy: Double) {
        x += dx
        y += dy
    }
}

let origin = Point(x: 0, y: 0)
var point = origin
point.translate(byX: 3, byY: 4)

// origin remains (0,0) due to value semantics
print(origin)  // Point(x: 0.0, y: 0.0)
print(point)   // Point(x: 3.0, y: 4.0)

Reference Type for Shared State

final class Person {
    var name: String
    var friends: [Person] = []
    
    init(name: String) {
        self.name = name
    }
}

let alice = Person(name: "Alice")
let bob = alice  // Both reference the same instance

bob.name = "Alicia"
print(alice.name)  // "Alicia" - shared mutable state

Summary

  • Copy-on-write optimization gives structs the performance of reference types while maintaining value semantics and safety.
  • Zero ARC overhead eliminates retain/release cycles and heap allocations, improving runtime performance.
  • Thread safety by default prevents data races without locks because each copy is independent.
  • Identity-free semantics simplify equality checks and enable use as dictionary keys and set elements.
  • Better generic performance through compiler monomorphization and predictable memory layout.
  • Less boilerplate compared to classes, with automatic memberwise initializers and no inheritance complexity.

Frequently Asked Questions

When is a Swift class better than a struct?

Choose a class when you need reference semantics for shared mutable state, such as managing a shared cache, implementing the observer pattern, or when you require inheritance for polymorphic behavior. Classes are also necessary when you need a deinitializer (deinit) to clean up resources, or when working with Objective-C APIs that expect objects.

Do structs always live on the stack?

No, while small structs typically live on the stack or inline within other values, large structs or those captured in closures may be allocated on the heap. However, even when heap-allocated, structs avoid the reference counting overhead that classes incur, and the compiler optimizes their layout for cache locality.

Can structs have methods and conform to protocols?

Yes, structs can have methods, computed properties, property observers, and can conform to protocols just like classes. They can also implement mutating methods that modify the struct's properties. The key difference is that structs cannot inherit from other structs or classes, and they do not support deinitializers or stored properties that are references to the instance itself.

How does copy-on-write work with custom structs?

The Swift standard library implements copy-on-write for types like Array and String using internal reference counting of a backing storage class. For your own custom structs, you can implement COW by wrapping a reference type (a class) as private storage and checking for uniqueness before mutation. However, for small value types, the compiler optimizations often make explicit COW unnecessary unless you are managing large, expensive-to-copy buffers.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →