Unexpecte behavior in Exhaustiveness Checking With sealed classes and generics
MiniSuperDev opened this issue · comments
Hello, check this case.
I'm using dart 3.4.1
.
sealed class A<T> {}
abstract class B<T, R> implements A<T> {
void bMethod(){
}
}
class C<T> implements A<T> {
void cMethod(){
}
void fun1(List<A<T>> list) {
for (final item in list) {
switch (item) {
case B<dynamic, dynamic>():
// Here Item should be a B type and should have a 'bMethod' method.
// But It's used like will be of type A
item.bMethod(); // ERROR: The method 'bMethod' isn't defined for the type 'A'.
case C<T>():
item.cMethod();
}
}
}
void fun2(List<A<T>> list) {
for (final item in list) {
// ERROR: The type 'A<T>' is not exhaustively matched by the switch
// cases since it doesn't match 'B<dynamic, dynamic>()'.
switch (item) {
case B<T, dynamic>():
item.bMethod();
case C<T>():
item.cMethod();
}
}
}
}
But I found an unexpected workaround
case B<dynamic, dynamic>():
++ item as B<T, dynamic>;
As you can see when you add item as B<T, dynamic>;
in the case B.
The type change.
From A<T>
to B<T, dynamic>
like you can see in the image , but it should be B<dynamic, dynamic>
ever and should be exhaustively matched by the switch.
sealed class A<T> {}
abstract class B<T, R> implements A<T> {
void bMethod() {}
}
class C<T> implements A<T> {
void cMethod() {}
void fun1(List<A<T>> list) {
for (final item in list) {
switch (item) {
case B<dynamic, dynamic>():
item.bMethod(); // The method 'bMethod' isn't defined for the type 'A'.
case C<T>():
item.cMethod();
}
}
}
}
vs
sealed class A<T> {}
abstract class B<T, R> implements A<T> {
void bMethod() {}
}
class C<T> implements A<T> {
void cMethod() {}
void fun1(List<A<T>> list) {
for (final item in list) {
switch (item) {
case B<dynamic, dynamic>():
item as B<T, dynamic>;
item.bMethod(); // Now works
case C<T>():
item.cMethod();
}
}
}
}
And regarding this, is it safe to make that cast?
I currently use it, but I don't know if it is safe.
case B<dynamic, dynamic>():
++ item as B<T, dynamic>;
Thanks
I would suggest you bind the variable to a type pattern instead, such as:
sealed class A<T> {}
abstract class B<T, R> implements A<T> {
void bMethod() {}
}
class C<T> implements A<T> {
void cMethod() {}
void fun1(List<A<T>> list) {
for (final item in list) {
switch (item) {
case B<dynamic, dynamic> item:
item.bMethod(); // Now works
case C<T> item:
item.cMethod();
}
}
}
}
However, this is intriguing as to why it is dealing with it differently.
The reason case B<dynamic, dynamic>()
does not promote is that B<dynamic, dynamic>
is not a subtype of A<T>
. It's a subtype of A<dynamic>
, but unrelated to A<T>
.
The reason B<T, dynamic>
is not exhausting B
is #3633/dart-lang/sdk#53486
@lrhn Hi, why is this line change the type? is safe to use?
case B<dynamic, dynamic>():
++ item as B<T, dynamic>;
sealed class A<T> {}
abstract class B<T, R> implements A<T> {
T bMethod();
}
class C<T> implements A<T> {
void cMethod() {}
void fun1(List<A<T>> list) {
for (final item in list) {
switch (item) {
case B<dynamic, dynamic>():
final copy1 = item;
item as B<T, dynamic>; // Change the type of the following items
final copy2 = item;
item as B<dynamic, dynamic>; // Do nothing after first cast
final copy3 = item;
item.bMethod(); // Now works
case C<T>():
final copy1 = item;
item as C<int>; // Do nothing
final copy2 = item;
item as C<dynamic>; // Do nothing
final copy3 = item;
item.cMethod();
}
}
}
}
The statement item as B<T, dynamic>;
is basically equivalent to
if (item is! B<T, dynamic>) throw TypeError(...);
This promotes item
to B<T, dynamic>
on the non-throwing branch, if it can be promoted to that type (meaning that B<T, dynamic>
is a subtype of the current type of item
).
It works, you can trust it.
It won't work if you couldn't promote to that type with an is
check either.