Skip to content

List.toList() Type problem #55321

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Closed
feduke-nukem opened this issue Mar 28, 2024 · 5 comments
Closed

List.toList() Type problem #55321

feduke-nukem opened this issue Mar 28, 2024 · 5 comments
Labels
closed-as-intended Closed as the reported issue is expected behavior type-question A question about expected behavior or functionality

Comments

@feduke-nukem
Copy link

feduke-nukem commented Mar 28, 2024

This tracker is for issues related to:

  • Dart core libraries (dart:async, dart:io, etc.)

Some other pieces of the Dart ecosystem are maintained elsewhere.
Please file issues in their repository:

Let's assume I have such code:

void main() {
  final holder = Holder<B>(contents: [Content(data: B())]);

  test(holder);
}

sealed class A {}

class B extends A {}

class C extends A {}

class Content<S extends A> {
  final S data;

  Content({required this.data});
}

class Holder<S extends A> {
  final List<Content<S>> contents;

  Holder({required this.contents});
}

So if I use toList() I will get TypeError:

void test(Holder holder) {
  final contents = holder.contents.toList();

  contents.add(Content(data: B())); // TypeError
}

But List.of() doesn't produce that error:

void test(Holder holder) {
  final contents = List.of(holder.contents);

  contents.add(Content(data: B())); // Okay
}

I wonder why? toList() under the hood uses List.of() but the result is different. List.from and [...contents] cause no problem, only toList() does.

Also that will work:

void test(Holder holder) {
  final contents = holder.contents.map((e) => e)).toList();

  contents.add(Content(data: B())); // Okay
}
@lrhn lrhn added the type-question A question about expected behavior or functionality label Mar 28, 2024
@lrhn
Copy link
Member

lrhn commented Mar 28, 2024

Because your code is not type safe. And nor is List.

It's the same error you'll get from

List<Object?> list = <C>[];
list.add(B()); // Type error, B is not a C

The function test takes a Holder, aka Holder<Object?>.
That contains some List<Content<T>>, deleted you haven't shown the type. I'm assuming it's a List<Content<C>>, but from where your code is looking, it has static type List<Content<Object?>>

If you try to add a Content<B> to that, it fails, as it should.

If you do .toList() on it, you get another List<Content<C>> with static type List<Content<Object?>>, and adding still fails.

If you do var list = List.of(...);, you create a new List<Content<Object?>> with the same elements. That's safe, and you can safely add to it. That is, the static element type becomes the runtime element type of the newly created list.
That's why it works.

@feduke-nukem
Copy link
Author

feduke-nukem commented Mar 28, 2024

Because your code is not type safe. And nor is List.

It's the same error you'll get from

List<Object?> list = <C>[];
list.add(B()); // Type error, B is not a C

The function test takes a Holder, aka Holder<Object?>. That contains some List<Content<T>>, deleted you haven't shown the type. I'm assuming it's a List<Content<C>>, but from where your code is looking, it has static type List<Content<Object?>>

If you try to add a Content<B> to that, it fails, as it should.

If you do .toList() on it, you get another List<Content<C>> with static type List<Content<Object?>>, and adding still fails.

If you do var list = List.of(...);, you create a new List<Content<Object?>> with the same elements. That's safe, and you can safely add to it. That is, the static element type becomes the runtime element type of the newly created list. That's why it works.

Thanks for answer but I still don't get it. toList() is a shortcut for List.of() (basically the same operation) then why behaviour differs?

@lrhn
Copy link
Member

lrhn commented Mar 29, 2024

If list is an instance of List<E>, then list.toList() works like List<E>.of(list), no matter what the static type of list is.

Doing List.of(list) infers the type of the created list from the static type of list, because the compiler infers the missing type argument to List. This is List<T>.of(list) for some T, which is inferred because you didn't write it.

Which means:

List<Object> list = <int>[1]; // Valid, up-cast.
var list2 = list.toList(); // Same as `List<int>.of(list);`, called by list itself, which knows it's an `int` list.
var list3 = List.of(list); // Same as `List<Object>.of(list);`, inferred from static type.

Those two calls are not the same.

@feduke-nukem
Copy link
Author

feduke-nukem commented Mar 29, 2024

If list is an instance of List<E>, then list.toList() works like List<E>.of(list), no matter what the static type of list is.

Doing List.of(list) infers the type of the created list from the static type of list, because the compiler infers the missing type argument to List. This is List<T>.of(list) for some T, which is inferred because you didn't write it.

Which means:

List<Object> list = <int>[1]; // Valid, up-cast.
var list2 = list.toList(); // Same as `List<int>.of(list);`, called by list itself, which knows it's an `int` list.
var list3 = List.of(list); // Same as `List<Object>.of(list);`, inferred from static type.

Those two calls are not the same.

So is that an expected behaviour?

Why doesn't an analyzer warn about it or something?

@lrhn
Copy link
Member

lrhn commented Mar 29, 2024

It's expected and specified behavior.

The analyzer doesn't warn, because it's perfectly valid code.

The inherent unsoundness in covariant generics is so deep in the language that it's impossible to warn about it, because that would make almost all uses of (for example, but not limited to) List.add give a warning.

It might be possible to locally deduce that you are casting a List<int> to List<Object>, and shortly after doing .add(x) on it with something that's not an int. But it's also incredibly easy to do that without the analyzer having any chance of recognizing it.

@a-siva a-siva added the closed-as-intended Closed as the reported issue is expected behavior label Mar 29, 2024
@a-siva a-siva closed this as completed Mar 29, 2024
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
closed-as-intended Closed as the reported issue is expected behavior type-question A question about expected behavior or functionality
Projects
None yet
Development

No branches or pull requests

3 participants