Back to Csharplang

C# Language Design Meeting for March 5th, 2025

meetings/2025/LDM-2025-03-05.md

latest5.8 KB
Original Source

C# Language Design Meeting for March 5th, 2025

Agenda

Quote of the Day

  • "I'm sure there are some funky types out there like in old .NET" "I plead the 5th"
  • "Give the dot a shot"

Discussion

Dictionary expressions

Champion issue: https://github.com/dotnet/csharplang/issues/8659
Specification: https://github.com/dotnet/csharplang/pull/9158/files#diff-342dab893eaa8c901a2fafe92e2c6c13aaee9e42adcbd9115fac92ac0d1b9a22

We reviewed the proposed conversion and construction rules for concrete dictionary types that don't use CollectionBuilderAttribute.

The conversion rules pertain to how we decide that a given type is a dictionary type. For the pertinent situation, where the type is not an interface and does not provide a CollectionBuilderAttribute, the proposal requires the type to have an iteration type of KeyValuePair<TKey, TValue> for some TKey and TValue, as well as a matching get-set indexer.

If the type explicitly implemented e.g. IDictionary<TKey, TValue> we wouldn't recognize it as a dictionary - the type itself needs to have the right members.

When a type is a dictionary type, collection expressions will be implemented using its indexer rather than Add methods.

The construction rules pertain to how we bind to the indexer on such a type.

For key value pair elements e1:e2 the proposal invokes the setter of the best applicable indexer, based on the key and value expressions in each pair. This means that the invoked indexer is not necessarily the one that qualifies the type as a dictionary type in the first place, by the conversion rules above. An alternative would be to always use the indexer that matches the iteration type - the one that made the type a dictionary type.

For collection expressions we needed more nuance, because there were existing expectations around what would work, based on collection initializers. However, for indexers there is not a corresponding existing initializer case to be consistent with.

For expression elements e and spread elements ..e the proposed rules require elements to implicitly convert to KeyValuePair<TKey, TValue>, exactly matching the iteration type of the dictionary type. There was some concern that this is too restrictive.

A more permissive alternative would be to allow elements to be KeyValuePair<...> ("KVP" for short) with different type arguments, as long as those type arguments are implicitly convertible to TKey and TValue respectively. This would be restrictive in a different way, since the elements would have to be a KVP, not just implicitly convert to one. This could even lead to breaking changes, where ordinary collection expressions have worked for dictionary types in the past due to user-defined conversions to KVP.

A combined alternative would look for any KVP types that the element is implicitly convertible to, and if there is exactly one check that its type arguments have implicit conversions to TKey and TValue. This would mitigate the potential breaking change, but searching for a unique implicit conversion seems a bit dubious.

Conclusion

  • The rules for when a type is considered a dictionary are approved as proposed.
  • The indexer used in dictionary expressions should be the one that qualifies the type as a dictionary type, not the best fit based on overload resolution.
  • In expression and spread elements we'll allow them to be KVPs with different key and value types, as long as those are in turn implicitly convertible to the key and value types of the dictionary. For now the elements have to be KVPs, not just implicitly convertible to them. If that becomes a problem we are open to loosening the rules, but we don't have a good proposal for that yet.

Target-typed static member lookup

Champion issue: https://github.com/dotnet/csharplang/issues/9138
Specification: https://github.com/dotnet/csharplang/blob/e2e957687cf2a165fb1a0fb05f1d72ddfbe8f932/proposals/target-typed-static-member-lookup.md

The feature allows the static members of a type to be accessed directly without qualification when the expression is target typed by that type:

c#
type.GetMethod("Name", .Public | .Instance | .DeclaredOnly); // BindingFlags.Public | ...

The proposal meshes well with some proposals for discriminated unions, but is useful for today's classes or enums.

It supports the common case where a type has static members of that type. Enums are common, and nested derived types are a way to express DUs today. Also, special values or factories are often exposed this way.

This is a place where a lot of redundancy and repetition exists today, especially when types are large and/or generic. Static usings aren't really an adequate response. They bring the members into scope everywhere, which can be very noisy, and for generic types they only allow specific instantiations to be included.

Using a sigil (.) in front of the name helps avoid breaking changes or convoluted lookup rules. It helps narrow the field of possible completions and may improve understanding of the code.

At the same time there's also a lot of resistance to having a sigil instead of just a name. It feels like users may have difficulty realizing when they could choose to use this feature. Without the dot it would feel like the compiler just knows what you mean.

Perhaps there is a path we can take that allows us to get feedback from the ecosystem on this question?

Finally there's also an argument that the whole feature makes code harder to read, and the meaning of names too complex to understand, regardless of syntax.

Conclusion

Most of us feel the feature is valuable, and we want to continue investigating and debating. The syntax question is a stumbling block for sure.