cfleming
05/28/2024, 12:15 AMabstract class Base<T : Base<T>>() {
fun doSomething(): T {
... do something ...
return this
}
}
class Builder : Base<Builder>() {
fun build(): Something {
... return an immutable object ...
}
}
val something = Builder().doSomething().build()
So I’d like to have a base object, where the derived classes will be builders. However, the return this
is marked with an error, since (I guess) the compiler can’t prove that all derived classes are instances of T
. Is there some way I can achieve this, or should I just return this as T
and suppress the unchecked cast?Youssef Shoaib [MOD]
05/28/2024, 12:31 AMabstract class Base<T : Base<T>>() {
internal fun doSomethingInternal() { ... }
}
fun <T: Base<T>> T.doSomething(): T = apply { doSomethingInternal() }
cfleming
05/28/2024, 12:32 AMSzymon Jeziorski
05/28/2024, 9:26 AMdoSomething()
is a method bound to Base
class which itself doesn't know about implementations. Using casting from Base<T>
to T
is not the way to go here and can lead to unexpected behavior for edge cases.
Take a look at following example:
abstract class Base<T : Base<T>> {
fun doSomething(): T {
return this as T
}
}
class Builder : Base<Builder>()
class EdgeCase : Base<Nothing>() // Nothing is a subtype of everything, therefore Nothing : EdgeCase
val builderSomething = Builder().doSomething() // returned type is Builder, as expected
val edgeCaseSomething = EdgeCase().doSomething() // returned type is Nothing, not EdgeCase
There are many possible solutions to your problem. If you still need doSomething
to be a member method of Base<T>
, then making it not return anything might be a wise choice - since this is a final method declared in the abstract class, you could never make its declared return type to designate actual implementation.
However, it would be perfectly fine to call it within some other type-aware scope as for example: `Builder().apply { doSomething() } // returned type is `Builder``
In case you would like doSomething
to be a part of public API and preserve its caller type as a return type at the same time, you can make it a generic extension function, assuming it doesn't use any private/protected members directly (wildcard in Base<*>
handles Base<Nothing>
case, depending on your use case, this may or may not be something you worry about)
fun <T : Base<*>> T.doSomething(): T = apply {
// use API from `Base` class
}
Builder().doSomething() // returned type is Builder
EdgeCase().doSomething() // returned type is EdgeCase
cfleming
05/28/2024, 10:07 PM