Skip to content

Classes and Structures

Classes and structures are Swift’s primary constructs for defining custom types. Structures are value types, copied on assignment, while classes are reference types, shared via references. This document explores their features, semantics, and use cases.

Structures

Structures (struct) are ideal for data with value semantics, such as coordinates or configurations.

Defining a Structure

Example:

swift
struct Point {
    var x: Double
    var y: Double
    
    mutating func moveBy(dx: Double, dy: Double) {
        x += dx
        y += dy
    }
    
    func distanceTo(_ other: Point) -> Double {
        return sqrt(pow(x - other.x, 2) + pow(y - other.y, 2))
    }
}

var p1 = Point(x: 1.0, y: 2.0)
var p2 = p1 // Copy
p1.moveBy(dx: 3.0, dy: 4.0)
print(p1) // (4.0, 6.0)
print(p2) // (1.0, 2.0)
print(p1.distanceTo(p2)) // 5.0

Memberwise Initializers

Structures automatically provide initializers for all properties.

Example:

swift
let p3 = Point(x: 0.0, y: 0.0)

Mutability

Methods modifying properties require mutating.

Example:

swift
struct Counter {
    private var value = 0
    mutating func increment() {
        value += 1
    }
}

Classes

Classes (class) support reference semantics, inheritance, and deinitialization, suitable for shared state or complex objects.

Defining a Class

Example:

swift
class Vehicle {
    var speed: Double
    let id: String
    
    init(speed: Double, id: String) {
        self.speed = speed
        self.id = id
    }
    
    func accelerate(by: Double) {
        speed += by
    }
    
    deinit {
        print("Vehicle \(id) deallocated")
    }
}

class Car: Vehicle {
    var brand: String
    
    init(brand: String, speed: Double, id: String) {
        self.brand = brand
        super.init(speed: speed, id: id)
    }
    
    override func accelerate(by: Double) {
        super.accelerate(by: max(0, by))
        print("\(brand) now at \(speed) mph")
    }
}

let car1 = Car(brand: "Toyota", speed: 60.0, id: "T1")
let car2 = car1 // Same reference
car1.accelerate(by: 10.0) // "Toyota now at 70.0 mph"
print(car2.speed) // 70.0

Inheritance

Classes support single inheritance.

Example:

swift
class ElectricCar: Car {
    var batteryLevel: Double
    init(brand: String, speed: Double, id: String, batteryLevel: Double) {
        self.batteryLevel = batteryLevel
        super.init(brand: brand, speed: speed, id: id)
    }
}

Deinitializers

Classes execute cleanup in deinit.

Example (see above Vehicle.deinit).

Value vs. Reference Semantics

  • Structures: Copied on assignment, mutations are independent.
  • Classes: Shared via references, mutations affect all references.

Example:

swift
struct Color {
    var hue: Double
}
var color1 = Color(hue: 0.5)
var color2 = color1
color1.hue = 0.7
print(color1.hue, color2.hue) // 0.7, 0.5

class Theme {
    var hue: Double
    init(hue: Double) { self.hue = hue }
}
let theme1 = Theme(hue: 0.5)
let theme2 = theme1
theme1.hue = 0.7
print(theme1.hue, theme2.hue) // 0.7, 0.7

Identity vs. Equality

  • ==: Compares values (requires Equatable).
  • ===: Compares references (classes only).

Example:

swift
extension Car: Equatable {
    static func == (lhs: Car, rhs: Car) -> Bool {
        return lhs.id == rhs.id
    }
}

let car3 = Car(brand: "Toyota", speed: 60.0, id: "T1")
print(car1 == car3) // true
print(car1 === car3) // false

Computed and Stored Properties

Both support properties (see VariablesAndConstants.md).

Example:

swift
class Circle {
    var radius: Double
    var area: Double {
        return Double.pi * radius * radius
    }
    init(radius: Double) { self.radius = radius }
}

Protocols and Extensions

Both can conform to protocols and be extended.

Example:

swift
protocol Describable {
    var description: String { get }
}

extension Point: Describable {
    var description: String {
        return "Point(\(x), \(y))"
    }
}

Best Practices

  • Structures First: Use structs for simple, immutable data.
  • Classes for Shared State: Use classes for objects with lifecycle or identity.
  • Minimize Inheritance: Prefer composition or protocols.
  • Equatable Conformance: Implement for custom equality checks.
  • Immutable Properties: Use let for properties that don’t change.
  • Deinit Usage: Clean up resources in classes.

Troubleshooting

  • Mutating Errors: Add mutating to struct methods.
  • Reference Cycles: Use weak or unowned (see MemoryManagement.md).
  • Inheritance Issues: Ensure override is used correctly.
  • Initializer Errors: Call super.init in subclasses.
  • Performance: Avoid deep copying large structs.

Example: Comprehensive Class and Struct Usage

swift
protocol Drivable {
    func drive()
}

struct Location {
    var latitude: Double
    var longitude: Double
    
    mutating func update(to newLocation: Location) {
        latitude = newLocation.latitude
        longitude = newLocation.longitude
    }
    
    var description: String {
        return "(\(latitude), \(longitude))"
    }
}

class Route {
    var stops: [Location]
    weak var vehicle: Vehicle?
    
    init(stops: [Location]) {
        self.stops = stops
    }
    
    func addStop(_ location: Location) {
        stops.append(location)
    }
    
    deinit {
        print("Route deallocated")
    }
}

extension Route: Drivable {
    func drive() {
        print("Driving through \(stops.map { $0.description }.joined(separator: " -> "))")
    }
}

var loc1 = Location(latitude: 37.7749, longitude: -122.4194)
var loc2 = loc1 // Copy
loc1.update(to: Location(latitude: 34.0522, longitude: -118.2437))
print(loc1.description, loc2.description) // (34.0522, -118.2437), (37.7749, -122.4194)

let route = Route(stops: [loc1, loc2])
route.addStop(Location(latitude: 40.7128, longitude: -74.0060))
route.drive() // Driving through (34.0522, -118.2437) -> (37.7749, -122.4194) -> (40.7128, -74.0060)

Released under the MIT License.