Why doesn't this compile, it feels like it should?...
# announcements
k
Why doesn't this compile, it feels like it should?
Copy code
abstract class A {
    open val x: String? = null
}

sealed class B : A() {
    override val x = "x"
}

class C : B() {
    override val x = null
}
If I add a type annotation on
B.x
of
String?
, then it works...
r
The deduced type of
B.x
is
String
, not
String?
The deduced type of
C.x
is
Nothing?
, which is not a subtype of
String
k
makes sense, when you put it that why - but, why is it deducing the type of
B.x
? Why isn't it's type the same as
A.x
?
r
Type deduction doesn't care that it's overriding something
k
huh, I figured there would be no type guessing, just the types from
A
... thanks, it's a bit surprising...
r
np
s
I'd think I have to concur with Karl. The type-inference engine could infer without ambiguity that C.x has the type of "String?", couldn't it?
r
Type inference infers the type of the initializer
The type of the initializer is
String
s
But if you would specify the type of C.x as "Integer", for example, the compiler would produce an error. This means the compiler does look at the type of its base-type.
True, if type-inference only works through how a variable/property is initialized, then yes, you'd need to explicitly provide the type for C.x. I was not aware that inference only works through initialization.
r
How the type was determined (whether you wrote it or it was inferred) and whether it's a subtype of the overriden declaration are orthogonal
s
Ah.. I see. A.x is a "String?" B.x is then overridden becoming a "String" due to its assignment right there. Then C.x is a String a well (can't suddenly. become a "String?" again or a "Nothing?")
👍 1
y
Basically, B overrides the type of property X to be
String
instead of
String?
because x is a
val
which means it is a
getter
only and so in that case B is allowed to `"refine" down the type of x to a subclass of
String?
(and it refined it to
String
) and so when C tries to override B it only sees the
String
type. Think about it this way: B is stating that it can A's constraint of returning a value that fits
String?
whenever x's getter is called, but in addition B is also now adding a constraint onto itself that is saying "and I promise that whenever x is called the value returned will be of type String` and it is allowed to do that because again it fits A's constraints. So now any class that uses a B object expects x to return a value of type
String
. Then, C comes around and is trying to override x to always return null, which is of type
Nothing?
. The issue with that is that C extends B, and so it is trying to return
Nothing?
for a field that promises it returns
String
. Now, if C was allowed to return null here, then someone who is using a B object that in reality is actually a C object would run into unexpected NPEs because, to the user's knowledge, B.x never returns null, or anything that isn't of type
String
Another exaggerated example would be having A.x actually be of type
Any?
, and then B.x being equal to
"x"
or something, which makes it of type String, and then having C.x set to
42
. Now that wouldn't compile for exactly the same reason because even though A.x is defined as Any?, C is forced to respect the constraints of B because it extends B.
👍 2
k
Thanks Youssef for the long and details explanation, it helped to clarify things further! I've been working with an assumption (don't know from where I've got it, maybe just how I imagined things should work) that if
B
doesn't specify a type explicitly of overriden field (
x
in this case), then it's type is exactly the type specified in the parent class. Once one writes out all the types explicitly (as Kotlin infers them), it's obvious one can't assign
null
to
C.x
...