Reasons to Include Function in Protocol Definition VS. Only Defining It in the Extension

Reasons to include function in protocol definition vs. only defining it in the extension?

Declaring the function as part of the protocol definition instructs the compiler to use dynamic dispatch when calling the function, as the compiler would expect types implementing the protocol to give an implementation for that function. This is called a method requirement. Now, if the type doesn't define the method, then the runtime resolves the method call to the method declared in the protocol extension.

However, declaring the function in the protocol extension only tells the compiler that he doesn't need to use the dynamic dispatch, and instead it uses the static dispatch, which is faster, but doesn't work very well with polymorphism, as the protocol extension implementation will be called even if the types conforming to the protocol also implement the method.

To exemplify the above, let's consider the following code:

protocol Shape {
func draw()
}

extension Shape {
func draw(){
print("This is a Shape")
}
}

struct Circle: Shape {
func draw() {
print("This is a Circle")
}
}

struct Square: Shape {
func draw() {
print("This is a Square")
}
}

let shapes: [Shape] = [Circle(), Square()]

for shape in shapes {
shape.draw()
}

The above code will have the output

This is a Circle 
This is a Square

This is because draw() is a method requirement, meaning that when draw() is invoked, the runtime will search for the implementation of draw () within the actual type of the element, in this case within Circle and Square.

Now if we don't declare draw as a method requirement, meaning we don't mention it within the protocol declaration

protocol Shape {
}

Then the compiler will no longer use the dynamic dispatch, and will go straight to the implementation defined in the protocol extension. Thus the code will print:

This is a Shape
This is a Shape

More, if we down cast cast an element of the array to the type we expect it would be, then we get the overloaded behaviour. This will print This is a Circle

if let circle = shapes[0] as? Circle {
circle.draw()
}

because the compiler is now able to tell that the first element of shapes is a Circle, and since Circle has a draw() method it will call that one.

This is Swift's way to cope with abstract classes: it gives you a way you to specify what you expect from types conforming to that protocol, while allowing for default implementations of those methods.

Why must Protocol defaults be implemented via Extensions in Swift?

The @optional directive is an Objective-C only directive and has not been translated to Swift. This doesn't mean that you can't use it in Swift, but that you have to expose your Swift code to Objective-C first with he @objc attribute.

Note that exposing to Obj-C will only make the protocol available to types that are in both Swift and Obj-C, this excludes for example Structs as they are only available in Swift!

To answer your first question, Protocols are here to define and not implement:

A protocol defines a blueprint of methods, properties, and other requirements [...]

And the implementation should thus be supplied by the class/stuct/enum that conforms to it:

The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements

This definition really applies to Protocols we use in our daily lives as well. Take for example the protocol to write a Paper:

The PaperProtocol defines a paper as a document with the following non-nil variables:

  • Introduction
  • Chapters
  • Conclusion

What the introduction, chapters and conclusion contain are up to the one implementing them (the writer) and not the protocol.

When we look at the definition of Extensions, we can see that they are here to add (implement) new functionalities:

Extensions add new functionality to an existing class, structure, enumeration, or protocol type. This includes the ability to extend types for which you do not have access to the original source code.

So extending a protocol (which is allowed) gives you the possibility to add new functionality and hereby give a default implementation of a defined method. Doing so will work as a Swift only alternative to the @optional directive discussed above.

UPDATE:

While giving a default implementation to a protocol function in Switch does make it "optional", it is fundamentally not the same as using the @optional directive in Objective-C.

In Objective-C, an optional method that is not implemented has no implementation at all so calling it would result in a crash as it does not exist. One would thus have to check if it exists before calling it, in opposition to Swift with an extension default where you can call it safely as a default implementation exists.

An optional in Obj-C would be used like this:

NSString *thisSegmentTitle;
// Verify that the optional method has been implemented
if ([self.dataSource respondsToSelector:@selector(titleForSegmentAtIndex:)]) {
// Call it
thisSegmentTitle = [self.dataSource titleForSegmentAtIndex:index];
} else {
// Do something as fallback
}

Where it's Swift counterpart with extension default would be:

let thisSegmentTitle = self.dataSource.titleForSegmentAtIndex(index)

How to force usage of protocol default extension override ?

I tried a few things, and somehow this works:

Declare contains in the protocol, not in an extension, like this:

func contains<T>(_ e: T) -> Bool where T: Equatable, T == BagObject

If you do things like T == BagObject in a class, a compiler error will show up saying that this makes T redundant, but apparently this is okay in a protocol.

Then, implement the default implementation like this:

extension Bag {
func contains<T>(_ e: T) -> Bool where T: Equatable, T == BagObject {
print("Default implementation is called")
return false
}
}

Then, you can just implement the concrete implementation directly in the class, as a non-generic method.

class BagConcreteImplementation<BagObject> : Bag {
func contains(_ e: BagObject) -> Bool where BagObject : Equatable {
print("Concrete implementation is called")
return bag.contains(e)
}

var bag = Array<BagObject>()
func add(_ e: BagObject) { bag.append(e) }
}

Without changing the call site, the code would print:

Concrete implementation is called
Concrete implementation is called

Why would we use extensions?

Per Swift documentation on Protocol Extensions:

Protocols can be extended to provide method, initializer, subscript,
and computed property implementations to conforming types. This allows
you to define behavior on protocols themselves, rather than in each
type’s individual conformance or in a global function.

This means you can run logic within the protocol extension function so you don't have to do it in each class that conforms to the protocol.

Personally, I also find extensions useful to extend the functionality of built-in classes like String or UIViewController since extensions can be called from anywhere in an app. I have some open-source extension snippets you can take a look at if you'd like.



Related Topics



Leave a reply



Submit