Home > Software engineering >  Why the test with "is" expression is more stable than the test with runtimeType?
Why the test with "is" expression is more stable than the test with runtimeType?

Time:10-02

On dart tour page (https://dart.dev/guides/language/language-tour#getting-an-objects-type) these have a statement that testing variable type with "is" expression is more stable. Why is it so?

CodePudding user response:

An is test is a subtype test.

If you do e is Iterable, you check whether the value of e implements Iterable<dynamic>, which includes any of List, Set and Queue, as well as all the subtypes of Iterable used internally by the runtime system, like _TakeIterable (which is used to implement Iterable.take). It matches both values of type Iterable<Object> and Iterable<int>. All of these are objects which can safely be used as an Iterable, and are intended to be used as such.

If you do e.runtimeType == Iterable, you are checking whether the Type object returned by e.runtimeType is equal to precisely the type Iterable<dynamic>. That will be false for any list, set or queue, for any actual iterable class which only implements Iterable, and even for something which returns the Type object of Iterable<int> or Iterable<Object?> from runtimeType.

I say that you check the object returned by e.runtimeType, not the run-time type of the value, because anyone can override the runtimeType getter.

I can make a class like:

class WFY {
  Type get runtimeType => Iterable<int>;
}
void main() {
  print(WFY().runtimeType == Iterable<int>);  // True!
}

The value returned by runtimeType doesn't have to have any relation to the actual runtime type of the object. Obviously it usually has, because there is no benefit in overriding runtimeType, because you shouldn't be using it for anything anyway,

Even if your code works today, say:

assert(C().runtimeType == C); // Trivial, right!

it might fail tomorrow if I decide to make C() a factory constructor which returns a subtype, _C implementing C. That's a change that is usually considered non-breaking, because the _C class can do everything the C interface requires, other than having C as actual runtime type.

So, doing Type object checks is not stable.

Another reason using is is better than comparing Type objects for equality is that it allows promotion.

num x = 1;
if (x is int) {
  print(x.toRadixString(16));  // toRadixString is on int, not on num
}

The is check is understod by the language, and trusted to actually guarantee that the value's runtime type implements the type you check against.

Comparing Type objects can mean anything, so the compiler can't use it for anything.

Some people like to use runtimeType in their implementation of ==, like;

class MyClass {
  // ...
  bool operator ==(Object other) =>
    MyClass == other.runtimeType && other is MyClass && this.x == other.x;
}

This is intended to avoid subclass instance being equal to super-class instances when you ask the superclass, but not if you ask the subclass (the "ColorPoint problem", where ColorPoint extends Point with a color, and is equal to a another ColorPoint with the same coordinates and color, but if you ask a plain Point whether it's equal to a ColorPoint, it only checks the coordinates.)

This use of runtimeType "works", but is not without issues. It means you cannot use mocks for testing. It means you cannot create a subclass which doesn't extend the state, only the behavior, and which would want to be equal to the superclass instances with the same state. And it means you do extra work, because you still need to cast the other object from Object to the surrounding type in order to access members, and Type object checks do not promote.

If possible, it's better to never allow subclasss of a concrete class that has a == method, and if you need to share other behavior, inherit that from a shared superclass. (In other words: Don't extend classes that aren't intended to be extended, don't put == on classes which are intended to be extended.)

  •  Tags:  
  • dart
  • Related