Take the following protocol and extension:
protocol ProtocolA {
func myFunc()
}
extension ProtocolA {
func myFunc() {
print("Default ProtocolA implementation.")
}
}
What is the difference, if any, between that and leaving the function out of the protocol definition entirely, like this:
protocol ProtocolB { }
extension ProtocolB {
func myFunc() {
print("Default ProtocolB implementation.")
}
}
I found one difference. If I define a struct that overrides the default implementation, I can only cast it to the protocol and call the protocol's implementation if I leave the function out of the definition:
struct A: ProtocolA {
func myFunc() {
print("Struct A's implementation.")
}
}
struct B: ProtocolB {
func myFunc() {
print("Struct B's implementation.")
}
}
A().myFunc() // "Struct A's implementation."
(A() as ProtocolA).myFunc() // "Struct A's implementation."
B().myFunc() // "Struct B's implementation."
(B() as ProtocolB).myFunc() // "Default protocol implementation."
In other words, if you take the function out of the protocol definition like in ProtocolB
then you can access the default implementation by casting the object to the protocol. On the other hand, if you leave the function in the protocol definition, then you can't cast to the protocol in order to get the default protocol behavior.
Leaving the function definition out of the protocol seems to allow for the most flexibility in terms of behavior.
What is the downside? What do you lose if you take the function out of the protocol definition? Do you lose any functionality at all?
Protocols let you describe what methods something should have, but don't provide the code inside. Extensions let you provide the code inside your methods, but only affect one data type – you can't add the method to lots of types at the same time.
Protocol composition is the process of combining multiple protocols into a single protocol. You can think of it as multiple inheritance. With protocol DoSomething , below, class HasSomethingToDo is able to do whatShouldBeDone and anotherThingToBeDone .
If you define a protocol instance method requirement that's intended to mutate instances of any type that adopts the protocol, mark the method with the mutating keyword as part of the protocol's definition. This enables structures and enumerations to adopt the protocol and satisfy that method requirement.
A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With