/cc @munificent
Yes, I definitely agree we could use guidance on this. I did add a guideline for .toList() versus new List.from(). I'm honestly not sure what the guidance should be for .retype() and .cast(). I don't have a lot of experience with them.
@leafpetersen @lrhn any thoughts?
One cannot use as to cast e.g., List<dynamic> to List<num> if the runtime type is not a subtype of it.
Here's an example (needs Dart 2):
void main() {
List<Object> foo = [1, 2.3];
print(foo.runtimeType); // List<Object>
var bar = foo as List<num>; // type 'List<Object>' is not a subtype of type 'List<num>' in type cast
}
Same example with .retype():
void main() {
List<Object> foo = [1, 2.3];
print(foo.runtimeType); // List<Object>
print(foo is List<num>); // false
var bar = foo.retype<num>();
print(bar.runtimeType); // CastList<Object, num>
print(bar is List<num>); // true
}
The only difference between .retype() and .cast() seems to be that the latter returns the same object if it passes reified is check.
void main() {
var foo = [1, 2.3]; // rely on inference
print(foo.runtimeType); // List<num>
var bar = foo.retype<num>();
print(bar.runtimeType); // CastList<num, num>
print(bar == foo); // false
var baz = foo.cast<num>();
print(baz.runtimeType); // List<num>
print(baz == foo); // true
}
It would also be helpful to add a section on down-casting async results (e.g. see https://github.com/dart-lang/sdk/issues/32672).
I would suggest that you hardly ever use cast or retype.
retype<T> wraps the list, forcing an as T check for every access, needed or not.cast<T> optionally wraps the list, avoiding the as T checks when not needed, but this comes at a cost of making the returned object polymorphic (the original type or the CastList), which interferes with the quality of the code that can be generated.If you are going to touch every element of the list, you might as well copy it with
new List<T>.from(original)
So I would only use cast or retype if my access patterns are sparse or if I needed to update the original.
@munificent regarding a.toList() vs (new) List<T>.from(a), I think nowadays I would mildly prefer (new) List.of(a) since you are guaranteed the result is a the system implementation, and not CraxyList implementation returned by toList(). With optional new, it is also nice and short:
a.toList()
List.of(a)
List.of(e) will, I think, usually be more inference friendly than e.toList() as well when e is an expression. That is, if upwards inference can produce a type List<T> for e, then both e.toList() and List.of(e) will have the right static and dynamic type. But if upwards inference cannot do so, but downwards inference can, then List.of(e) will work, but e.toList() will not.
Iterable<T> foo<T>() => [];
void bar(List<int> a) {}
void main() {
bar(List.of(foo())); // Inferred as List.of<int>
bar(foo().toList()); // Inferred as foo<dynamic>
}
As a part of documentation update, could API docs be refined for List.of and List.from?
List.of
Creates a list from
elements.
I think that List.of description should mention that this ctor will create a List with the same generic type as elements.
List.from
Creates a list containing all
elements.
Use of "all" here may imply to some readers (like myself, initially) that there's a version of this ctor that skips some elements.
I think that List.of description should mention that this ctor will create a List with the same generic type as elements.
The thing is, it won't, at least not always.
It will create a new growable built-in List<T> where T is the type you provide in the constructor invocation. If you don't write a type, then the static type of the argument is used to infer the type argument passed to the constructor.
We don't generally say in class or method doc, what happens when you omit a type argument, because that's always handled by inference and treated like if you do pass a type argument. It's non-trivial to explain what inference does.
However, it makes sense to provide examples of common uses, and a case with no explicit type argument would be a good example to have.
As a part of documentation update, could API docs be refined for
List.ofandList.from?
Yes, but that should be a separate issue. Would you mind filing one here: https://github.com/dart-lang/sdk/issues
Thanks!
Is this issue still valid after dart-lang/sdk#33075 ?
Just saw there is still "as" vs "cast"
There's still a little bit of work to do, yes. But it's simpler because we don't have to distinguish between cast() and retype() at least.
...and it's done (for now, at least).
speaking of which, is there safe cast option available?
similar to kotlin and swift
Not currently, no, though I agree that could be a useful feature.
i wrote a little piece of helper method, not handy as 'as?' but hopefully may be helpfully to some
/// ignoreNull = true will not throw error when input obj is null
/// assertResult = true will throw error when obj is not null and is not of given type T
static T safeCast<T>(dynamic obj, {bool ignoreNull = true, bool assertResult = false}) {
if (obj == null) {
if (!ignoreNull) {
final error = "safeCast passed in obj is null";
print(error);
assert(false, error);
}
return null;
}
final T result = (obj is T) ? obj : null;
if (result == null && obj != null) { // obj will never be null, but this looks more clear
final error = "safeCast obj:$obj is not of type:${T.runtimeType}";
print(error);
if (assertResult) assert(false, error);
}
return result;
}
@hereisderek I think that usage of your safeCast() method:
final someClass = safeCast<SomeClass>(someDynamic) ?? SomeClass.empty();
is less clearly as simple:
final someClass = someDynamic is SomeClass ? someDynamic ?? SomeClass.empty();
Most helpful comment
One cannot use
asto cast e.g.,List<dynamic>toList<num>if the runtime type is not a subtype of it.Here's an example (needs Dart 2):
Same example with
.retype():The only difference between
.retype()and.cast()seems to be that the latter returns the same object if it passes reifiedischeck.It would also be helpful to add a section on down-casting async results (e.g. see https://github.com/dart-lang/sdk/issues/32672).