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:
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:
let p3 = Point(x: 0.0, y: 0.0)
Mutability
Methods modifying properties require mutating
.
Example:
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:
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:
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:
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 (requiresEquatable
).===
: Compares references (classes only).
Example:
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:
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:
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
orunowned
(seeMemoryManagement.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
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)