I am learning generics and I have written such a class, but I am not quite sure how it works.
This is the code:
sealed class Result<out T> {
    abstract val data: T?
    abstract val error: String?
    class Success<out T>(override val data: T) : Result<T>() {
        override val error: String? = null
    }
    class Error<out T>(override val error: String, override val data: T? = null) : Result<T>()
}
fun main() {
    val resultNullable = getResultNullable()
    println(resultNullable.data)
}
fun getResultNullable(): Result<String?> {
    return Result.Success(null)
}
This code works fine, but I wonder why this line doesn't throw any error:
return Result.Success(null)
The class Success is declared to accept non-nullable value of type T, but it indeed accepts null.
Shouldn't it be declared as below to accept null values?
class Success<out T>(override val data: T?) : Result<T>() {
        override val error: String? = null
    }
I was searching for an explanation on the web, but I am struggling with understanding, so I decided to bring my own example here.
Thank you in advance for any support.
T is a generic type parameter, it doesn't say anything about the nullability of the type it can represent, because it's not constrained in any way. Users of the Result class can use either a nullable type OR a non-nullable type for T.
The return type of getResultNullable() is Result<String?> (note the question mark), so for this particular result, T = String?. This is why it's completely fine to create an instance with Result.Success(null).
You can see type parameters a bit like function parameters, but for types. While a function parameter s: String can be set to any value of type String, a type parameter T can be "set" to any type, nullable or not.
That being said, just like you can constrain the type of a function parameter, you can also add constraints on a type parameter. For instance, using T : Any ensure only non-nullable types are passed as T, because the type T has to be a subtype of Any (which is not nullable). The absence of constraints is equivalent to T : Any?.
So far we've only talked about the declaration of T in the class, such as class Success<out T>, but not the usages of it throughout the generic class or function.
When using T as the return type of a function, or as a function parameter type, we can add further variations of it:
no matter what the actual T is, we can refer to the nullable version of it by using T?. If T is String, then T? is String?. If T is already String?, then T? is also String?.
the opposite concept is the non-nullable version of T, and its denoted as T & Any. If T is String, then T & Any is String as well (already non-nullable). If T is String?, then T & Any is the non-nullable String.
These things can be useful in cases where we deal with null explicitly and generically. For instance:
// no matter what T is, this could return null
fun <T> makeNullMaybe(value: T): T? {
    if (Random.nextBoolean()) {
        return null
    } else {
        return value
    }
}
// no matter what T is, this will return a non-null value
fun <T> nonNullOrThrow(value: T): T & Any {
    if (value == null) {
        error("it's null!")
    }
    return value // smart-cast to the non-nullable version of T
}
                        If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With