- "So 'The customer wants it therefore it's wrong' is your new motto?"
We looked at an inconsistency in the order of evaluation between Index
and Range
operations, when those are applied to a type that does not have
native methods that accept Index
or Range
types. The Index
portion of the spec is fairly straightforward and we were easily able to determine
the compiler had a bug with the lowering it performed. However, the specification is unclear about the Range
translation: it does not clearly
communicate the order of evaluation for the receiver
, Length
, and expr
expressions that are cached. Our current behavior is inconsistent with
the Index
behavior, and likely confusing to users. After a bit of discussion, we unanimously agreed to standardize the order of evaluation to be
consistent with the written original order in the source, followed by compiler-needed components if necessary. For indexers, this is receiver
, then
expr
, then Length
if needed.
Codify the expected behavior as receiver
then expr
then Length
.
We took a first look at a proposal for a "collection literal" syntax in C#, doing a general overview of the proposed syntax form and gathering general
feedback on the proposal. We hope to approach this proposal as a "one syntax to rule them all" form, that can achieve correspondence with the list
pattern syntax form, support new types that collection initializers can't today (such as ImmutableArray<T>
), and serve as a zero or even negative
cost abstraction around list creation that is as good or better than hand-written user code.
There's some unfortunate bad interaction with existing collection initializers, however. They'll still exist, and they will be advantageous in some
cases. Since the user is explicitly calling new Type
on them, they have an obvious natural type, while collection literals will need some form of
target-type for cases where there is no obvious natural type or if the user is trying to select between ambiguous overloads. They also probably wouldn't
support this new splat operator, which also makes them worse in that case; even if we extended support for it, it's very likely that the new form would
lower to a more efficient form with the pre-length calculation that it able to support.
We're unsure about some of the various axes of flexibility in this space. Some of them that we talked about are:
- Existing collection initializers require the type to implement
IEnumerable
to be considered collection-like. Do we want to keep this restriction for the new form? It would lead to an odd non-correspondence with list patterns, since they are pattern-based and not interface-based. The proposal actually started more restrictive, but loosened to the current form after considering things likeHashSet<int> h = [1, 2, 3];
and deciding that was perfectly fine. - How specific do we want to make the lowering for various forms? Since it's a goal of the feature to be as optimal as possible here, if we prescribe too specific of lowering forms then we potentially make it difficult to optimize the implementation. We know from experience, though, that while we'll have a small window of opportunity just after ship to make changes to the way code is emitted, making changes years on will be dangerous and will likely have impacts on some user's code.
- We think that a natural type for this syntax will have a lot of benefits, such as allowing it to be used for interfaces (
IEnumerable<int> ie = [1, 2];
) and it will be in line with the recent work we did around lambda expressions. However, unlike lambdas, we don't have a single list type in .NET that is unambiguously the natural type it should be. We'll need to dig into the pros and cons of the space and decide on how we want to approach this.
We also talked briefly about possible extensions of this space. One obvious one is dictionary literals. We think we can move ahead with collection literals for now, as long as we keep both dictionary literals and dictionary patterns, to be sure we're holding ourselves to the correspondence principle. Another area we should investigate is list comprehensions. We're not sure whether we want comprehensions, but we should at least look at the space and make sure we're comfortable that we'd never consider them or leave ourselves the space to be able to consider them in the future.
Finally, we want to look at other contemporary languages and see what they do for this space. For example, F# has dedicated syntax forms for each of it's
main list/sequence types. Unlike C#, they have largely unified on a very few specific collection types. This allows them to have dedicated syntax for each
one, but this approach isn't likely to work well in C# because we have a much wider variety of commonly-used collections, tuned for performance across a
variety of situtations. Kotlin takes a different approach of having well-known methods to construct different collections, such as listOf
or arrayOf
.
It's possible that, with params Span
, we'd be able to achieve 90% of what we're trying to do without needing to build anything into the language itself.
And then there are languages like Swift, Python, Scala, and others that actually have a dedicated literal for lists, with varying degrees of flexibility.
No conclusions today. A smaller group will begin a deep dive into the space to flesh out the various questions and components of this proposal, and come back to LDM with more research in the area.