dart-lang / language

Design of the Dart language

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.

image
image

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>;

image

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.