10 KiB
C# Language Design Meeting for June 24th, 2020
Agenda
- Confirming Function Pointer Decisions
- Parameter Null Checking
- Interface
static
Member Variance - 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:
- Prepend
CallConv
onto the identifier. No casemapping is performed. - 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 definesSystem.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:
- 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.
- In this proposal, we can consider all properties today as having this
- 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."
- 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.