Swift Polymorphism

Polymorphism lets you treat objects of different but related types through a single common interface, so the correct implementation runs automatically at call time.

What Is Polymorphism

Polymorphism means "many forms": a single piece of code can operate on values of several different concrete types as long as they share a common supertype or protocol. In Swift this shows up whenever you store subclass instances in an array of their superclass type, or store different conforming types in an array of a shared protocol type.

Dynamic Dispatch with Classes

When you call an overridden method through a superclass-typed reference, Swift looks up the actual runtime type of the instance and calls that type's implementation. This is called dynamic dispatch, and it's what makes a loop over [Employee] automatically call Manager.pay() or Developer.pay() depending on what's really stored in each array slot.

  • Subtype polymorphism: a subclass instance can be used wherever its superclass type is expected.
  • Protocol-based polymorphism: any conforming type can be used wherever the protocol type is expected.
  • Parametric polymorphism: generic functions and types work uniformly across many types via type parameters.
  • Ad-hoc polymorphism: function overloading lets the same function name behave differently per parameter type.

Polymorphism through class inheritance

class Employee {
    let name: String
    init(name: String) { self.name = name }
    func pay() -> Double { 0 }
}

class Manager: Employee {
    override func pay() -> Double { 6000 }
}

class Developer: Employee {
    override func pay() -> Double { 5000 }
}

let staff: [Employee] = [Manager(name: "Asha"), Developer(name: "Ben")]

for person in staff {
    print("\(person.name) earns \(person.pay())")
}
// Asha earns 6000.0
// Ben earns 5000.0

Polymorphism through a protocol

protocol Shape {
    func area() -> Double
}

struct Circle: Shape {
    var radius: Double
    func area() -> Double { .pi * radius * radius }
}

struct Rectangle: Shape {
    var width: Double
    var height: Double
    func area() -> Double { width * height }
}

let shapes: [Shape] = [Circle(radius: 2), Rectangle(width: 3, height: 4)]

let totalArea = shapes.reduce(0) { $0 + $1.area() }
print("Total area: \(totalArea)")
Note: Structs can be just as polymorphic as classes when they conform to a shared protocol. The dynamic behavior comes from the protocol's witness table, not from class inheritance, so you get polymorphism without giving up value semantics.

Type Casting: is, as?, and as!

When a collection is typed as a common supertype or protocol, you sometimes need to recover the specific concrete type. The is operator checks a type without casting, as? performs a safe, optional downcast, and as! forces the downcast and crashes if it's wrong.

Safely downcasting with as?

class Media { let title: String; init(title: String) { self.title = title } }
class Movie: Media { var director = "" }
class Song: Media { var artist = "" }

let library: [Media] = [Movie(title: "Interstellar"), Song(title: "Yesterday")]

for item in library {
    if let movie = item as? Movie {
        print("\(movie.title) is a movie")
    } else if let song = item as? Song {
        print("\(song.title) is a song")
    }
}
DispatchApplies ToWhen Chosen
Dynamic dispatchClass methods, overridden methodsResolved at runtime based on the actual instance type
Static dispatchStructs, enums, and calls made directly on a concrete typeResolved at compile time
Protocol witness dispatchProtocol requirements called through a protocol-typed valueResolved at runtime via a witness table
Note: as! performs a forced downcast: if the actual type doesn't match, your program crashes immediately. Prefer as? with optional binding unless you have already proven the type with is or as? earlier in the same scope.

Exercise: Swift Polymorphism

What does polymorphism mean in Swift's class hierarchies?