- Lambdas with explicit return types
- Target typing of collection expressions to core interfaces
- Loosening requirements for collection builder methods
For method arguments that are lambdas with explicit return types (and hence also explicit parameter types), this proposal removes the requirement that the lambda body bind correctly from overload resolution. As best we can tell, the requirement is vacuous, and no breaking behavior would come of this change.
The check is known to be algorithmically heavy in nested scenarios, and can cause an IDE to be bogged down for a considerable time. This is the case for implicit lambdas as well, but by removing the check from explicit lambdas, those could be a fallback for code that currently runs into this problem.
Approved. We couldn't think of any way this would cause a change in semantics.
What should C# do when a collection expression is target typed (implicitly converted) to one of the core interface types (
IEnumerable<T>
,
IReadOnlyCollection<T>
,
IReadOnlyList<T>
,
ICollection<T>
and
IList<T>
, plus their non-generic counterparts)?
IEnumerable<int> numbers = [ 1, 2, .. otherNumbers, 3 ];
If we allow this at all, we need to decide how a value of a concrete type is obtained for each interface. Our choice here will impact several key aspects of the design:
- Simplicity - it's easy to explain.
- Universality - it works everywhere.
- Brevity - you rarely need to embellish (with casts or the like).
- Performance - it maintains or improves performance compared to manually written creation code.
- Safety - avoids unwanted and surprising mutations through downcasting.
We considered the following general approaches:
- Do not allow target typing to interfaces
- Specify and guarantee corresponding concrete types
- Transparently pick concrete types as an implementation detail
The argument for supporting target typing to the core interfaces is that it will be very common. Especially many APIs (about 25% of all collection-taking APIs) take IEnumerable<T>
. If we go with option 1, collection expressions will not satisfy the goal of replacing the overwhelming majority of collection creation scenarios.
We've postponed other aspects of collection expressions (natural types, dictionary expressions) to future versions of the language, but this scenario seems much more mainline, and would be hard to live without.
The language already has special knowledge of these interfaces: Arrays are allowed to implicitly convert to them, and the IEnumerable
interfaces are consumed by foreach
and produced by iterators. So building specific behavior into the language for them would be nothing new.
Option 1 would only be the best option if we cannot make a good choice on behalf of the user. Option 2 and 3 are different strategies for how to make that default choice for them.
With option 2 we specify exactly which type gets instantiated for each interface. The option is very simple to understand if we pick a single type across all interfaces (e.g. List<T>
), but that is not great for performance (List<T>
always comes with two allocations, extra space to support mutation, etc.) or for safety (you can cast and mutate from the read-only interfaces). Alternatively we can pick different concrete types for each interface, but now it's a lot less simple! Also, for the read-only interfaces (including IEnumerable<T>
) we don't actually have ideal concrete types in the BCL today.
Option 3 gives us full freedom to pick ideal concrete types for each interface, or even different types for the same interface, depending on specific circumstances. In fact, the compiler can synthesize a brand new type for a given occurrence if that's the right trade-off, or the runtime can provide specifically optimized types that aren't in the normal public line-up. We could change our minds on the specifics from release to release, since users can't (easily - and definitely shouldn't!) take a dependency on the specific choices we make.
For the readonly interfaces, we could probably get significant performance optimizations. We could inline values, eliminate the count, reuse the object as its own enumerator to save an allocation (we use this trick in iterators today), etc. For empty collections we could reuse the same object every time.
For the mutable interfaces, List<T>
would actually be a fine choice. However, even if we use it, we wouldn't guarantee it version over version.
The approach of not telling you the exact type already has precedence in the language in the form of iterators, as well as in the BCL through LINQ query operators. The philosophy is also similar to pattern matching, where the logic of a switch
is highly optimized, and usually faster than what the user would have manually written.
Looking to the future, there aren't good existing dictionary types to implement IReadOnlyDictionary<TKey, TValue>
, and the optimal implementation strategy would depend heavily on e.g. the size of the dictionary. We could probably generate much better ones than could be provided in the BCL, because we can let the circumstances decide.
With option 3 users can't get a clear answer to what type we create. On the other hand, being able to tell people we will give them a really good one has its own simplicity! The upshot is that option 3 is simple only if the user can trust us to do so remarkably well in the overwhelming majority of cases that they never have to think about it.
We can't ignore the non-generic core interfaces (IEnumerable
, ICollection
and IList
). They probably don't change the overall decision, but they should be handled. Many APIs and frameworks, especially older ones, rely on these, but not actually often as a target type. More commonly they come in as object
and are type-tested against these interfaces at runtime. So while target typing should of course work for the non-generic interfaces, it is in fact more important that the concrete types we provide for the generic interfaces also implement the appropriate corresponding non-generic interfaces. That way, those type-discovery scenarios would work well over collections generated from collection expressions.
There's a more general question about type tests in option 3. If we do use a public type, people could discover it. Or they could discover any other collection interfaces that the thing implements. The latter might not be uncommon, so we should make deliberate decisions about that.
As a final consideration, this decision might intuitively seem linked to the (currently postponed) question about natural types for collection expressions - what do you get when you use var
?. However, the situations are different: With interfaces there's a clearly stated surface area to match, and it's ok for the concrete type not to be able to do anything else. For natural types there's a much more complex decision about what surface area to provide by default when people don't give a type, and there's no particular reason for that design choice to be linked to target typing.
We unanimously support option 3. We don't want to give even the most performance-conscious users any reason to shun collection expressions. We like the wiggle room for further optimizations in the future, and we think it is simplifying for the user not to have to care about exactly what gets generated. It fits the declarative motto of saying the "what", not the "how".
The working group will decide on the specifics for each target interface.
One of the ways types can support collection expressions is through a builder pattern. An example in the BCL is ImmutableArray<T>
. We put a [CollectionBuilder(...)]
attribute on it to say where to find a suitable factory method, in this case the existing ImmutableArray.Create
static method.
In the BCL we have an interface IImmutableList<T>
as well. The BCL could choose to point it to another factory method, but not the same one as above, because our current rule says the method must return the exact target type. This seems overly restrictive, and the proposal is to allow certain implicit conversions from the return type of the method to the type carrying the attribute. There's a range of choices around which conversions to allow. In order from loosest to tightest:
- Allow any implicit conversion.
- Allow standard conversions (excludes user-defined conversions).
- Allow reference and boxing conversions.
- Allow identity conversions only (i.e., don't fix the scenario).
We support option 3, reference and boxing conversions. It's a reasonable but conservative compromise that solves the scenario we have today, without forcing us to think through too much weirdness.