Usage of extensions for protocol conformance in owned objects

Hey!

To start with, I'd like to mention that I'm relatively new to Swift. Saying that, I have about a decade of experience as a developer, where I've used various languages and frameworks and overall I'm pretty familiar with software engineering principles, best practices and design patterns.

In Swift, I've seen a lot of code and examples where protocol conformance is done using extensions. Now, I do understand (and also the docs are very clear on this one) that it can be very useful when working with objects we do not own - such as 3rd part libraries, etc. What I'm struggling to understand is what are the benefits of doing so for objects I do own.

To clarify further, here's a simple example:

Usually when I'm writing a class, and I want it to conform to a protocol (or implement an interface) I'd write something like this:

protocol MyProtocol {
    func foo() -> Int
}

class MyClass : MyProtocol {
    func foo() -> Int {
        return 10
    }
}

I think it is very clear to someone who opens the file and reads the code, that the class conforms to MyProtocol protocol.

On the other hand, in many cases I see this:

protocol MyProtocol {
    func foo() -> Int
}

class MyClass {
    
}

extension MyClass: MyProtocol {
    func foo() -> Int {
        return 10
    }
}

At times, the extension isn't even declared at the same file the object is declared at.

I do not see the benefits of doing so, on the contrary, I feel it makes the code harder to understand and perhaps, in some way, violating the Open-Closed principle from SOLID (

software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification

)

Here's an example of the issue:

protocol MyProtocol {
    func foo() -> Int
}

class MyClass {
    
}

extension MyClass: MyProtocol {
    func foo() -> Int {
        return 10
    }
}

class MyOtherClass: MyClass {
    // foo here cannot be changed or overridden
    func foo() -> Int {
        return 5
    }
}

It is not possible to override foo inside MyOtherClass, which is expected to be possible.

To summarize, I'd love to hear what the benefits of using extensions for protocol conformance in owned objects is. To me it has a bit of a code smell at this point. Would love to learn otherwise :slight_smile:

Thank you for your time and input.

It's at least partly about code organization. Sometimes in the same file, sometimes spread across several files. Look here in the swift-collections package; at the OrderedDictionary type. All those files define the functionality of one, complex type, but each file tells you where the code related to various conformances is located. Extensions make that possible. If all those conformances were in one file, in one type definition, it would be thousands of lines long and readability would suffer.

1 Like

I'd argue extensions are an example of that principle, not a violation of it. Extensions cannot add stored members, they can only work with what's already there (or other extensions they can see).

2 Likes

Thank you, it is a nice example you have provided. I do see value for doing so when handling very long or complex objects.

I'm curious, how do you feel regarding smaller and less complex objects? Do you feel that at some point this can be considered as overuse / abuse ?

The extension itself doesn't disturb the Open-Closed principle, however it leads to a situation where the class you have extended is a bit limited in case of inheritance.

Just sharing my thoughts :slight_smile:

Another important use case for extension conformance is when implementing delegate/data source protocols. By defining the conformance in an extension, it is possible to keep the conformance together. The compiler assists this by generating the necessary conformance methods.

This is a frequent question. See previous discussions like

This feature I don't like or understand, it does look like a bug... It is possible for @objc methods but not otherwise. What is the reason for that restriction?

Ignoring optimizations, @objc methods are found at run time by dynamic lookup in a table keyed by selector (method name), which is constructed by checking every extension. Swift methods are found at run time by lookup into an offset-based table thatā€™s attached directly to the class object and thus canā€™t be modified once built. This is faster and uses less memory, but has more limitationsā€”in particular, cross-module extensions canā€™t really get into this table because the code to build it is part of the module that owns the class.

(This is a high-level summary; itā€™s a little more complicated than this in practice.)

Not supporting overridable methods in extensions within the module is a different question thatā€™s mostly policy rather than technical, but it started off as ā€œwe never got around to implementing itā€.

3 Likes

One benefit you get from declaring protocol conformances in an extension is that the compiler will check your conformances for you. So if you have a protocol like this:

protocol P {
    var v: Int { get }
}
extension P {
    var v: Int { 0 }
}

and you conform to it like this:

struct S {}
extension S: P {
    var v: Double { 3 }
}

then it will give you a warning: property 'v' nearly matches defaulted requirement 'v' of protocol 'P'. If you conform to it like this:

struct S: P {
    var v: Double { 3 }
}

Then you'll get no warning.

While this example is a bit silly, these warnings can be very helpful when you have complex associated type relationships.

3 Likes

Indeed that a member defined in an extension in the same module (or in the same file!) behaves differently than a ember defined in the type itself ā€“ feels an artificial limitation that could be fixed.

This is a quintessential example:

E.g. I did a mere refactoring and this starts giving a warning and if I use treat warnings as errors I can't ignore the warning. I view this as a bug or a very unfortunate feature of current Swift.

I'd say it should be have the same (warning in both cases or no warning in both cases).

A slight tangent

I'd go one step further: inability to put variables in extensions defined in the same module is also an artificial limitation:

import Foundation
import SwiftUI
import UIKit
import RealityKit

class C: NSObject {
}

// Same file of a different file in the same module - ok āœ…
extension C: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 10 }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
    }
}

// Same file of a different file in the same module - ok āœ…
extension C: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    }
}

// Same file of a different file in the same module - not OK šŸ˜¢
// šŸ›‘ Extensions must not contain stored properties
extension C: AnimationDefinition {
    var name: String = ""
    var blendLayer: Int32 = 0
    var fillMode: AnimationFillMode = .both
    var bindTarget: BindTarget = .jointTransforms
    var trimStart: TimeInterval?
    var trimEnd: TimeInterval?
    var trimDuration: TimeInterval?
    var offset: TimeInterval = 0
    var delay: TimeInterval = 0
    var speed: Float = 0
    var repeatMode: AnimationRepeatMode = .repeat
    var duration: TimeInterval = 0
}

It's deliberate that you won't get a warning in both cases; there's no way to silence the warning if you didn't want a match otherwise.

I don't want the match here:

class C {
    var v: Int { 0 }
}
class D: C {
    var v: Double { 3 } // šŸ›‘ Property 'v' with type 'Double' cannot override a property with type 'Int'
}

but that's not possible. We could've done the same with protocols for consistency, no?


In fairness...
class C {
    func v() -> Int { 0 }
}
class D: C {
    func v() -> Double { 3 } // āœ…
}

Consistency is not a strong feature of Swift :thinking: