Classes and Structs - RaduG/swift_learning GitHub Wiki

Overview

Structs and Classes are similar but have different purposes. Both can:

  • hold data and functionality (properties, methods, initialisers)
  • be extended (note to self: do not confuse with inheritance)
  • conform to protocols
  • implement subscript functionality

However:

  • Structs are always passed by value (single reference), whereas Classes are passed by reference (multiple variables can point to the same instance)
  • Classes provide inheritance
  • Classes can be type casted
  • Classes can have deinitialisers

Syntax:

class SomeClass {
  // body
}

struct SomeStruct {
  // body
}

Structs and Enums are value types

They are always passed by value. Example:

struct Time {
  var seconds: Int = 0

  mutating func tick() {
    self.seconds += 1
  }
}

var t1 = Time()
print("t1: \(t1.seconds)") // 0
t1.tick()
print("t1: \(t1.seconds)") // 1
var t2 = t1 // ~equivalent to t2 = Time(t1.seconds)
print("t2: \(t2.seconds)") // 1
t1.tick()
print("t1: \(t1.seconds)") // 2
print("t2: \(t2.seconds)") // 1

Note that the mutating keyword is used to mark that the method mutates the struct. With structs, to mutate the struct, the attribute being changed also has to be a var. If the struct is created as a constant (with let), all attributes are automatically constants. Example:

let t3 = Time()
t3.tick()

Will cause a compiler error:

structs_and_classes.swift:41:4: error: cannot use mutating member on immutable value: 't3' is a 'let' constant
t3.tick()
~~ ^
structs_and_classes.swift:40:1: note: change 'let' to 'var' to make it mutable
let t3 = Time()
^~~
var

Identity operator

Use === and !==:

let person1 = Person(firstName: "John", lastName: "Doe")
let person2 = person1
let person3 = Person(firstName: "John", lastName: "Doe")

print(person1 === person2) // true
print(person1 === person3) // false
print(person1 !== person3) // true

Lazy properties

Evaluation of properties can be deferred until they are needed. Example:

class SomeOtherClass {
  lazy var property: Void = print("Property was initialised")
  let someValue: Int

  init(someValue: Int) {
    self.someValue = someValue
  }
}

let inst = SomeOtherClass(someValue: 100)
print(inst.someValue)
print("Calling .property")
print(inst.property)

Computed properties

Properties may have getter and setter functions.

struct Line {
  struct Point {
    let x: Double
    let y: Double
  }

  var p1: Point
  var p2: Point

  // This property only has a getter
  var length: Double {
    sqrt(pow(p2.x - p1.x, 2) + pow(p2.y - p1.y, 2))
  }

  var centre: Point {
    get {
      let cX = p1.x + (p2.x - p1.x) / 2
      let cY = p1.y + (p2.y - p1.y) / 2
      return Point(x: cX, y: cY)
    }
    // if newCentre not explicitly given, the name newValue is assumed
    set (newCentre) {
      let currentCentre = self.centre
      let deltaX = newCentre.x - currentCentre.x
      let deltaY = newCentre.y - currentCentre.y

      p1 = Point(x: p1.x + deltaX, y: p1.y + deltaY)
      p2 = Point(x: p2.x + deltaX, y: p2.y + deltaY)
    }
  }
}

var line = Line(p1: Line.Point(x: 0, y: 0), p2: Line.Point(x: 10, y: 10))
print(line.length) // 10.0
print(line.centre) // Point(x: 5.0, y: 0.0)
line.centre = Line.Point(x: 1, y: 1)
print(line.length) // 10.0
print(line.p1) // Point(x: -4.0, y: 1.0)
print(line.p2) // Point(x: 6.0, y: 1.0)

Property observers

Typically used to observe changed in inherited computed properties:

// assumes Line is a class rather than a struct
class SpecialLine: Line {
  override var centre: Point {
    willSet (newValue) {
      print("Setting centre to \(newValue)")
    }

    didSet {
      print("Replaced centre \(oldValue) with \(centre)")
    }
  }
}

var specialLine = SpecialLine(p1: Line.Point(x: 0, y: 0), p2: Line.Point(x: 10, y: 10))
print(specialLine.centre) // Point(x: 5.0, y: 0.0)
specialLine.centre = SpecialLine.Point(x: 1, y: 1)

Note: If passing a property with observer as an inout argument, its observers will always be called due to the copy-in copy-out memory model.

Property wrappers

Property wrappers allow separation and reusing of management code across properties.

Basic example:

@propertyWrapper
struct AlwaysInt {
  private var n: Int

  init() {
    self.n = 0
  }

  var wrappedValue: Double {
    get {
      Double(self.n)
    }
    set {
      self.n = Int(newValue)
    }
  }
}

struct Baby {
  @AlwaysInt var age: Double
}

var baby = Baby()
print(baby.age)
baby.age = 12.3
print(baby.age)

However, in the example above, Baby.age cannot have a default value set in Baby because AlwaysInt does not have an appropriate constructor for it. To fix this:

@propertyWrapper
struct AlwaysInt {
  private var n: Int

  init() {
    self.n = 0
  }

  init(wrappedValue: Double) {
    self.n = Int(wrappedValue)
  }

  var wrappedValue: Double {
    get {
      Double(self.n)
    }
    set {
      self.n = Int(newValue)
    }
  }
}

struct Baby {
  @AlwaysInt var age: Double

  init(age: Double) {
    self.age = age
  }
}

var baby = Baby(age: 15)
print(baby.age)
baby.age = 12.3
print(baby.age)

It's also possible to have more than one argument for a property wrapper, in which case they can be "dynamic":

@propertyWrapper
struct AlwaysLessThan {
  private let limit: Double
  private var currentValue: Double

  var wrappedValue: Double {
    get {
      self.currentValue
    }
    set {
      self.currentValue = min(newValue, self.limit)
    }  
  }

  init(limit: Double) {
    self.limit = limit
    self.currentValue = 0
  }

  init(wrappedValue: Double, limit: Double) {
    self.limit = limit
    self.currentValue = min(limit, wrappedValue)
  }
}

struct NewPerson {
  @AlwaysLessThan(limit: 220) var heightCm: Double
  @AlwaysLessThan(limit: 99) var age: Double
}

var person10 = NewPerson()
print("\(person10.heightCm), \(person10.age)")
person10.heightCm = 250
person10.age = 122
print("\(person10.heightCm), \(person10.age)")

Property wrappers can also expose a "projected value" which is accessible using the same variable name, but with a prepended $. In the example below, projectedValue is used to show if the stored value exceeded the limit.

@propertyWrapper
struct AlwaysLessThan {
  private let limit: Double
  private var currentValue: Double

  var projectedValue: Bool = false
  var wrappedValue: Double {
    get {
      self.currentValue
    }
    set {
      self.currentValue = min(newValue, self.limit)
      self.projectedValue = newValue > self.limit
    }
  }

  init(limit: Double) {
    self.limit = limit
    self.currentValue = 0
  }

  init(wrappedValue: Double, limit: Double) {
    self.limit = limit
    self.currentValue = min(limit, wrappedValue)
  }
}

struct NewPerson {
  @AlwaysLessThan(limit: 220) var heightCm: Double
  @AlwaysLessThan(limit: 99) var age: Double
}

var person10 = NewPerson()
print("\(person10.heightCm), \(person10.age), \(person10.$heightCm), \(person10.$age)")
person10.heightCm = 250
person10.age = 122
print("\(person10.heightCm), \(person10.age), \(person10.$heightCm), \(person10.$age)")

Static properties

To define static properties, declare them as static. Static members are not accessible through instances.

class SomeClassWithStaticAttribute {
  static var limit = 20
  let x: Int

  init(x: Int) {
    self.x = x
  }
}`

Methods can also be static but they use the class keyword instead. In the context of a class method, self refers to the type itself.

class SomeClassWithStaticFunction {
  let x: Int

  class func makeInstance(x: Int) -> SomeClassWithStaticFunction {
    return self.init(x: x + 1)
  }

  required init(x: Int) {
    self.x = x
  }
}

let inst2 = SomeClassWithStaticFunction.makeInstance(x: 100)
print(inst2.x)

Mutating structs and enums

As structs and enums are value types, when they are mutated new instances are effectively being created with the new values. A mutating function can also assign a new instance to self.

struct SmartPoint {
  var x: Int
  var y: Int

  mutating func flip() {
    /// let temp = x
    /// x = y
    /// y = temp
    /// equivalent to
    self = SmartPoint(x: y, y: x)
  }
}

var p1 = SmartPoint(x: 10, y: 20)
print(p1)
p1.flip()
print(p1)
enum TrafficLight {
  case red, redAmber, green, amber

  mutating func next() {
    switch self {
    case .red:
      self = .redAmber
    case .redAmber:
      self = .green
    case .green:
      self = .amber
    case .amber:
      self = .red
    }
  }
}

var tl = TrafficLight.red
print(tl)
tl.next()
print(tl)
tl.next()
print(tl)

Subscripts

Classes, structs and enums can implement subscript access with arbitrary numbers of parameters. Subscripts can also have setters just like computed properties.

struct PowersTable {
  let base: Int

  subscript(power: Int) -> Double {
    return pow(Double(base), Double(power))
  }
}

let tbl = PowersTable(base: 2)
for p in 1...5 {
  print(tbl[p])
}
class SomeClass {
  subscript(indices: Int...) -> Int {
    print(indices)
    return 0
  }
}

let sc = SomeClass()
print(sc[1, 2, 3])