Back to Csharplang

Collection literals working group meeting for August 11, 2023

meetings/working-groups/collection-literals/CL-2023-08-11.md

latest4.4 KB
Original Source

Collection literals working group meeting for August 11, 2023

Agenda

  • Implementations used when targeting interface types

Discussion

Following up from the Aug 8th language design meeting, the working group considered specific concrete implementations to use for each interface that collection expressions can target. We reviewed the Compiler-synthesized types proposal in preparation for upcoming language design meetings which will get into more of the details of the collection expressions feature.

The language design meeting on Aug 8th concluded that we can later change the concrete type created for interfaces even after people start using the feature. For now, we'll decide on a starting point to recommend which we feel comfortable with.

Targeting mutable interfaces

When a collection expression targets IList<T> or ICollection<T>, we'll construct a List<T>. Even if people start depending on it, we could still change it in the future, similar to how .NET Core changed the concrete type returned from Enumerable.Empty<T>().

Targeting readonly interfaces

These are IEnumerable<T>, IReadOnlyCollection<T>, and IReadOnlyList<T>. Empty collections will get whatever instance the framework returns from Array.Empty<T>(). For non-empty collections targeting readonly interfaces, we considered three alternatives:

A: Synthesized type with a field of type IList<T> or List<T>
B: Just use the BCL's ReadOnlyCollection<T> type
C: Use List<T> for unknown-length construction and T[] for known-length.

Alternative C is great for performance but loses the safety that is a selling point to a significant part of our audience. Alternative B adds in the possibility for folks to cast and depend on the exact type (even though we do reserve the right to change the concrete type anyway), but it also doesn't have some of the benefits of alternative A; there are things we could do better in private implementations such as sealing it and wrapping a List<T> field rather than IList<T>, gaining better codegen with devirtualization and inlining.

With the extra indirection, indexing through a readonly wrapper is measurably worse than on an unwrapped array, twice as slow in a benchmark. On the other hand, wrappers add no cost to enumeration via foreach and IEnumerable<T> over the wrapped collection because the wrapped collection's enumerator can be returned directly through the wrapper.

For known-length collections, it may be worthwhile to construct and wrap a T[] rather than a List<T>, or even synthesize interface implementations with hardcoded counts and hardcoded fields for elements for certain sizes. T[] wouldn't be great for unknown-length collections since arrays must be exactly sized, often requiring a final copy from a larger-sized buffer that was used to hold the items while the length wasn't yet known. We could wrap an IList<T> field to handle both, or we could generate different wrappers to contain a List<T> field or a T[] field for the two situations as needed.

Even if the target type is only IEnumerable<T>, the synthesized types should implement IReadOnlyList<T>, IList<T>, and nongeneric IList. This is in case of subsequent usage against runtime checks, such as data binding or LINQ optimizations. It could result in bigger generated code size that isn't trimmable. Method bodies could be shared though, since most of the IList<T> and nongeneric IList members are mutating members which contractually should just throw NotSupportedException.

Conclusion

For ICollection<T> or IList<T>, we will build a new List<T>.

For (non-empty) IEnumerable<T>, IReadOnlyCollection<T>, or IReadOnlyList<T>, initially we will synthesize a private type within the assembly which is similar to the framework's System.Collections.ObjectModel.ReadOnlyCollection<T>. This is a straightforward starting point which prevents casting to the framework class. The design space is open and the compiler can create specialized classes later in a data-driven fashion.

In C# 13 when we get to dictionaries, IDictionary<T> will be a new Dictionary<T>, and IReadOnlyDictionary<T> will follow a strategy consistent with the strategy for IReadOnlyList<T>, such as synthesizing a private type similar to System.Collections.ObjectModel.ReadOnlyDictionary<TKey, TValue>.