4

What's the difference in Dart between casting with the as keyword and casting with the cast method?

See the following example:

import 'dart:convert';

class MyClass {
  final int i;
  final String s;

  MyClass({required this.i, required this.s});

  factory MyClass.fromJson(Map<String, dynamic> json) =>
      MyClass(s: json["s"], i: json["i"]);
}

void main() {
  const jsonString = '{"items": [{"i": 0, "s": "foo"}, {"i": 1, "s":"bar"}]}';
  final json = jsonDecode(jsonString);

  final List<MyClass> thisWorks = (json["items"] as List)
      .cast<Map<String, dynamic>>()
      .map(MyClass.fromJson)
      .toList();

  final List<MyClass> thisAlsoWorks = (json["items"] as List)
      .map((json) => MyClass.fromJson(json as Map<String, dynamic>))
      .toList();

  final List<MyClass> thisCrashes =
      (json['items'] as List<Map<String, dynamic>>)
          .map(MyClass.fromJson)
          .toList();
}

The last call (casting with as) results in an exception: type 'List<dynamic>' is not a subtype of type 'List<Map<String, dynamic>>' in type cast.

I expected that casting with as would work like casting with the cast method without resulting in an exception.

Giuseppe Cianci
  • 407
  • 3
  • 9
  • 1
    Does this answer your question? [Why an explicit ".cast<>()" function in Dart instead of "as <>"](https://stackoverflow.com/questions/49541914/why-an-explicit-cast-function-in-dart-instead-of-as) – Md. Yeasin Sheikh Feb 02 '22 at 16:31
  • That question goes in the same direction, but does not answer my question. In there it says that the Effective Dart guide states "DON’T use cast() when a nearby operation will do." https://dart.dev/guides/language/effective-dart/usage#dont-use-cast-when-a-nearby-operation-will-do `cast` is not as performant because it actually creates a new iterable. I've added another example (`thisAlsoWorks`) that shows that it's somehow possible to use `as` instead of `cast`. I just don't understand why the last example results in an error. – Giuseppe Cianci Feb 02 '22 at 16:44
  • 1
    That's because the JSON decoder has to return types that reflect the range of types it could encounter in the JSON. The reason that you get `Map` is because JSON types could be numbers, booleans, maps or lists, and that isn't known by the decoder until it starts decoding. Similarly, the decoder returns `List` because in JSON a list could be of any valid JSON type. Just because your JSON happens to contain a list of objects, doesn't mean that *all* JSON does! So,... – Richard Heap Feb 02 '22 at 18:38
  • 1
    ... `json['items']` is a `List` that just happens to contain only instances of `Map`. This means that `.cast()` works because it "Returns a view of this list as a list of R instances". And `json as Map` works because all the dynamics are indeed (in your case) instances of `Map`. But the original list remains a `List` because that's what it has to be to represent what an arbitrary JSON array contains. – Richard Heap Feb 02 '22 at 18:42

1 Answers1

4

as performs a cast that, after performing a runtime check, changes the static (known-at-compile-time) type of an object. It does not affect the identity of the object. Using as on a collection (e.g. List, Map, Set) casts that collection object itself, not its elements.

Collections provide a .cast method that returns a new object (a "view") for the collection that performs as casts for each element.

void main() {
  // `list1` is created as a `List<Object>` but happens to store only ints.
  List<Object> list1 = <Object>[1, 2, 3];

  try {
    // This cast will fail because `list1` is not actually a `List<int>`.
    list1 as List<int>;
  } on TypeError catch (e) {
    print(e); // This gets printed.
  }

  // `list2` is a `List`-like object that downcasts each element of `list1`
  // to an `int`. `list2` is of type `List<int>`.
  //
  // Note that `list2` is not a copy of `list1`; mutations to `list2` will
  // affect `list1`.
  List<int> list2 = list1.cast<int>();
  
  // `list3` is `list2` upcast to a `List<Object>`.  No explicit cast is
  // necessary because if a class `Derived` derives from a class `Base`, then
  // Dart also considers `List<Derived>` to be a derived class of `List<Base>`.
  List<Object> list3 = list2;
  
  // `list4` is `list3` downcast to `List<int>`.  Since `list3` refers to the
  // same object as `list2`, and since `list2`'s actual runtime type is
  // `List<int>`, this cast succeeds (unlike `list1 as List<int>`).
  List<int> list4 = list3 as List<int>;

  print(identical(list1, list2)); // Prints: false
  print(identical(list2, list3)); // Prints: true
  print(identical(list3, list4)); // Prints: true
}
jamesdlin
  • 81,374
  • 13
  • 159
  • 204