- Someone in the room picks up a knife to slice some bread "That is going to make it impossible for the people online to hear" "Thank you for advocating for us" "Thank you for also drawing attention to the fact that we don't have any bread"
#7822
https://github.com/dotnet/csharplang/blob/dcf46ba377e1ce356f69f981f36b2273c3179ca3/proposals/dictionary-expressions.md
Today, we took a look at the first proposal for dictionary expressions. There are a number of open questions that need to be answered, including syntax decisions;
however, in order to make progress on the more thorny semantics questions, we are using the [key: value]
strawman syntax from the proposal for now. We will make
concrete decisions on the syntax at a later time. The syntax we end up with is important, of course, but fairly unconnected to the semantics we need to delve into
at the moment.
The first two questions we looked at are highly related. They are:
- Should we allow KeyValuePair expression elements in dictionary expressions?
- Should we allow spreading an enumerable of KeyValuePair in a dictionary expression?
The first question can even be expressed in terms of the second: after all, if we allow the second, and we give collection expressions a natural type, then users
could just do [.. [keyValuePairExpression]]
even if we don't allow 1. This question brought up a recurring theme through the rest of the open questions we looked
at: we're currently calling these dictionary expressions, because that's a catchy name that immediately conveys the general target of these. However, what are they
actually? Are they dictionary expressions, or are they actually collections of associated keys and values? The LDM is unanimously in favor of supporting both these
scenarios.
Next, we looked at whether these expressions should be able to initialize generalized collection types, or just "dictionary" types. Specifically, can these types
initialize a List<KeyValuePair<TKey, TValue>>
and things like it, or are they stuck initializing things like Dictionary<TKey, TValue>
. This is a trickier
question; our correspondence principle for creation and consumption does not generally hold up for such scenarios. As an example:
List<KeyValuePair<string, int>> list = ["mads": 21];
Console.Write(list["mads"]); // Can't consume like this because it's a List
No matter what syntax we choose for the initialization form, there won't be a correspondence with usage here, as List<>
is accessed by index, not by key. This
gives a few members of the LDM pause, but overall, we're in favor of allowing this.
The next topic was on how restrictive we should be on KeyValuePair
itself; namely, must the type involved be exactly
System.Collections.Generic.KeyValuePair<TKey, TValue>
, or should it be expanded? One example is (TKey string, TValue value)
. This tuple type is conceptually a
KVP, and some collections use it or other similar tuple types instead of KVP. For example, PriorityQueue<TElement, TPriority>
exposes an UnorderedItems
property
that is an IReadOnlyList<(TElement Element, TPriority Priority)>
. We may want to enable merging of 2 queues by doing
PriorityQueue<TElement, TPriority> p = [.. originalQueue1.UnorderedItems, .. originalQueue2.UnorderedItems];
. This example raises several questions of its own
(what APIs would we use, since there's neither Add
nor indexers on PriorityQueue
?), but there is at least some merit in considering the idea.
There's another question here, too: what about conversions between keys and values in the source collection expression and the destination? For example:
Dictionary<int, int> d = [shortKey: shortValue];
, where the key and value need to be subjected to a short->int
conversion. Are conversions like this acceptable?
This plays back into the question of what these collections actually are: if they're conceptually collection expressions, then the conversions around the keys and
values may be less acceptable. However, if they're collections of associated keys and values, then subjecting the individual parts may be fine.
Ultimately, LDM did not reach a decision on this question today. We'll need to dig more into it in future meetings.
Finally, we looked at the semantics of how a dictionary expression will build its resulting value: does it overwrite existing values, or does it error when there are
duplicate keys? There are few different interesting points here: we're currently calling these dictionary expressions. That, combined with how collection expressions
are specified around Add
methods, may give users a natural intuition that we'll be calling Add
on the dictionary, which will throw when a duplicate is encountered.
However, System.Collection.Generics.Dictionary<,>
may not be the only dictionary a user would key off of, and .NET has very inconsistent behavior for what Add
on
a dictionary-like type will do. ImmutableDictionary<,>
will not throw, and will keep the original value if the new value compares equal to the original value.
FrozenDictionary<,>
immediately throws NotSupportedException
s; the list goes on, but the only consistent bit is that we're inconsistent in behavior. These
discrepancies had us reconsider the question from another direction: rather than "What is the behavior", we instead want to consider "What APIs are we calling"; this
will allow each scenario to tailor its APIs to handle the question directly, rather than forcing a single behavior on all collection types.
There's some concern among members that using an indexer, rather than calling an Add
method, will lead to concerning behavioral differences if a user decides to
refactor existing collection initializers to dictionary expressions. We do have existing behavior in our IDE fixers that can help convey that a refactoring is not
exactly semantics-preserving, but it would require the user understanding the exact difference and examining usages to ensure that they're not falling afoul of bad
behavior in the presence of indexer assignment. However, a larger portion of the LDM is more concerned that, in the presence of spreads (which have no collection
initializer analogy), Add
is more often the wrong thing to call than using an object initializer. That is the direction we'll go with for now, and explore how we
specify the rules given indexer usage instead of Add
calls.
Questions 1 and 2 are wholeheartedly approved; KVP expressions and spreads of KVP collections will work in dictionary expressions.
Question 3 is approved; dictionary expressions will be able to convert to collection types that have a KVP element type.
Question 4 needs more time to be considered.
Question 5 settled on using indexers as the lowering form.