Reputation: 9
I'm seeing some strange behavior at the interface of protocol extensions and generics. I'm new to Swift, so possibly I misunderstand, but I don't see how this can be correct behavior.
First let's define a protocol, and extend it with a default function implementation:
protocol Foo {
}
extension Foo {
static func yo() {
print("Foo.yo")
}
}
Now define a couple of conforming types:
struct A: Foo {
}
struct B: Foo {
static func yo() {
print("B.yo")
}
}
A.yo()
B.yo()
As expected, A.yo()
uses the default implementation of yo
, whereas B.yo()
uses the explicit implementation provided by B
: the output is
Foo.yo
B.yo
Now let's make a simple generic type:
struct C<T: Foo> {
static func what() {
T.yo()
}
}
C<A>.what()
C<B>.what()
C<A>.what()
prints Foo.yo
, as expected. But C<B>.what()
also prints Foo.yo
!
Surely the meaning of C<B>
is simply the template for C
with B
substituted in for the type parameter T
? Yet B
's version of yo
is not being called.
What am I missing? I'm using Swift 5.2.2.
Now, as it turns out you can fix this problem by declaring yo
in the original definition of Foo
. If we do this:
protocol Foo {
static func yo()
}
then C<B>.what()
works as I would expect, printing B.yo
. I can't understand the original behavior in the first place, but even less can I understand how this would change it.
In my actual application I can't use this fix, because I am extending a pre-existing protocol with a function that I want to specialize in a particular conforming type.
Upvotes: 0
Views: 90
Reputation: 63331
Generics are resolved at compile time. They're not dynamically dispatched like method calls on class hierarchies or protocols. That staticness is kind of their point, that's where the performance wins stem from.
As far as I can tell, Foo.yo()
and B.yo()
are totally unrelated functions. Calling Foo.yo()
does a statically dispatched call to Foo
, and likewise, calling B.yo()
causes a statically dispatched call to B
.
Yet, if you up-cast B.self
to a Foo.Type
, and you call yo()
on it, you end up with a statically dispatched call to Foo
:
(B.self as Foo.Type).yo()
To get dynamic dispatch (to achieve the kind of polymorphism you're after), you need to define yo
as a requirement of the protocol. That establishes a relationship between B.yo()
(which is now a part of the conformance to the protocol) and Foo.yo()
(which is a default implementation for types who don't provide their own).
protocol Foo {
// static func yo() // uncomment this
}
extension Foo {
static func yo() {
print("Foo.yo")
}
}
struct A: Foo {
}
struct B: Foo {
static func yo() {
print("B.yo")
}
}
struct C<T: Foo> {
static func what() {
T.yo()
}
}
A.yo()
B.yo()
(B.self as Foo.Type).yo()
C<A>.what()
C<B>.what()
Results before:
Foo.yo
B.yo
Foo.yo
Foo.yo
Foo.yo
Results after making yo
a requirement:
Foo.yo
B.yo
B.yo
Foo.yo
B.yo
Upvotes: 2
Reputation: 33
It’s hard to suggest a fix for your exact situation without more details of the exact situation- are you not able to provide these? Suffice to say this is the expected behaviour and its to do with some optimisations and assumptions the compiler makes.
You might want to check out this article on static vs dynamic dispatch in Swift: https://medium.com/@PavloShadov/https-medium-com-pavloshadov-swift-protocols-magic-of-dynamic-static-methods-dispatches-dfe0e0c85509
Upvotes: 0