meetings/2023/LDM-2023-11-15.md
params improvementsparams improvementshttps://github.com/dotnet/csharplang/issues/7700
https://github.com/dotnet/csharplang/pull/7661
We started today by reviewing the latest proposal for improving params. This has morphed several times over the past few years; previous issues included
https://github.com/dotnet/csharplang/issues/179 and https://github.com/dotnet/csharplang/issues/1757; this proposal encompasses both of these, and tries to
unify with the C# 12 feature collection expressions. The general driving principle of this proposal, and how the LDM is thinking of the feature, is that if
the user can make a collection of the type, they should be able to mark it params. This may require some spec work between the two features to extract out
common elements without requiring invasive changing of everywhere that params exists today, but it's a good driving principle.
Overall, we are very interested in the feature. It's a rare example of something that both adds expressivity to C# while actually simplifying the language:
with this change and a bit of spec work, we have a much simpler story around collections in the language. You can deconstruct them via pattern matching,
index into them, construct them via collection expressions, and provide implicit construction via params. We consider whether we could simply not do this
feature and let collection expressions fill the gap, but there's one clear problem we need to solve: the BCL would like to add params (ReadOnly)Span overloads
to many APIs to make them more efficient. This isn't a gap that can be solved by collection expressions, and we think that, if we're going to make a change to
params, we should do the entire feature to make reasoning about collections in the language easier, rather than just changing the special cases users need
to think about.
We also considered some ref-struct specific design questions. In terms of allocations, we think the right approach is to again align with collection expressions;
if a collection expression wrapping the arguments passed to a params Span allocates, then the expanded invocation form should behave the same. We left a lot
of leeway in the language for optimizations here, so we think continuing to keep to the same guarantees is the obvious thing to do. It also helps prevent
surprise behavioral differences if a user passes a collection expression to a params parameter vs calling in expanded form. The other question we considered
is whether to have the params parameter be scoped by default. Some members were concerned that params is not an obvious enough indicator of scopedness,
but we have somewhat already crossed this bridge with out parameters. We also don't have any current scenarios that need an unscoped params parameter, only
ones that need scoped. Given that, we think we should start with scoped by default, and let feedback inform whether we've made the right decision.
Finally, we noticed that the overload resolution tiebreaking rules may be missing a few clauses that collection expressions have around non-ref struct comparisons, so we'll make sure that's in the spec if needed.
Proposal is accepted. We will follow the same allocation guarantess as collection expressions, and ref struct params parameters will be scoped by default.
https://github.com/dotnet/csharplang/issues/5354
https://github.com/dotnet/csharplang/issues/7626
This issue came up late in the design cycle and presented somewhat of a challenge to the compiler. In particular, some types may violate what we'd otherwise
consider an idiomatic implementation, having an Add method that allows T? while the collection iteration type is T. While we don't think that's a totally
unreasonable pattern, it does generally violate our pre-existing rules for collection expressions. For example, if a collection iteration type is T1, we don't
allow using an unrelated T2 as an expression, even if the collection type technically has an Add(T2) method. This differs from collection initializers, and
we think that we should carry that difference through here. This also means that any collection expression that are used with older collection types that only
implement IEnumerable will not get nullability warnings; this is because IEnumerator.Current is unannotated. Given that we already skip nullability warnings
there for foreach, we're ok with skipping them on construction as well.
We will do nullability analysis based on the iteration type of the collection expression.
https://github.com/dotnet/csharplang/issues/7684
This is more of a bugfix clarification to the range feature in C# 8. For scenarios where an indexer is used in a nested member initializer, we need to decide what counts as the "indexer" as defined in the spec:
When an initializer target refers to an indexer, the arguments to the indexer shall always be evaluated exactly once. Thus, even if the arguments end up never getting used (e.g., because of an empty nested initializer), they are evaluated for their side effects.
The question becomes: is the "indexer" here the virtual Index-based indexer, or is it the real int-based indexer that is called under the hood? This ends
up having an effect on whether we call Length on the collection multiple times or not. While pathological, this could potentially be observable if the nested
member initializer appends to the containing collection in some fashion. If we treat the Index-based indexer as the indexer referred to in the spec here, then
appends will be observed. If we instead say the int-based indexer is the indexer referred to by the spec, appends would not be observed, as Length would only
be evaluated once. This has an even further wrinkle for empty nested initializers: do such initializers actually evaluate Length? If the int-based indexer is
the "indexer" in the above, the wording would suggest yes, as it is an "argument" to the indexer. However, we also think that there's a line of reasoning where
"argument" is only referring to the user-written code: Length is not user written code, so by that logic, it would be safe to elide in the empty case. That leaves
us with a few options:
Index indexer. Revaluate Length on every nested member initializer.Length even when the nested object initializer is empty.Length when the nested object initializer is empty.While we think that, if we were redoing the feature today, we'd pick option 4, we don't think that we can make such a change at this point in the language evolution. After some discussion, we settled on option 3, falling back to 2 if it ends up being too complicated to implement.
Option 3, cache the argument to the real indexer, do not evaluate Length when the nested object initializer is empty, falling back to option 2 if it proves an
implementation challenge.