Apple Swift version 5.4.2 (swiftlang-1205.0.28.2 clang-1205.0.19.57)
Can someone explain to me what is happening here and why the constrained method is not called?
class MyTest<Output> {
let myVar: Output
init(myVar: Output) {
self.myVar = myVar
}
func foo() {
bar()
}
func bar() {
print("'bar' without generic constraint")
}
func bar() where Output == Int {
print("'bar' constrained to 'Int' output")
}
}
let myTest1 = MyTest(myVar: 5)
myTest1.foo()
let myTest2 = MyTest(myVar: "String")
myTest2.foo()
The output is as follows:
'bar' without generic constraint
'bar' without generic constraint
Why is the 'bar' method with the generic constraint to the 'Int' type not called by 'myTest1.foo()' ?
Where does Swift lose the type info?
It gets really absurd when you call "bar" directly:
let myTest1 = MyTest(myVar: 5)
myTest1.bar()
let myTest2 = MyTest(myVar: "String")
myTest2.bar()
This leads to the following output:
'bar' constrained to 'Int' output
'bar' without generic constraint
I.e. in this case the type constrained method is being called.
I really don't understand what is going on here and why. It's like a method call inside the class type itself suddenly loses the type info and calls the non-constrained method.
You're expecting dynamic dispatch, but in swift dynamic dispatch happens only in a few places:
protocol conformance
overrides in subclasses
objc stuff
In all other cases, the methods are dispatched statically, which means that in line 9 compiler has to choose a single function to call. Only the one from line 12 will work for all cases, so this one is chosen
Huh. I would've expected for it to be a compile-time error, because Self is not necessarily generic, and even if it was, it already has the generic argument filled in and you cannot tackle a second one on the end to make MyTest<String><Int>. Could be filed as a bug report to bugs.swift.org
Anyway, here's a workaround without force casting:
func foo() {
if let intSelf = self as? MyTest<Int> {
intSelf.bar()
} else {
bar()
}
}
The type of myTest1 is MyTest<Int> which means that it's known at compile time that Output is Int. Because of that both versions of bar can work here, and compiler chooses the more specific one (the one at line 16)
It's still statically dispatched, but this time compiler has more information about the types
Without knowing internals of the compiler this is in my opinion a very unintuitive behavior.
As a developer one could also have assumed that for generic classes several variants are created at compile time. If the variant with a special type was created at runtime, the type-constrained variant of a method could also be called without dynamic dispatch, since the variant and the methods to be called were already determined at build time.
What I find particularly dangerous is if you look at the following example:
class MyTest<Output> {
let myVar: Output
init(myVar: Output) {
self.myVar = myVar
}
func foo() {
bar()
}
private func bar() {
print("'bar' without generic constraint")
}
private func bar() where Output == Int {
print("'bar' constrained to 'Int' output")
}
}
The type constrained private bar function is basically a dead method and will never be called, especially not for MyTest<Int> instances. You can't call it outside the class itself and since the method is not called by any other generic method the bar method without the type constraint will always be chosen due to the reasons you mentioned.
Such an implementation can be read in such a way that the developer had the intention to provide a special implementation of a method for a well-known generic type parameter.
So from a developer's point of view: how do you implement a generic class in Swift and tell the compiler to use a special implementation for well-known types of a generic parameter?
I agree that this can be unintuitive and a bit cumbersome to work with. If what you want to achieve in your actual use case is to let the implementation of bar() depend on Output you can attach it to the output type:
class MyTest<Output: P> {
let myVar: Output
init(myVar: Output) {
self.myVar = myVar
}
func foo() {
Output.bar(for: self)
}
}
protocol P {
static func bar<Output: P>(for myTest: MyTest<Output>)
}
extension P {
static func bar<Output: P>(for myTest: MyTest<Output>) {
print("'bar' without generic constraint")
}
}
extension String: P {}
extension Int: P {
static func bar<Output: P>(for myTest: MyTest<Output>) {
print("'bar' constrained to 'Int' output")
}
}
func test() {
let myTest1 = MyTest(myVar: 5)
myTest1.foo() // 'bar' constrained to 'Int' output
let myTest2 = MyTest(myVar: "String")
myTest2.foo() // 'bar' without generic constraint
}
test()
@Jens This is an interesting way to do it. I didn't know this approach, so I think for a while and find a nit.
If one wants to do real work with MyTest.myVar in the function, he has to do forced type casting. Otherwise it can't compile, because while the Output generic type is atually Int, the knowledge is only in generic system, not in the protocol.
extension Int: P {
static func bar<Output: P>(for myTest: MyTest<Output>) {
if (myTest.myVar as! Int) == 10 { print("OK")} // forced type casting
}
}
If this is a common approach to use generic and protocol (I don't know if it is), it would be good if Swift can add support to get rid of the forced type casting (I don't know how).
Actually, if you use this variant you may cause other issues with classes conforming to P:
class MyOutput {
let val: Int
init(val: Int) { self.val = val }
}
extension MyOutput: P {}
This causes a compiler error:
Protocol 'P' requirement 'bar(for:)' cannot be satisfied by a non-final class ('MyOutput') because it uses 'Self' in a non-parameter, non-result type position
So I wouldn't say your first approach was a mistake
Thanks for the great solution! It turned out so simple.
BTW, I have the impression that Self doesn't work well with class inheritance (I mean its behavior is a bit confusing, though I forgot the details). But that's another topic.
I just found this from searching for something similar… is this still the legit workaround or has anything shipped recently in Swift that gives engineers new solutions for this?