csharplang/meetings/2020/LDM-2020-06-24.md
2020-12-14 20:33:50 -08:00

10 KiB

C# Language Design Meeting for June 24th, 2020

Agenda

  1. Confirming Function Pointer Decisions
  2. Parameter Null Checking
  3. Interface static Member Variance
  4. Property Enhancements

Quote of the Day

"It feels a little bit like we're playing code golf here"

Discussion

Confirming Function Pointer Decisions

https://github.com/dotnet/roslyn/issues/39865#issuecomment-647692516

There are a few open questions from a previous LDM and a followup email chain that need to be confirmed before they can be implemented. These questions center around calling convention type lookup and how identifiers need to be written in source. The grammar we had roughly proposed after the previous meeting is:

func_ptr_calling_convention
   : 'managed'
   | 'unmanaged' ('[' func_ptr_callkind ']')?

func_ptr_callkind
  : 'CallConvCdecl'
  | 'CallConvStdcall'
  | 'CallConvThiscall'
  | 'CallConvFastcall'
  | identifier (',' identifier)*
Calling Convention Lookup

When attempting to bind the identifier used in an unmanaged calling convention, should this follow standard lookup rules, such that the type must be in scope at the current location, or is using a form of special lookup that disregards the types in scope at the current location? The types valid in this location are a very specific set: they must come from the System.Runtime.CompilerServices namespace, and the types must have been defined in the same assembly that defines System.Object, regardless of the binding strategy used here, so it's really a question of whether the user has to include this namespace in their current scope, adding a bunch of types that they are generally not advised to use directly, and whether they can get an error because they defined their own calling convention.

Conclusion

Given the specificness required here, we will use special name lookup.

Required identifiers

The previous LDM did not specify the required syntax for the identifiers quite explicitly enough for implementation, and specified that identifiers should be lowercase while also having upper case identifiers in some later examples. The following rules are proposed as the steps the compiler will take to match the identifier to a type:

  1. Prepend CallConv onto the identifier. No casemapping is performed.
  2. Perform special name lookup with that typename in the System.Runtime.CompilerServices namespace only considering types that are defined in the core library of the program (the library that defines System.Object and has no dependencies itself).

We also reconsidered the decision from the previous LDM on using lowercase mapping for the identifier names. There is convention for this in other languages: C/C++, for example, use __cdecl or similar as their calling convention specifiers, and given that this feature will be used for native interop with libraries doing this it would be nice to have some parity. However, this would introduce several issues with name lookup: existing special name lookup allows us to modify the identifier specified in source, but it does not allow us to modify the names of the types we're matching against, which we would need to do here. There is certainly an algorithm that could be specified here, but we overall felt that this was too complicated for what was a split aesthetic preference among members.

Conclusion

The proposed rules are accepted. As a consquence, the identifier specified in source cannot start with CallConv in the name, unless the runtime were to add a type like CallConvCallConv.

Parameter Null Checking

We ended the previous meeting on this with two broad camps: support for the !! syntax, and support for some kind of keyword. Email discussion over the remainder of the week and polling showed that a clear majority supported the !! syntax.

Conclusion

We will be moving forward with !! as the syntax for parameter null checking:

public void M(Chitty chitty!!)

Interface static Member Variance

https://github.com/dotnet/csharplang/issues/3275

We considered variance in static interface members. Today, for co/contravariant type parameters used in these members, they must follow the full standard rules of variance, leading to some inconsistency with the way that static fields are treated vs static properties or methods:

public interface I<out T>
{
    static Task<T> F = Task.FromResult(default(T)); // No problem
    static Task<T> P => Task.FromResult(default(T));   //CS1961
    static Task<T> M() => Task.FromResult(default(T));    //CS1961
    static event EventHandler<T> E; // CS1961
}

Because these members are static and non-virtual, there aren't any safety issues here: you can't derive a looser/more restricted member in some fashion by subtyping the interface and overriding the member. We also considered whether this could potentially interfere with some of the other enhancements we hope to make regarding roles, type classes, and extensions. These should all be fine: we won't be able to retcon the existing static members to be virtual-by-default for interfaces, as that would end up being a breaking change on multiple levels, even without changing the variance behavior here.

We also considered whether this change could be considered a bug fix on top of C# 8, meaning that users would not have to opt into C# 9 in order to see this behavior. While the change is small and likely very rarely needed, we would still prefer to avoid breaking downlevel compilers.

Conclusion

We will allow static, non-virtual members in interfaces to treat type parameters as invariant, regardless of their declared variance, and will ship this change in C# 9.

Property Enhancements

In a previous LDM we started to look at various enhancements we could make to properties in response to customer feedback. Broadly, we feel that these can be addressed by one or more of the following ideas:

  1. Introduce a field contextual keyword that allows the user to refer to the backing storage of the property in the getter/setter of that property.
    • In this proposal, we can consider all properties today as having this field keyword. As an optimization, if the user does not refer to the backing field in the property body, we elide emitting of the field, which happens to be the behavior of all full properties today.
    • Of the proposals, this allows the most brevity for simple scenarios, allowing some lazily-fetched properties to be one-liners.
    • This proposal does not move the cliff far: if you need to have a type differing from the type of the property, or multiple fields in a single property, then you must fall back to a full property and expose the backing field to the entire class.
    • There are also questions about the nullability of these backing properties: must they be initialized? Or should we provide a way to declare them as nullable, despite the property itself not being nullable?
    • There was also concern that this is adding conceptual overhead for not enough gain: education would be needed on when backing fields are elided and when they are not, complicated by the additional backcompat overhead of ensuring that field isn't treated as a keyword when it can bind to an existing name.
  2. Allow field declarations in properties.
    • Conveniently for this proposal, a scoping set of braces for properties already exists.
    • This addresses the issue of properties backed by a different storage type: if you have a property that uses a Lazy<T> to initialize itself on first access, for example.
    • This also allows users to declare multiple backing fields, if they want to lock access to the property or combine multiple pieces of information into a single type in the public surface area.
    • In terms of user education, this is the simplest proposal. Since a set of braces already exists, the education is just "There's a new scope you can put fields in."
  3. Introduce a delegated property pattern into the language.
    • There have been a few proposals for this, such as #2657. Other languages have also adopted this type of feature, including Kotlin and Swift.
    • This is by far the most complex of the proposals, adding new patterns to the language and requiring declaration of a whole new type in order to remove one or possibly 2 fields from a general class scope.
    • By that same token, however, this is the most broad of the proposals, allowing users to write reusable property implementations that could be abstracted out.

The LDM broadly viewed these proposals as increasing in scope: the field keyword allows the most brief syntax, but forces users off the cliff back to full class-scoped fields immediately if their use case is not having a single backing field of the same type. Meanwhile, property-scoped fields don't allow for and encourage creating reusable helpers, like delegated properties would.

We also recognize that regardless of what decisions we make today, we're not done in this space. None of these proposals are mutually exclusive, and we can "turn the crank" by introducing one, and then adding more in a future release. There is interest among multiple LDM members in adding some form of reusable delegated properties or property wrappers, and adding one of either the field keyword or property-scoped fields does not preclude adding the other in a later release. Further, all of these proposals are early enough that we still have a bunch of design space to work through with them, while designing ahead enough to ensure that we don't add a wart on the language that we will regret in the future.

Conclusion

A majority of the LDM members would like to start by exploring the property-scoped locals space. We'll start by expanding that proposal with intent to include in C# 10, but will keep the other proposals in mind as we do so.