Lesson 76Protocols

Protocol Extensions

What are Protocol Extensions?

Protocol extensions let you provide default implementations for protocol requirements. This is the foundation of Protocol-Oriented Programming in Swift!

What They Can Do

Default Implementations

Provide default behavior for requirements

Add New Methods

Add functionality to all conforming types

Constrained Extensions

Add methods only when constraints are met

Extend Standard Library

Add to Collection, Sequence, etc.

Constrained Extensions

extension Collection where Element: Numeric {
    var total: Element { ... }
}

Static vs Dynamic Dispatch

Important Gotcha!

  • Methods in protocol = dynamic dispatch (overrides work)
  • Methods only in extension = static dispatch (overrides ignored when using protocol type)

Benefits Over Inheritance

  • Works with value types (structs, enums)
  • Multiple protocol conformance
  • More flexible composition
  • Retroactive modeling
main.swift
// PROTOCOL EXTENSIONS
// Add default implementations to protocols

// BASIC PROTOCOL EXTENSION
protocol Greetable {
    var name: String { get }
    func greet() -> String
}

extension Greetable {
    // Default implementation
    func greet() -> String {
        return "Hello, \(name)!"
    }

    // Additional functionality
    func formalGreet() -> String {
        return "Good day, \(name)."
    }
}

struct Person: Greetable {
    var name: String
    // Uses default greet() implementation
}

struct Robot: Greetable {
    var name: String

    // Override default implementation
    func greet() -> String {
        return "Beep boop, I am \(name)"
    }
}

let person = Person(name: "Alice")
let robot = Robot(name: "R2D2")

print(person.greet())      // Hello, Alice!
print(person.formalGreet()) // Good day, Alice.
print(robot.greet())       // Beep boop, I am R2D2

// EXTENDING STANDARD LIBRARY PROTOCOLS
extension Collection {
    var isNotEmpty: Bool {
        return !isEmpty
    }

    func printAll() {
        for element in self {
            print(element)
        }
    }
}

let numbers = [1, 2, 3]
print(numbers.isNotEmpty)  // true
numbers.printAll()         // 1, 2, 3

// CONSTRAINED PROTOCOL EXTENSIONS
extension Collection where Element: Numeric {
    var total: Element {
        return reduce(0, +)
    }
}

print([1, 2, 3, 4, 5].total)  // 15
print([1.5, 2.5, 3.0].total)  // 7.0

// Only for Equatable elements
extension Collection where Element: Equatable {
    func countOf(_ element: Element) -> Int {
        return filter { $0 == element }.count
    }
}

print([1, 2, 2, 3, 2].countOf(2))  // 3

// PROTOCOL-ORIENTED DESIGN
protocol Identifiable {
    var id: String { get }
}

protocol Timestamped {
    var createdAt: Date { get }
}

protocol Describable {
    var description: String { get }
}

// Default implementations
extension Identifiable {
    var id: String {
        return UUID().uuidString
    }
}

extension Describable where Self: Identifiable {
    var description: String {
        return "Object with ID: \(id)"
    }
}

struct Article: Identifiable, Timestamped, Describable {
    var title: String
    var createdAt: Date = Date()
    // Gets default id and description!
}

// ADDING FUNCTIONALITY TO EXISTING TYPES
extension Equatable {
    func isEqual(to other: Self) -> Bool {
        return self == other
    }
}

print(5.isEqual(to: 5))      // true
print("hi".isEqual(to: "hello"))  // false

// PROTOCOL EXTENSION VS CLASS INHERITANCE
// Protocols: Multiple conformance, value types supported
// Classes: Single inheritance, reference types only

protocol Flyable {
    var maxAltitude: Int { get }
}

extension Flyable {
    func fly() {
        print("Flying up to \(maxAltitude) feet!")
    }
}

protocol Swimmable {
    var maxDepth: Int { get }
}

extension Swimmable {
    func swim() {
        print("Swimming down to \(maxDepth) feet!")
    }
}

// Struct can conform to multiple protocols!
struct Duck: Flyable, Swimmable {
    var maxAltitude: Int = 500
    var maxDepth: Int = 10
}

let duck = Duck()
duck.fly()   // Flying up to 500 feet!
duck.swim()  // Swimming down to 10 feet!

// PROTOCOL EXTENSION GOTCHA
protocol Drawable {
    func draw()
}

extension Drawable {
    func draw() {
        print("Default drawing")
    }

    func erase() {
        print("Default erasing")
    }
}

struct Circle: Drawable {
    func draw() {
        print("Drawing circle")
    }

    func erase() {
        print("Erasing circle")
    }
}

let shape: Drawable = Circle()
shape.draw()   // Drawing circle (dispatched dynamically)
shape.erase()  // Default erasing (static dispatch!)

Try It Yourself!

Create a Stackable protocol with push/pop methods. Add a default peek() implementation via protocol extension!