Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Unexpected error when down-casting a List #2087

Closed
esDotDev opened this issue Feb 1, 2022 · 8 comments
Closed

Unexpected error when down-casting a List #2087

esDotDev opened this issue Feb 1, 2022 · 8 comments
Labels
request Requests to resolve a particular developer problem

Comments

@esDotDev
Copy link

esDotDev commented Feb 1, 2022

This code will throw the error:
Uncaught Error: TypeError: Instance of 'Bar': type 'Bar' is not a subtype of type 'Foo'

void main() {
  List<Foo> fl= [Foo()];
  List<Mixin> l = fl.cast<Mixin>();
  l.add(Bar()); // this is the line that throws
}

class Foo with Mixin {}

class Bar with Mixin {}

mixin Mixin {}

If you rewrite it as this, it works as expected:

  List<Mixin> l = [Foo()];
  ...

If you replace the cast (or toList()) with a map().toList(), it will also work as expected:

List<Foo> fl= [Foo()];
List<Mixin> l = fl.toList(); // causes runtime error when bar is added later
List<Mixin> l = fl.map((i) => i as Mixin).toList() // works as expected
l.add(Bar());

This is not specific to mixins, class + extends has identical behavior.

@esDotDev esDotDev added the request Requests to resolve a particular developer problem label Feb 1, 2022
@Levi-Lesches
Copy link

Levi-Lesches commented Feb 1, 2022

From the documentation for List<E>.cast<R>:

Elements added to the list (e.g., by using add or addAll) must be instances of R to be valid arguments to the adding function, and they must also be instances of E as well to be accepted by this list as well.

Seems like the docs covered this already, noting that even if you cast your list from Foo to Mixin, all new elements still need to be of type Foo.

Some discussion on this repo seems to indicate that the reason this happens is that .cast is actually a runtime check, and the list is still fundamentally a List<Foo>, just with a wrapper that makes sure all instances are also Mixins as well. So if you want to change the static type of the list from Foo to Mixin, you should use List.of instead (note that I used simpler names here):

class A { }
class B extends A { }
class C extends A { }

void main() {
  List<B> b = [B()];
  List<A> a1 = b.cast<A>();
  List<A> a2 = List.of(b);
  a1.add(C());  // runtime error
  a2.add(C());  // okay
}

@esDotDev
Copy link
Author

esDotDev commented Feb 1, 2022

Hmm, ok. That's really odd, I would expect the error to happen when the cast happens then, not sometime later when something is added. Like why even allow this?

What about in the case of a toList()? I guess that is just doing an implicit cast as well? Very sneaky :(

List<Foo> fl= [Foo()];
List<Mixin> l = fl.toList();
l.add(Bar()); // error here, 

It's just very confusing, cause even if toList is doing a cast, isn't List.of also doing the same thing? And why does my map().toList() work, when all it is doing is a cast as well?

ty for the workaround!

@eernstg
Copy link
Member

eernstg commented Feb 1, 2022

@esDotDev, if you use the cast instance method to obtain a List<Mixin> from a given List<Foo> (or a List<T2> from List<T1>, for any T1 and T2) then you will get a wrapper object where the underlying List<Foo> (List<T1>) will be used to hold the elements, and the new element type Foo (respectively T2) will be checked when the access occurs. In particular, the operation will throw if an element is added which is not a Foo/T2. This can be quite useful if the original list has a large number of elements, and only a few of them will be accessed using the wrapper.

But if you want to use the given list as a List<Mixin> (List<T2>), with no special constraints, then you should use the constructor List.of:

void main() {
  List<Foo> fl= [Foo()];
  List<Mixin> l = List.of(fl);
  l.add(Bar()); // Does not throw.
}

You could also use List.from, if the existing list has an element type which is not related to the element type of the list you're creating (but, somehow, it's known that all the elements have the new element type). List.from is less safe and slower, so you wouldn't use it unless you have to.

@esDotDev
Copy link
Author

esDotDev commented Feb 1, 2022

Thanks for the clarification.

This makes sense to me when speaking of a cast on the list itself, but I'm still confused why toList doesn't work, as under the hood it appears to just be doing List.of() itself.

Doesn't work:
 List<Mixin> l = fl.cast<Mixin>();
 List<Mixin> l = fl.toList();

Works:
  List<Mixin> l = List.of(fl);
  List<Mixin> l = fl.map((i) => i as Mixin).toList();
  List<Mixin> l = List.from(fl);

We don't use cast in practice (I didn't know it existed until today), we normally use toList() in situations like this, and let the compiler infer type.

@lrhn
Copy link
Member

lrhn commented Feb 1, 2022

The toList method returns a list of the same type as the original list. Since the original list is a List<Foo>, the .toList() is also a List<Foo>, which can be assigned (unsafely) to List<Mixin>.

What you might want is var l = fl.cast<Mixin>().toList(); which creates a new List<Mixin> with the same (Foo) elements as fl. Or just var l = <Mixin>[...fl];

@esDotDev
Copy link
Author

esDotDev commented Feb 1, 2022

Ah, ok, so it basically comes down to the fact the compiler is allowing this:

List<Foo> fl= [Foo()];
List<Mixin> l = fl; // This is not really a List<Mixin>, but compiler says nothing.

Is there a linter rule to have the compiler call this out for me?

In terms of usage, a manual List.of() seems to work fine. It's just that I expected toList would be down-casting to the target type as well.

I mean, looing at the function definition it should?

List<E> toList({bool growable = true}) {
    return List<E>.of(this, growable: growable);
  }

Why is E not Mixin here? It looks like the return type should be establishing the generic?

List<Mixin> l = fl.toList();

@Levi-Lesches
Copy link

Is there a linter rule to have the compiler call this out for me?

My understanding is that this will be a language feature when static variance is introduced. Specifically, this looks like a case of use-site variance, #753.

Why is E not Mixin here? It looks like the return type should be establishing the generic.

Notice the usage of E here vs E in List.of. In the latter case, we're using a constructor, so the E comes from whatever generic you pass to it, or whatever is inferred:

List<Mixin> l = List.of<Mixin>(fl);  // okay, E is Mixin

However, when you use fl.toList(), you already have a list, and that list already has an E -- Foo.

List<Mixin> l = fl.toList<Mixin>();  // toList is declared with 0 type parameters

So fl.toList() is returning List<Foo>.of(fl), and doesn't care about Mixin at all. Note that your workaround with .map does work because you're essentially doing the following:

List<Mixin> l = fl.map<Mixin>((i) => i as Mixin).toList();

Now, .toList() is being called on an Iterable<Mixin>, which happily returns a List<Mixin>.

@esDotDev
Copy link
Author

esDotDev commented Feb 1, 2022

Ok, ty all for the time!

@esDotDev esDotDev closed this as completed Feb 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request Requests to resolve a particular developer problem
Projects
None yet
Development

No branches or pull requests

4 participants