Tutorial outline

Unit 1

  1. 01Getting started
  2. 02Names and functions
  3. 03Control
  4. 04Higher-order functions
  5. 05Environments and closures
  6. 06Functional abstraction
  7. 07Recursion
  8. 08Tree recursion

Unit 2

  1. 09Sequences
  2. 10Dictionaries, sets, and tuples
  3. 11Optionals
  4. 12Structures and enumerations
  5. 13Linked lists and trees
  6. 14Mutation and value semantics
  7. 15Classes and inheritance
  8. 16Protocols and generics
  9. 17Iterators and lazy sequences
  10. 18Efficiency and memoization

Unit 3

  1. 19Algebraic data types and pattern matching
  2. 20Writing an interpreter
  3. 21Error handling
  4. 22Concurrency with async and await
  5. 23Testing
  6. 24Building a Swift package

Classes and inheritance

Reference types for things that should be shared: CS61A’s bank account as a class, identity with ===, type properties, subclasses that override behavior, and object lifetimes.

CS61A
Classes · Inheritance · Composing Programs 2.5–2.7

A bank account is not a value to be copied. If two people share an account, a deposit by one must be visible to the other. Things like that, with an identity and a state that changes over time, are objects, and Swift models them with classes.

Defining a class

CS61A’s example is the bank account. A class looks much like a structure:

class Account {
    let holder: String
    var balance = 0

    init(holder: String) {
        self.holder = holder
    }

    func deposit(_ amount: Int) -> Int {
        balance += amount
        return balance
    }

    func withdraw(_ amount: Int) -> Int {
        if amount > balance {
            print("Insufficient funds")
            return balance
        }
        balance -= amount
        return balance
    }
}

let account = Account(holder: "Ada")
print(account.deposit(100))   // 100
print(account.withdraw(30))   // 70

Two differences from a structure show up already. A class has no automatic memberwise initializer, so init must set every property that has no default. And deposit changes balance without being marked mutating, even though account was bound with let.

Reference semantics

That second difference is the important one. A class is a reference type: a variable holds a reference to an object, and assigning it copies the reference, not the object.

What would Swift print?

class Account {
    var balance = 0
}
let mine = Account()
let ours = mine
ours.balance += 10
print(mine.balance)

Type Error if running it would crash. For several lines of output, put each on its own line.

Show answer and explanation

Answer10

mine and ours refer to the same object, so a change made through ours is visible through mine. This is Python’s aliasing, on purpose. The let means the reference cannot change; the object it refers to still can.

=== asks whether two references point to the same object. == is not available unless the class defines what equality means:

class Account {
    var balance = 0
}

let a = Account()
let b = a
let c = Account()
print(a === b, a === c)   // true false

c is a different account that happens to have the same balance. Identity and equality are different questions, and with classes you need to know which one you are asking.

Type properties

A property marked static belongs to the class itself, not to any one object. CS61A uses a class attribute for the interest rate shared by every account:

class Account {
    static let interestRate = 0.02
    var balance = 0
}

print(Account.interestRate)   // 0.02

Structures and enumerations can have static properties too.

Inheritance

A subclass is a class defined in terms of another, its superclass. It gets all of the superclass’s properties and methods, and can override some of them. A checking account is an account that charges a fee for withdrawals:

class Account {
    var balance = 0

    func deposit(_ amount: Int) -> Int {
        balance += amount
        return balance
    }

    func withdraw(_ amount: Int) -> Int {
        if amount > balance {
            print("Insufficient funds")
            return balance
        }
        balance -= amount
        return balance
    }
}

class CheckingAccount: Account {
    static let withdrawFee = 1

    override func withdraw(_ amount: Int) -> Int {
        super.withdraw(amount + CheckingAccount.withdrawFee)
    }
}

let checking = CheckingAccount()
_ = checking.deposit(20)
print(checking.withdraw(5))   // 14

override is required, so you cannot replace a method by accident. super.withdraw calls the superclass’s version, reusing its logic rather than copying it.

What would Swift print?

class Account {
    var balance = 0
    func withdraw(_ amount: Int) -> Int {
        if amount > balance {
            print("Insufficient funds")
            return balance
        }
        balance -= amount
        return balance
    }
}
class CheckingAccount: Account {
    override func withdraw(_ amount: Int) -> Int {
        super.withdraw(amount + 1)
    }
}
let checking = CheckingAccount()
checking.balance = 20
print(checking.withdraw(5), checking.withdraw(100))

Type Error if running it would crash. For several lines of output, put each on its own line.

Show answer and explanation

AnswerInsufficient funds\n14 14

Both arguments to print are evaluated before anything is printed. The first withdrawal takes 5 plus the fee, leaving 14. The second asks for 101, which prints “Insufficient funds” immediately and returns 14 unchanged. Only then does print write 14 14.

A variable of the superclass type can hold a subclass object, and calling a method runs the version for the object’s actual class:

class Account {
    var balance = 0
    func withdraw(_ amount: Int) -> Int { balance -= amount; return balance }
}
class CheckingAccount: Account {
    override func withdraw(_ amount: Int) -> Int { super.withdraw(amount + 1) }
}

let accounts: [Account] = [Account(), CheckingAccount()]
for account in accounts {
    account.balance = 10
    print(account.withdraw(3))   // 7, then 6
}

Put the lines in order

Put the lines in order so the program prints 14.

  1. var balance = 0
  2. }
  3. func withdraw(_ amount: Int) -> Int {
  4. override func withdraw(_ amount: Int) -> Int {
  5. print(checking.withdraw(5))
  6. let checking = CheckingAccount()
  7. super.withdraw(amount + 1)
  8. }
  9. return balance
  10. }
  11. balance -= amount
  12. class CheckingAccount: Account {
  13. checking.balance = 20
  14. }
  15. class Account {

Show the correct program
class Account {
    var balance = 0
    func withdraw(_ amount: Int) -> Int {
        balance -= amount
        return balance
    }
}
class CheckingAccount: Account {
    override func withdraw(_ amount: Int) -> Int {
        super.withdraw(amount + 1)
    }
}
let checking = CheckingAccount()
checking.balance = 20
print(checking.withdraw(5))

Object lifetimes

Swift frees an object automatically when nothing refers to it any more, using automatic reference counting (ARC). A class can run code at that moment in a deinitializer:

class Tracker {
    let name: String
    init(_ name: String) {
        self.name = name
        print("hello", name)
    }
    deinit {
        print("bye", name)
    }
}

do {
    let tracker = Tracker("t")
    print("using", tracker.name)
}
print("after")

The do block makes a scope. When it ends, the last reference to the tracker disappears, so “bye t” is printed before “after”. Two objects that refer to each other can keep each other alive forever; the Swift book’s Automatic Reference Counting chapter explains how weak references break such cycles.

Lab 14: Objects

Q1: Vending machine

This is a CS61A lab classic. Write a VendingMachine class that sells one product at one price. It starts with no stock and no funds.

  • restock(_:) adds stock and returns "Current <product> stock: <n>".
  • addFunds(_:) returns "Current balance: $<n>", but if the machine is empty it keeps nothing and returns "Nothing left to vend. Please restock. Here is your $<n>.".
  • vend() returns "Nothing left to vend. Please restock." when empty, "Please add $<n> more funds." when short, and otherwise sells one item, returning "Here is your <product>." or "Here is your <product> and $<n> change.". Change is returned, so the balance goes back to 0.
Show solution
class VendingMachine {
    let product: String
    let price: Int
    private var stock = 0
    private var funds = 0

    init(product: String, price: Int) {
        self.product = product
        self.price = price
    }

    func restock(_ amount: Int) -> String {
        stock += amount
        return "Current \(product) stock: \(stock)"
    }

    func addFunds(_ amount: Int) -> String {
        if stock == 0 {
            return "Nothing left to vend. Please restock. Here is your $\(amount)."
        }
        funds += amount
        return "Current balance: $\(funds)"
    }

    func vend() -> String {
        if stock == 0 {
            return "Nothing left to vend. Please restock."
        }
        if funds < price {
            return "Please add $\(price - funds) more funds."
        }
        let change = funds - price
        stock -= 1
        funds = 0
        return change > 0 ? "Here is your \(product) and $\(change) change." : "Here is your \(product)."
    }
}

let machine = VendingMachine(product: "candy", price: 10)
assert(machine.vend() == "Nothing left to vend. Please restock.")
assert(machine.addFunds(15) == "Nothing left to vend. Please restock. Here is your $15.")
assert(machine.restock(2) == "Current candy stock: 2")
assert(machine.vend() == "Please add $10 more funds.")
assert(machine.addFunds(7) == "Current balance: $7")
assert(machine.addFunds(5) == "Current balance: $12")
assert(machine.vend() == "Here is your candy and $2 change.")
assert(machine.addFunds(10) == "Current balance: $10")
assert(machine.vend() == "Here is your candy.")
assert(machine.addFunds(15) == "Nothing left to vend. Please restock. Here is your $15.")

private keeps stock and funds inside the class, so the only way to change them is through the three methods, which keep them consistent.

Q2: Savings with interest

Write SavingsAccount, a subclass of Account (as defined in this lesson, with deposit and withdraw), that adds a method addInterest() which increases the balance by Account.interestRate, rounded down to a whole number, and returns the new balance. Withdrawals from a savings account should be refused entirely (return the balance unchanged) when they would leave less than 10.

Show solution
class Account {
    static let interestRate = 0.02
    var balance = 0

    func deposit(_ amount: Int) -> Int {
        balance += amount
        return balance
    }

    func withdraw(_ amount: Int) -> Int {
        if amount > balance {
            return balance
        }
        balance -= amount
        return balance
    }
}

class SavingsAccount: Account {
    static let minimumBalance = 10

    func addInterest() -> Int {
        balance += Int(Double(balance) * Account.interestRate)
        return balance
    }

    override func withdraw(_ amount: Int) -> Int {
        if balance - amount < SavingsAccount.minimumBalance {
            return balance
        }
        return super.withdraw(amount)
    }
}

let savings = SavingsAccount()
assert(savings.deposit(500) == 500)
assert(savings.addInterest() == 510)
assert(savings.withdraw(505) == 510)   // would leave 5, refused
assert(savings.withdraw(500) == 10)

addInterest is new, so it needs no override; withdraw replaces an inherited method, so it does. Int(...) of a Double rounds toward zero.

Q3: Shared or copied?

Predict what this program prints, then run it.

struct PointValue {
    var x = 0
}
class PointObject {
    var x = 0
}

var v1 = PointValue()
var v2 = v1
v2.x = 5

let o1 = PointObject()
let o2 = o1
o2.x = 5

print(v1.x, o1.x)
Show solution

It prints 0 5. The structure is copied, so changing v2 leaves v1 at 0. The class instance is shared, so changing it through o2 is visible through o1. The two type definitions are identical except for one keyword.

What’s next

Next lesson: protocols and generics, for code that works with many types at once.