I propose that we:
List constructor to List([int? count, T? value = null]), but statically treat it as List(int count, T value) in NNBD code, and as List([int count]) in non-NNBD code (like now).List<T>() to <T>[].List<T>(n) to List<T?>(n, null). (If the code is List<T>(x.length)..setAll(0, x) or similar, perhaps recommend to the user to rewrite using a literal with a spread).The NNBD behavior of the unnamed List constructor is that:
List<T>() creates an empty growable list, and is redundant with the [] literal.List<T>(n) is invalid if T is potentially non-nullable (even if n is zero, because the compiler doesn't check the value, just whether there is syntactically an argument).List<T>(null) is still possible and a run-time error. It differs from List<T>().See also #509.
The second item means that you can never create a List<T>(n) where T is a type variable.
This has lead to some amount of confusion during migration of existing code. Feedback is that it is confusing, and that, e.g., they expect List<T>(0) to work.
I recommend that we reconsider this design (which was never great) and either:
List.filled/List.empty.List.filled. Then List<int>(5, 0) works. It's redundant with List.filled, but shorter and if we make the second argument required, it will prevent the errors from List.filled when the fill value is null. OrWe'll have to include this in the migration tool, but I think it might be worth it. The List() is useless, the List(n) is dangerous and doesn't cover the uses that users actually care about.
The current behavior is inconsistent and confusing. There has always been special-casing around the List constructor, and we are already planning to special behavior for it in NNBD code. I think this change is a cleaner approach than just sticking to the current behavior.
Will this declaration be flagged as an error? var x = List<int>(10, null);
If yes, the signature List([int count, T? value = null]) would be confusing.
Potential alternative: just restrict the type parameter of List constructor to be nullable type. Currently, the language has no way to formally specify this restriction (we cannot write anything like List<T>() where T==T? or List<T?>()), but the compiler can special-case one constructor, and in any case, your proposal involves some magic.
This would make migration easier IMO.
With this proposal, in NNBD code, var x = List<int>(10, null); would be invalid. The type of the List<int> constructor in NNBD code would be treated as if it is List(int count, int value), and null is not assignable to int value.
The signature List([int count, T? value = null]) is indeed confusing, that's why no code sees it. We special case the static type in both NNBD and non-NNBD code, and the signature above is just the actual implementation which accepts both behaviors.
Only allowing nullable type parameters is, as you say, not supported by the language, and it has to work with type variables too. I'm also not just worrying about migration, but also about the API that we end up with. The List(count, fillValue) to create a fixed-length list is actually a good API.
A API where we accept a nullable fill value and then throw at run-time if it's not there, is worse.
I think removing the constructor entirely is likely to be annoyingly painful for relatively little benefit. How about we just mark it deprecated and have the error message point people to list literals, List.empty(), and List.filled()?
(For what it's worth, "Effective Dart" has long pushed users towards list literals and a significant fraction of calls to List() that I've seen in code were, ahem, written by you personally @lrhn. :) I don't think it's that common in the wild outside of people that are new to Dart and bringing existing C#/Java idioms with them.)
3.
List<T>(null)is still possible and a run-time error. It differs fromList<T>().
Remind me why we made this choice? Why are we not migrating this constructor to List([int length = 0])?
- just statically disallow its use with any type parameter which is potentially non-nullable. There's no good way to do so.
This is what is currently planned right? What do you mean by "There's no good way to do so"?
- Migrate all existing uses of
List<T>(n)toList<T?>(n, null).
This is probably a bad idea. It sounds like a recipe for runtime casts failures and/or static errors to me - most of the time you probably want to end up with a List<T>.
- _but_ statically treat it as
List(int count, T value)in NNBD code
This feel odd to me. The existing functionality is unique, and useful (albeit only in relatively rare situations). This change make the functionality redundant with List.filled.
I think a secondary issue is what developers have used the unnamed List constructor for. In the past, I've used the unnamed constructor to preallocate the size of a growable array: List<String>(128)..length = 0;
This is because Dart's List lacks what other language's lists/vectors have: capacity. Capacity is an implementation detail. Capacity exists because there are times where we know better than the VM, we know our final list size, and we don't want to pay the cost of growing the list in the application's common case, just in the extreme case.
Post-NNBD, Dart has no way of maintaining this behavior without making the entire list nullable.
The lack of expressive List types have made juggling the behaviors of various types of lists hard. We have lists that act like lists, we have lists that act like arrays, we have lists that are immutable, etc. It may be too late for a Iterable -> ReadOnlyArray -> Array -> List split, but having to remove parts of dart:core or add static rules to warn against using parts of it shows that a split in list types would be valuable, and establishes developer intent.
When a newcomer looks at List.filled, the natural question is: it's filled, as opposed to what? Probably, there's another constructor for an un-filled list (whatever that means)? But what exactly could that mean? Aha, there's an unnamed constructor List([int count]), which apparently creates an un-filled list. Granted, it's still filled, but it's filled with nulls, so probably that counts as un-filled. At least, we figured out what un-filled means.
With the proposed change, List(count, value) becomes synonymous with List.filled(count, value). The above reasoning is no longer applicable. It's "filled", as opposed to ... yet another "filled", which is as filled as its brother, but is spelled differently? It looks like a distinction without a difference. Maybe we can eliminate "filled" one now as its name carries no meaning at all?
As an aside, "filled" constructor also has an optional parameter "growable", which is somehow entangled with the property of being "filled", but not with the property of being "un-filled", even though the "unfilled" one is also filled - albeit with nulls, but the fact that it's filled with nulls has absolutely nothing to do with the property of being growable or not.
One could have thought that the value passed in "filled" would somehow be used as default while "growing" the list - after all, "growable" is what distinguishes this constructor - but no, turns out, when you "grow" it by setting a new length, the added elements are initialized with nulls and not with this "default". (I know, post-NNBD, length setter is gone, I just mentioned it for historical perspective) .
To keep it short, if we go with List(count, value), API will become less confusing if we incorporate "growable" in it and get rid of "filled" constructor completely IMO. (I can hardly imagine a scenario when you need a List containing 10 zeros, and want to make it growable. Can someone provide an example? Otherwise, for growable list, it looks more logical to start with <int>[] or List<int>.empty() rather than "filled" or any variant of it)
BTW, the fact that we make "value" a required parameter is important. If it's a "silent null" as it is now, this creates a spectacular pitfall for java programmers switching to dart. The problem is that, as @ds84182 noted above, in java we have fixed-size arrays, and in dart we have only List, but the brain of java programmer is wired in such a manner that List<int>(10) is (unconsciously!) being interpreted as an analog of java's int array of size 10 initialized with zeros. True, it's a stupid mistake, in retrospect, but I fell into this trap twice :-)
EDIT: I think we need 2 constructors:
List<T>(int count, T value); // for fixed-size list, like java's array
List<T>.empty([int initialCapacity=-1]); // for growable list, like java's ArrayList
In addition to that @tatumizer, typed arrays (which are Lists!) are initialized to 0 and do not allow null. They do not allow for growing, but can technically shrink with typed array views. These are 1:1 with Java's primitive arrays. You cannot resize them, you can (unlike Java) make sub-views.
Question for @lrhn: If you have a constructor like List(int count, [T value]), does value become required when the parameter is non-null? If not, it seems like it would be useful to have const default values now.
Implementation might be a bit weird, but ultimately having the ability to get the default zero value for a type parameter would be useful. It wouldn't even need to be exposed directly, just indirectly when you omit the default value for a parameterized type (so devs could write T defaultOf<T>([T value]) => value; as a tiny escape hatch to turn a type into some default instance of it, just like where Type typeOf<T>() => T is used). Then the list constructor could be defined as List(int count, [T value]), which would gracefully zero initialize an int list and would still null initialize an int? list.
When the given type is statically known to not have a default initializer, the parameter (and transitively all preceding parameters) becomes required. If the type is not statically known to have a default initializer, a runtime failure should occur (MissingDefaultValueError?).
To generalize default initialization, you could say something like "the default value for a union type is the first type with a default value, ignoring any preceding types without default values". Then you could declare that int? == Null | int, FutureOr<int> = int | Future<int>, and default initialization would work in both cases to provide null for int? and 0 for FutureOr<int>.
tl;dr: I think the currently specified NNBD behavior for the List constructor is painful in its inconsistency, and I'd prefer a version which does not need special-casing in the type system in the long run (when we remove non-NNBD mode).
My underlying motivation here is to end up with a good API in the long run, even if it costs a little more in the migration period.
And yes, we statically disallow creating a list with a potentially nullable type. When I say that there is "no good way to do that", I mean that the currently specified behavior isn't that good. It's not founded in the type system, and it's an exception only for this one constructor, and it's an exception we'll have to keep around forever.
The current special-casing of the List constructor is a minimal change, but it's not an a API I would have come up with if I started from scratch today.
The restriction that you cannot write List<T>(n) if T is potentially non-nullable is fairly intrusive and not supported by the type system. It means that you must always write a ? on the type when you use that constructor. If constructors had return types, we would have made it List<E?>. So it's a special case. It's not alone; length= has a similar issue, I have no solution for that, though.
What I would create from scratch is something like List(int count, E fillValue) . The fill value is required because in many cases it will be necessary. The value null is no longer a valid default value, and treating it as such for backwards compatibility will likely just extend the pain indefinitely instead of fixing it now.
It's is redundant with List.filled (except that that one can create growable lists). I'd likely be fine with removing the List constructor entirely, but if there is a need for a short way to create a fixed-length list, then List(len, 0) seems like it solves that. Another option is to introduce a literal syntax (probably not [: 6 * null :], no matter how neat it looks :)).
Question for @lrhn: If you have a constructor like List(int count, [T value]), does value become required when the parameter is non-null? If not, it seems like it would be useful to have const default values now.
No, it does not. The declaration is invalid because the optional parameter has a type which is potentially non-nullable and it has no default value.
It would be nice if that was possible, but ... it's not possible in practice. We tried it, and there was too many ways it just didn't work.
We have made some migrations where an optional parameter gets type T? instead of T, and it's a run-time error to pass null when T is non-nullable (fx List.fillRange). I wish I had a better solution for those, but the "treat the static type in one way in NNBD and another in non-NNBD mode" hack is a hack, and it only works for static functions, not interface signatures that someone else need to implement.
- List
(null) is still possible and a run-time error. It differs from List (). Remind me why we made this choice? Why are we not migrating this constructor to List([int length = 0])?
Maybe we can. We still need to make List() act differently from List(0), and the current implementation relies on there being no default value.
Another option is to introduce a literal syntax (probably not [: 6 * null :], no matter how neat it looks
[...6.times(null)] :-)
(times returns an iterable)
Sadly, it's a growable list. :(
Maybe we can. We still need to make
List()act differently fromList(0), and the current implementation relies on there being no default value.
I think the current implementations already use magic here, since List() works, but List(null) throws an error. So clearly they must be detecting whether a value was passed or not, which is not otherwise possible with an argument of type int.
It's is redundant with
List.filled(except that that one can create growable lists). I'd likely be fine with removing theListconstructor entirely,
I think I might be more supportive of this approach. We say:
This doesn't seem substantially more breaking to me (the migration tool should just rewrite the constructor to List.filled) and it avoids having to build in support for rewriting the API of core libraries in the front ends.
Deprecation plus disallow in NNBD mode should make the constructor eventually go away.
I'm fine with this. All use-cases can be covered by List.empty and List.filled, the only thing we lose is a short syntax for creating fixed-length lists. They are relatively rare. As Bob pointed out, I use them, mainly because I know they are cheaper for the VM, but I don't see much other use.
I have specified that it is an error to use this in Null safe code.
Most helpful comment
I think I might be more supportive of this approach. We say:
This doesn't seem substantially more breaking to me (the migration tool should just rewrite the constructor to
List.filled) and it avoids having to build in support for rewriting the API of core libraries in the front ends.