Please consider the following code:
protocol P {}
class X {}
class Y: P {}
func foo<T>(_ closure: (T) -> Void) { print(type(of: closure)) }
func foo<T>(_ closure: (T) -> Void) where T: P { print(type(of: closure)) }
let xClosure: (X?) -> Void = { _ in }
foo(xClosure) // prints "(Optional<X>) -> ()"
let yClosure: (Y?) -> Void = { _ in }
foo(yClosure) // prints "(Y) -> ()"
Why does the foo(yClosure)
call resolve to the version of foo
constrained to T: P
?
I understand why that version prints what it prints,
what I don't see is why it gets called instead of the other one.
To me it seems that the non-P version would be a better match for T == (Y?) -> Void
.
Sure, the constrained version is more specific, but it requires conversion
(an implicit conversion from (Y?) -> Void
to (Y) -> Void
),
while the non-P version could be called with no conversion.
Is there a way to fix this code in a way such that the P-constrained version gets called only if the parameter type of the passed-in closure directly conforms to P, without any implicit conversions?
Specificity seems to always trump variance conversions, according to my experiments. For example:
func bar<T>(_ x: [Int], _ y: T) { print("A") }
func bar<T: P>(_ x: [Any], _ y: T) { print("B") }
bar([1], Y()) // A
bar
is more specific, but requires a variance conversion from [Int]
to [Any]
.
For why you can convert from (Y?) -> Void
to (P) -> Void
, see this. Note that Y
is a subtype of Y?
, by compiler magic.
Since it is so consistent, this behaviour seems to be by design. Since you can't really make Y
not a subtype of Y?
, you don't have a lot of choices if you want to get the desired behaviour.
I have this work around, and I admit it's really ugly - make your own Optional
type. Let's call it Maybe<T>
:
enum Maybe<T> {
case some(T)
case none
// implement all the Optional methods if you want
}
Now, your (Maybe<Y>) -> Void
won't be converted to (P) -> Void
. Normally I wouldn't recommend this, but since you said:
in the real-world code where I encountered this, the closure has multiple params, any of them can be optional, so this would lead to a combinatorial explosion.
I thought reinventing Optional
might be worth it.