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])