Swift's final
keyword is often overlooked or dismissed as not necessary. Applying the final
keyword is a subtle technique to optimize your code in a powerful way. Let me explain what I mean by that.
Inheritance
If we discuss the final
keyword, then we also need to talk about inheritance. It makes no sense to prefix a type definition of a value type with the final
keyword. In fact, the compiler will throw a compile time error if you do.
Why is that? If you prefix a class definition with final
, you prevent anyone from subclassing that class. Value types don't support inheritance so it doesn't make sense to mark a struct or enum as final
.
The final
keyword communicates to the compiler that the class cannot and should not be subclassed. Applying the final
modifier to a class definition is a clear signal.
final class Person {
}
Applying final
to a class isn't the only option you have. It is possible to apply final
in a more subtle form. Any method, property, or subscript that is prefixed with the final
modifier cannot be overridden. This is useful if a property, method, or subscript cannot be declared as private
or fileprivate
, but you don't want subclasses to override it.
class Car {
// MARK: - Properties
var name: String
// MARK: -
final var canDrive: Bool
...
}
Whole Module Optimization
The compiler marks entities as final
if it knows it can safely do so. What do I mean by that? The compiler uses Whole Module Optimization to optimize the code you write. As the name suggests, it inspects and evaluates the code at the level of the module.
When Whole Module Optimization is enabled, the compiler evaluates each file in the context of the module. It puts the pieces of your project together to better understand how it is architected. Take a look at this example.
We define a class with name Car
. Car
defines a property, name
, of type String
and a method with name drive()
.
class Car {
// MARK: - Properties
var name: String {
"Car"
}
// MARK: - Methods
func drive() {
// ...
}
}
Car
has a subclass, FastCar
. FastCar
overrides the name
property, but it doesn't override the drive()
method.
class FastCar: Car {
// MARK: - Properties
override var name: String {
"Fast Car"
}
}
When Whole Module Optimization is enabled, the compiler inspects the implementations of Car
and FastCar
and looks for ways to optimize. It notices that FastCar
subclasses Car
and that it overrides the name
property. Because FastCar
doesn't override the drive()
method, the compiler can infer final
on the drive()
method.
class Car {
// MARK: - Properties
var name: String {
"Car"
}
// MARK: - Methods
final func drive() {
// ...
}
}
Even though we forgot to apply the final
keyword to the drive()
method, the compiler didn't. This is one example. The compiler performs many more optimizations to create a build that is fast and performant.
Does this mean that you don't need to apply the final
keyword? Absolutely not. Don't forget what I wrote earlier. The final
keyword has several applications and optimizing performance is only one aspect.
Whole Module Optimization is disabled by default for debug builds. This means that debug builds take less time to compile, but it implies that debug builds aren't as optimized as distribution builds.
Need Help?
I use SwiftLint in most of the projects I work on because it is easy to forget best practices. It happens—more than I care to admit—that I forget to mark a class that isn't subclassed as final
. SwiftLint kindly reminds me with a warning.