DEV Community

Gamya
Gamya

Posted on

Swift Classes โ€” Inheritance, Override, and the final Keyword ๐Ÿงฌ

What if you could take an existing class and say "I want everything this already does, plus a few things of my own"? That's inheritance. Let's talk about it.

Okay so here's a thought experiment.

Imagine you're building an anime RPG. You need a Ninja class. You create it, give it a name, a village, and a method called fight(). Great. Done.

Then you realise you also need a Hokage class. A Hokage is still a ninja โ€” they have a name, they belong to a village, they can fight. But they also have extra responsibilities. They lead. They make speeches. They sign off on missions.

Now you have a choice. You could copy and paste everything from Ninja into Hokage. But that's a nightmare โ€” every time you update Ninja, you'd have to update Hokage too, and if you forget, the two drift apart and weird bugs show up.

Or you could use inheritance. Which is Swift saying: "Just tell me what's the same and what's different, and I'll handle the rest." ๐Ÿฅ


How Inheritance Works

Here's the base class โ€” the parent (or "superclass"):

class Ninja {
    let name: String
    let village: String

    init(name: String, village: String) {
        self.name = name
        self.village = village
    }

    func fight() {
        print("\(name) engages in battle!")
    }

    func printSummary() {
        print("\(name) is a ninja from \(village).")
    }
}
Enter fullscreen mode Exit fullscreen mode

Now here's a child class (or "subclass") that inherits from it:

class Hokage: Ninja {
    var title: "String"

    init(name: String, village: String, title: "String) {"
        self.title = title
        super.init(name: name, village: village)
    }

    func giveOrder() {
        print("\(title) \(name) commands: protect the village!")
    }
}
Enter fullscreen mode Exit fullscreen mode

The : Ninja after Hokage is what makes it a subclass. That one colon gives Hokage everything Ninja has โ€” name, village, fight(), printSummary() โ€” for free, without copying a single line.

let naruto = Hokage(name: "Naruto", village: "Konoha", title: "\"Seventh Hokage\")"
naruto.fight()        // "Naruto engages in battle!" โ€” inherited from Ninja
naruto.giveOrder()    // "Seventh Hokage Naruto commands: protect the village!"
naruto.printSummary() // "Naruto is a ninja from Konoha." โ€” also inherited
Enter fullscreen mode Exit fullscreen mode

naruto is a Hokage, but Swift knows he's also a Ninja, so all the Ninja methods work on him without any extra effort.


What Happens When You Want to Change an Inherited Method?

Here's where it gets interesting. What if Hokage's version of printSummary() should say something different from Ninja's version? After all, "Naruto is a ninja from Konoha" is technically accurate but undersells the situation a bit.

This is called overriding โ€” replacing a parent's method with your own version. And Swift makes you be very explicit about it by requiring the override keyword:

class Hokage: Ninja {
    var title: String

    init(name: String, village: String, title: String) {
        self.title = title
        super.init(name: name, village: village)
    }

    func giveOrder() {
        print("\(title) \(name) commands: protect the village!")
    }

    override func printSummary() {
        print("\(name) is the \(title) of \(village) โ€” show some respect.")
    }
}
Enter fullscreen mode Exit fullscreen mode

Now when you call printSummary() on a Hokage, you get the Hokage version. Call it on a plain Ninja, you get the Ninja version. Swift figures out which one to use based on what the object actually is.


Why Does Swift Force You to Write override?

This might seem like extra typing for no reason, but override is actually doing two important jobs at once.

Job 1 โ€” It stops accidental overrides.

Imagine you create a method in your subclass called printSummary() not realising the parent already has one. Without override, you'd silently replace the parent's behaviour and wonder why things stopped working. With override required, Swift stops you: "Hey, you're replacing something that exists. Was that on purpose?"

Job 2 โ€” It stops override typos.

Imagine you meant to override printSummary() but you accidentally typed printSumary() (one m). Without the override check, Swift would just create a brand new method with a typo in the name and your override would silently do nothing. With override, Swift goes: "You said override, but there's nothing in the parent called printSumary. Check your spelling."

It's one keyword that catches two completely different categories of mistakes. Not bad.


What About Adding to an Inherited Method Instead of Replacing It?

Sometimes you don't want to completely replace the parent's method โ€” you just want to run the parent's version and then add something on top. You can do that with super:

override func printSummary() {
    super.printSummary() // runs Ninja's version first
    print("...and also the \(title) of \(village).")
}
Enter fullscreen mode Exit fullscreen mode

super refers to the parent class. Calling super.printSummary() runs whatever Ninja defined, and then you can add your own lines after it.


The final Keyword โ€” "Nobody Inherits From This"

Sometimes you build a class and you know, very firmly, that nobody should ever subclass it. Maybe it does something critical where overriding the wrong method would cause weird, hard-to-debug behavior. Maybe you just don't want to support that level of customization.

That's what final is for:

final class Akatsuki {
    var name: String

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

    func revealPlan() {
        print("The plan... is Pain's.")
    }
}
Enter fullscreen mode Exit fullscreen mode

Mark a class as final and Swift will refuse to let anyone inherit from it. Try it and you'll get a compile error immediately.

Apple uses this in their own frameworks. There's a class called AVPlayerViewController โ€” for playing videos โ€” where Apple's documentation essentially says "do not subclass this, things will break in undefined ways." In older Objective-C code they had to say that in plain English and hope developers read it. In Swift, final enforces it in the compiler.

Think of final as documentation you can't ignore. It doesn't just tell future developers not to subclass โ€” it makes it physically impossible.


A Quick Recap

Concept What It Means
Inheritance A child class gets all properties and methods from its parent
: ParentClass The syntax that makes one class inherit from another
override Required when replacing a parent's method with your own version
super Calls the parent class's version of a method
final Prevents anyone from inheriting from this class

The One Thing To Hold Onto

Inheritance is powerful, but it's also easy to over-use. The guideline most Swift developers follow is: only inherit when the child genuinely is a more specific version of the parent. A Hokage is a Ninja โ€” that's genuine. A Village is not a Ninja โ€” that would be strange and wrong.

If you're not sure whether inheritance is the right tool, it probably isn't. Composition (putting objects inside other objects) is usually cleaner. But when inheritance is the right call, override and final are there to make sure it doesn't quietly go sideways on you. ๐ŸŒธ


This article was written by me; AI was used to improve grammar and readability.


Top comments (2)

Collapse
 
vinimabreu profile image
Vinicius Pereira

"Documentation you can't ignore" is a good frame, and final has a second benefit worth knowing: it helps the compiler too. A call on a non-final method may need dynamic dispatch, since any subclass could override it; mark it final and the compiler can resolve the call statically, which is faster. It is also why a reasonable default is to declare classes final from day one and only remove it when a subclass genuinely earns its place. Supporting inheritance is a commitment, every overridable method becomes an API you have to keep safe, so opting into it deliberately beats leaving the door open by accident.

Collapse
 
gamya_m profile image
Gamya

The compiler optimization angle is one I hadn't thought to include and it's a genuinely useful extra reason to reach for final by default โ€” not just as intent communication but as a performance hint the compiler can actually act on. Dynamic dispatch being the cost of leaving the door open is a nice way to frame it too, because it makes the trade-off concrete rather than just philosophical.

"Supporting inheritance is a commitment" is probably the line that should go in the article honestly โ€” because the way it's usually taught, final feels like a restriction you add when you want to prevent something. Flipping it to "inheritance is an opt-in commitment you make deliberately" completely changes the default posture, and that's a much healthier way to think about it as a codebase grows. Really appreciate you adding this.