I have this code snippet:
class A<T> {
const A(this.value);
final T value;
}
class B<T> extends A<T> {
const B(super.value);
T get getter => value;
}
void main() {
final A<int> a = B<int>(2);
if (a is B) {
a.getter; // <- Lint: The getter 'getter' isn't defined for the type 'A<int>'.
// Work if I do:
// (a as B).getter;
}
if (a is B<int>) {
a.getter;
}
}
When I check a is B
, a
is still of type A<int>
inside the if
branch (and not B
).
But when I check a is B<int>
, a
is actually cast as B<int>
inside the if
branch.
I was wondering what was the reason for that.
Is it related to Dart extended classes with generic types won't use the direct parent type ?
CodePudding user response:
This is working as intended/specified. (Not necessarily as desired, but that's for the future to fix.)
Your a
variable has type A<int>
.
When you do if (a is B)
, you omitted to provide a type parameter to the generic B
type. Because of that, the type parameter is inferred, and the code is really:
if (a is B<dynamic>) ..
Now, B<dynamic>
is not a subtype of A<int>
. The type B<int>
would be, and B<dynamic>
is a subtype of A<dynamic>
, but there is absolutely no subtype relationship between A<int>
and B<dynamic>
.
Dart only performs type promotion on a variable when it's actually a promotion, which means only when it's to a subtype of the existing type of the variable. Otherwise, the variable could lose abilities.
Say you have a variable of type Foo
. You check that it's also a Bar
, which is not a subtype of Foo
(because its value is really a Baz
which implements both Foo
and Bar
). If Dart changed the type of the variable to Bar
, you could no longer call Foo
-only methods on it.
And Dart does not have intersection types in its type system, so it cannot say that the variable is both Foo
and Bar
(typically written as Foo & Bar
). So, it has to choose either For
or Bar
, and it chooses to preserve the existing type and to not promote.
So, that's why it works to do if (a is B<int>)
, because then you are checking for a subtype, and therefore you get promotion to that subtype (while still also being the supertype, because that's what being a subtype means).
It's not directly related to the linked issue which is more about not being able to find A<Object>
as the least upper bound of B extends A<int>
and `C extends A.
It's also not completely unrelated, conceptually. Your type hierarchy does contain a diamond structure:
A<dynamic>
/ \
A<int> B<dynamic>
\ /
B<int>
Since B<int>
has two unrelated supertypes, you are able to have a variable whose static type is one of those, A<int>
, and you check whether the value is another of them, B<dynamic>
, and the type system cannot help you with finding a type satisfying both. It would have to figure out that the value is definitely a B<int>
, and it doesn't. It doesn't find a common subtype.
(In the linked example, it failed to find a common supertype instead.)
The reason (a as B).getter
works is that (a as B<dynamic>).getter
works.
The a
value is-a B<int>
which is-a B<dynamic>
, so the cast succeeds.
It's not casting to a subtype, but casts don't need to. It just has to be a supertype of the actual value's type, the current static type of the variable isn't important
Maybe Dart could be smarter. Being smarter always comes with the risk of being just too clever, and doing something the user does not expect. Or being almost clever enough, and thinking it has a solution that's better, when in fact there are multiple possible solutions, and it picks the wrong one. The current Dart promotion rules are simple:
- You can only promote local variables.
- You can only promote to a subtype of the current type.
- It never promotes to a type that you haven't checked for.
Simple and predictable. Just a little too stupid sometimes, but usually you can fix the program by providing the missing information, like doing a is B<int>
.