Start learning free

LESSON 407 OF 415 · OBJECT-ORIENTED PROGRAMMING

Polymorphism in Swift: One Call, Many Behaviors

Lesson Read the full explanation

TL ; DR:

Polymorphism is when a single piece of code (like animal.speak()) does different things depending on which subclass the object actually is. You write the call once, and the right override runs automatically.


let animals: [Animal] = [Dog(), Cat()]
for a in animals {
    a.speak()   // dispatches to Dog.speak then Cat.speak
}


Full lesson:

Polymorphism (Greek for "many forms") is what makes inheritance and interfaces actually useful at scale. Once Dog and Cat both inherit from Animal and override speak(), you can stop caring which one you're holding. Any code that takes an Animal and calls speak() works for both.

Why it matters

Imagine you're writing a function that prints what every animal in a zoo says:

func roll(_ animals: [Animal]) {
    for a in animals { a.speak() }
}

This function works for any current or future kind of animal. Add a Cow subclass tomorrow — this loop runs Cow.speak() automatically, no edits needed. That's the win: code that works today and keeps working as the type hierarchy grows.

Static type vs dynamic type

The reference is typed as Animal. The object it actually points to is a Dog, or a Cat, etc. Polymorphism is the language dispatching the method call based on the dynamic type (what it is at runtime) rather than the static type (what the variable was declared as).

Notes about Swift

  • Methods of a class are dispatched dynamically by default, so there is no virtual keyword: the subclass just writes override func and the right version runs.
  • A method marked final (or a final class) cannot be overridden, so it is not dispatched dynamically.

Your task

Animal, Dog, and Cat are already defined, with the right overrides. Inside mainFunction, build a collection containing one Dog and one Cat (typed as Animal), then loop through it calling speak() on each.

Expected output:

Woof
Meow
STEP 1 OF 2
    Ctrl + Enter to check

    SWIFT · OBJECT-ORIENTED PROGRAMMING

    Polymorphism in Swift, explained

    What polymorphism means

    Polymorphism, from the Greek for "many forms", lets one piece of code work with objects of different classes. You write the call once, and Swift runs the version of the method that belongs to the object's actual class.

    The idea needs a parent class and subclasses that override one of its methods. Here Animal defines speak(), and Dog and Cat each replace it:

    class Animal {
        func speak() {
            print("...")
        }
    }
    
    class Dog: Animal {
        override func speak() {
            print("Woof")
        }
    }
    
    class Cat: Animal {
        override func speak() {
            print("Meow")
        }
    }
    

    Any code that receives an Animal can call speak() without knowing which kind it holds. That is the payoff: a function written today keeps working when you add a Cow subclass tomorrow, with no edits to the function.

    Static type and dynamic type

    Every variable has a type written in the code, its static type. The object it refers to has a real class at run time, its dynamic type. With a subclass instance stored under the parent type, the two differ:

    let pet: Animal = Dog()
    pet.speak()
    print(type(of: pet))
    
    Woof
    Dog
    

    The compiler only knows pet is an Animal, so it only lets you use what Animal offers. At run time the call goes to Dog.speak(), because that is what the object is. Swift picks the method from the dynamic type.

    Swift allows the assignment because a Dog is an Animal: a subclass instance can be used wherever its superclass type is expected. No cast is needed.

    Collections of mixed types

    Polymorphism is most useful with a collection. An array typed [Animal] can hold a Dog next to a Cat:

    func roll(_ animals: [Animal]) {
        for a in animals { a.speak() }
    }
    
    let animals: [Animal] = [Dog(), Cat()]
    roll(animals)
    roll(animals + [Cow()])
    

    This prints Woof and Meow, then Woof, Meow and Moo, assuming Cow is another subclass that overrides speak() to print Moo. The loop inside roll never mentions a specific class.

    The type annotation matters. Without it, [Dog(), Cat()] still works, because Swift infers the shared parent Animal as the element type, but writing [Animal] states the intent and keeps the array open for other subclasses.

    The lesson's exercise

    The starter code already contains Animal, Dog and Cat with their overrides. The only work is inside mainFunction of the Solution class. The output must be Woof and then Meow.

    1. Declare the array. Make a constant animals of type [Animal] holding a Dog() and a Cat(), in that order. A let is enough, since nothing is added later.
    2. Loop over it. Write for a in animals and call a.speak() inside. The first pass reaches Dog.speak() and the second reaches Cat.speak().

    The complete solution:

    class Animal {
        func speak() {
            print("...")
        }
    }
    
    class Dog: Animal {
        override func speak() {
            print("Woof")
        }
    }
    
    class Cat: Animal {
        override func speak() {
            print("Meow")
        }
    }
    
    class Solution {
        func mainFunction() {
            let animals: [Animal] = [Dog(), Cat()]
            for a in animals {
                a.speak()
            }
        }
    }
    

    If the loop printed ... twice, the calls would be using Animal's own method. That does not happen in Swift for overridden class methods, which is the point of the exercise.

    Swift details that matter

    • Overrides need override. Writing func speak() in a subclass without the keyword is a compile error, which protects you from accidentally replacing a method.
    • Class methods are dynamically dispatched. Unlike in some languages, you do not mark the parent method with virtual. Swift decides at run time for methods of a class, unless the method or class is marked final, which forbids overriding.
    • Not an override means no dispatch. A subclass that does not override speak() inherits the parent's version. A Plain: Animal subclass with an empty body prints ....
    • Checking the real class. Use is to test it and as? to get an optional of the subclass type. pet as? Dog gives a Dog? holding the object, while pet as? Cat gives nil. Needing this often suggests the behavior should be an overridden method instead.
    • Structs have no subclasses. Subclass polymorphism belongs to classes. For structs and enums, polymorphism comes through protocols, which a later lesson covers.
    • Protocol extensions can surprise you. When a protocol adds a method in an extension without declaring it as a requirement, a call through the protocol type uses the extension's version even if the concrete type defines its own. Declare the method in the protocol itself to get dynamic behavior.

    Key takeaways

    • Polymorphism means the same call runs different code depending on the object's actual class.
    • A subclass instance can be stored under its superclass type, such as [Animal] holding a Dog and a Cat.
    • Write code against the parent type, and new subclasses work without changes.
    • Swift needs override in the subclass and no special marker in the parent.
    Keep going with SwiftAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free