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.)