Skip to content

Latest commit

 

History

History
47 lines (35 loc) · 4.17 KB

LDM-2023-09-27.md

File metadata and controls

47 lines (35 loc) · 4.17 KB

C# Language Design Meeting for September 27th, 2023

Agenda

Quote of the Day

  • "Note today's date. Today is a historical day when we finish collection expressions" collective laughter

Discussion

Collection expressions

#5354
#7542

Today in our wrapping up of the last issues in collection expressions, we took a look at what optimizations the compiler is allowed to make. There's a tension here between allowing the compiler to make user code fast, and how understandable the feature will be. There are a number of features that we want the compiler to be able to take advantage of that might not normally be visible, or not be chosen, at the location of the collection expression. For example, .NET 8 is introducing a new set of AddRange extension methods on List<T> for adding a ReadOnlySpan to a List efficiently. We'd like to pick these methods for spreading an array into a collection expression target-typed to a List, but because AddRange is an extension method, it's never even discovered before the instance AddRange(IEnumerable) method is picked. This is not just a problem for collection expressions, of course; it's a problem for regular invocations as well. But do we want to limit collection expressions to behaving exactly as if the user had written the AddRange call themselves, or can we make it better? And if we can make it better, where do we limit ourselves? For user-defined collection types, should we search non-imported namespaces for better extension AddRange methods? If so, where do we stop? And how do API authors know how to design their collections to take advantage of these rules?

One thing we wanted to do as part of determining what APIs can be taken advantage of is to determine what our definition for the Base Class Library, or BCL, actually is. With input from the runtime team, we determined that what we should consider the BCL is anything in the System.* namespace. With that definition, we decided to split up our decisions: should we be allowed to assume well-behavedness and call any method at all for BCL types? And, as a separate question, should we be allowed to do that for non-BCL types? For BCL types, we think this is fairly safe. The System namespace has been "special" to the .NET ecosystem since the very beginning, and we feel ok with assuming that we can call things like the extension AddRange for BCL types in collection expression spreads. We're also ok with calling things like CopyTo on BCL types. We're more wary about non-BCL types, particularly around type author discoverability. How do we make sure that type authors know what their collections should do to get optimal performance, for example, or how do we know that a random AddRange method would actually do what we expect for specific BCL types like List? After much debate, we think we need to be more specific for non-BCL types, provide a specific list of things that we will call, and respect the normal rules that users will see if they write out the code manually.

Separately, we would like to address some of the problems that caused AddRange to be an extension in the first place. Because arrays and strings are both convertible to ReadOnlySpan<T> and implement IEnumerable<T>, adding that AddRange as an instance method would create ambiguities when passing a parameter of that type to AddRange. The array problem can be fixed with another more specific overload, but the string case cannot, as it would require generic specialization. This is a problem that we keep running into with various language features, and we think it's time to address it in C# 13.

Conclusion

The compiler can rely on all implementation details of BCL types, and potentially call methods that would not normally be preferred by the language in that location. For user types, we need to enumerate the list of methods that the compiler can call, and respect the normal language rules in those locations. This include methods from well-known BCL interfaces implemented by non-BCL types. We will look at potential ways to unify this behavior in C# 13.