Let's say I want to define a strongly typed UIView
wrapper that adds padding to another view:
public final class PaddedView<Content: UIView>: UIView {
public init(
content: Content,
padding: UIEdgeInsets
) {
self.content = content
self.padding = padding
addSubview(content)
// Set up constraints
}
public let content: Content
public let padding: UIEdgeInsets
}
Then I want to make a convenient extension to pad a view:
public extension UIView {
func padded(with padding: UIEdgeInsets) -> PaddedView<Self> {
.init(content: self, padding: padding)
}
}
This does not compile, and produces the following error:
Covariant 'Self' or 'Self?' can only appear at the top level of method result type
I recently learned of a trick to work around this (by investigating how the publisher<T>(for: KeyPath<Self, T>)
extension on NSObject
is implemented). We introduce a protocol representation of the class:
public protocol UIViewProtocol: UIView {}
Then extend the class to conform to the protocol:
extension UIView: UIViewProtocol {}
Then change the generic parameter and the extension to use the protocol:
public final class PaddedView<Content: UIViewProtocol>: UIView {
...
}
public extension UIViewProtocol {
func padded(with padding: UIEdgeInsets) -> PaddedView<Self> {
.init(content: self, padding: padding)
}
}
Covariant Self
is allowed to appear anywhere in a function signature on a protocol, so this now compiles, and it achieves the intended result (if I call let result = something.padded(padding: ...)
, I can query result.content
and get back the correctly typed something
).
That made me curious: why does this restriction exist on classes but not on protocols? It seems, although I haven't thought deeply about it, than any time you'd get stuck on this limitation with classes, you can introduce this protocol equivalent trick and surmount it, which makes it seem like the compiler has already solved the problem it would need to solve to allow the same with the class directly.
Maybe it has to do with existentials? What happens if I had an any UIViewProtocol
and I tried to call .padded
on it? Well I can't. Generic parameters can't be covariant, so this appearance of Self
is in a non-covariant position, which means this extension gets deleted from the existential. Existentials of protocols that use covariant Self
didn't exist at all until Swift 5.7, and extensions that introduced such a use were deleted from existentials. Perhaps forbidding anything but the only possible covariant ("top level" of return value) use of Self
in a class extension was an older approach to avoiding this problem, on par with forbidding the comparable protocol existentials entirely?
But what's interesting is you can still "erase" a subclass up to its base class: let erasedView: UIView = UILabel()
, and the extension is available, and you'll get an instance of PaddedView<UIView>
back (that's not just the static type of the return value but its actual dynamic type too). So apparently the existential that gets opened when going into this extension is the static type if that's a class
, but the dynamic type if it's a protocol
. The extension isn't available on a protocol existential here, but would be if I changed the return type to UIView
. Then if I declared erasedView
as an any UIViewProtocol
, the result of erasedView.padded
would have a static type of UIView
but a dynamic type of PaddedView<UILabel>
. This does not happen if I make the return type UIView
and extend UIView
instead of UIViewProtocol
(the static type would be UIView
and the dynamic type would be Padded<UIView>
). So introducing the protocol enables me to at least end up with the "correct" dynamic type (bound to the actual dynamic type of the receiver), which isn't possible if I extend the class.
Why couldn't it (or shouldn't it) work that way with the class directly, where it would simply bind the generic parameter of the return type to the known static type of whatever the extension is called on? That is, why can't I put this extension on UIView
, and then when I call something.padded
, it just binds Self
to the static type of something
?
Does it have something to do with the way dispatch is implemented? I've been caught off guard before by how dispatch works in conditional extensions on generic types, and concluded that generic concrete types must have a singular representation under the hood: one method table (for both statically and dynamically dispatched member functions) for the type itself, which is very different than, say, C++, where each specialization of a template class is its own full-blown class with its own compiled methods (that makes constrained method overloading possible). I may be remembering this incorrectly, but I recall being unable to make conditional overloading work on a generic struct
, but when I replaced the generic struct
with a protocol
with an associatedtype
and hung the extensions off of that, I was able to make it work.