Compare commits

..

2 commits

Author SHA1 Message Date
AlekseyTs 6413e59c4b
PR feedback
Co-authored-by: Julien Couvreur <jcouv@users.noreply.github.com>
2021-11-03 09:27:57 -07:00
AlekseyTs 1836890edb
Explicitly specify order of evaluation for implicit Index/Range support
This is a follow up on an LDM decision, see https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-11-01.md for more information.
2021-11-02 13:52:30 -07:00
7 changed files with 95 additions and 367 deletions

View file

@ -57,7 +57,7 @@ the behavior here as it has also been a source of Roslyn-API consumer confusion
#### Conclusions
We will proceed with `field` as the keyword, and would like to have .NET 6 warning wave to nudge users to write code that has no chance of being
We will proceed with `field` as the keyword, and would like to have .NET 6 warning wave to nudge users to write code that has no change of being
ambiguous.
### Improved interpolated strings

View file

@ -1,80 +0,0 @@
# C# Language Design Meeting for November 3rd, 2021
## Agenda
1. [Name shadowing in local functions](#name-shadowing-in-local-functions)
2. [`params Span<T>`](#params-spant)
## Quote(s) of the Day
- "params Span<T>. This is an easy one, we should be out in 10 minutes."
- "For some value of 8"
- "Stack goes up. Stack goes down. You can't explain that."
- "That killed my monologue." "Did you mean 'evil villain monologue'?" "No one killed me during the monologue
therefore I can't be an evil villain." "No one kills the evil villain during the monologue, they just learn the evil plan and then escape to thwart it when the villain
leaves the implementation to his minions."
- "Solved the mystery: cat sitting on the spacebar"
## Discussion
### Name shadowing in local functions
https://github.com/dotnet/csharplang/discussions/5327
In C# 8, we relaxed the name shadowing requirements for lambdas and local functions, but we wanted to revisit the specific implementation strategy and whether we went too
far. In particular, we allowed a local function to _both_ capture a variable from an outer scope and shadow that variable in a nested scope inside the local function. This
_and_ in the previous sentence is the part we're concerned with: several members of the LDT didn't realize that we were agreeing to this in the original discussions on
shadowing.
There are two general models of shadowing in C#:
* Locals shadowing another local within the same function. This is expressly disallowed, and has been since C# 1.
* Locals shadowing a field. This is allowed, but the user can always get to the field version by using `this`. In the same method, it is possible to use the field without
the `this` qualifier and shadow it in a nested scope.
Shadowing between local functions while also capturing that local from the outer scope has similarities to both of these models: on the one hand, it's a local variable,
even if that variable has been potentially lifted to a field in an implicit closure. On the other hand, it's a variable from a scope outside the current function, just like
a field. This duality leads to conflicting resolution strategies, but we ultimately think this has more in common with locals shadowing other locals in the same method than
it does with locals shadowing fields.
The rules we wish we had implemented are: if a lambda or local function (or a nested lambda or local function) captures a variable from an outer method, it is an error to
shadow that variable. However, these rules have been out in the wild for nearly 3 years at this point, and we don't feel that the level of concern warrants a language change
or a warning wave to cover it. If an analyzer wants to implement such a suggestion (in dotnet/roslyn-analyzers, for example), they are free to, but the compiler itself will
not do so.
#### Conclusion
We regret that simultaneous capture/shadowing was allowed, and now it's too late to really fix it. Future features should keep this regret in mind and not use the way shadowing
in local functions/lambdas works as precedent.
### `params Span<T>`
https://github.com/dotnet/csharplang/issues/1757
`params Span<T>` is a feature that has been requested since we first implemented `Span<T>` in C# 7, and has a number of benefits. Today, users interested in performance with
`params` methods need to maintain overloads for common scenarios, such as passing 1-4 arguments (as we do for `Console.WriteLine`). These APIs are often not straightforward
to implement: if there was a simple common method they could all delegate to, that API would have been exposed in the first place.
Because of this focus, `params Span<T>` is an interesting first for the language: a `Span<T>`-based API that is explicitly targetted at the average C# developer, rather than
at someone who already knows what a `Span<T>` and why they would want to use it. There are also strong conflicting desires in the space that makes it very difficult to design
a single lowering strategy. We want to avoid allocations where possible, but we also want to avoid blowing out the stack when large numbers of parameters are passed to a
`params` method. This is particularly important in recursive code: Roslyn regularly runs into issues when inlining decisions can affect whether we are able to compile some
customer's code, as the compiler is an extremely recursive codebase.
At the same time, we also want to avoid being overly specific in the language specification on how this feature works. We would like to view the details as more an implementation
concern, much like we do with how lambdas are emitted, and avoid making strong guarantees about what will or won't stackalloc for what scenarios. As this is more targetted to
perf than lambdas are, it's possible that we can't be as glib in this space as we can for lambdas, but it's an aspiration for the feature.
Despite our desire to be less specific, we do think it's important to the initial design, and therefore the conception of who the feature is for, to have an implementation
strategy in mind. There are a number of different strategies that have been brought, with various pros and cons:
* Using a custom struct with a specific layout, and then making a span from the start of that struct.
* A shadow-stack that exists solely for the purpose of being able to give out and return stackalloc'd chunks of memory (think RAII stack space).
* Pushing variables onto the stack, then making a span from the start of those pushes (similar to the first approach, but without a dedicated type to be wrapped).
* `stackalloc` space that can be popped after a function is called.
* Not doing any kind of optimized IL in C#, and relying on the JIT to perform the escape analysis and translation from array to stackalloc when viable.
#### Conclusion
We'd like to see a group from the compiler and the runtime work through these proposed strategies and come up with "the way" or combination of ways that this will work, so we
can start to see how the user experience for the feature would actually work.

View file

@ -20,20 +20,15 @@ All schedule items must have a public issue or checked in proposal that can be l
## Nov 10, 2021
- Self-constraints (Tanner): https://github.com/dotnet/csharplang/pull/5387
## Nov 3, 2021
- Name shadowing in nested functions (Chuck): https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/shadowing-in-nested-functions.md
- `params Span<T>` and implicit stack allocation of arrays (Chuck)
# C# Language Design Notes for 2021
Overview of meetings and agendas for 2021
## Nov 3, 2021
[C# Language Design Notes for November 3rd, 2021](https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-11-03.md)
1. Name shadowing in local functions
2. `params Span<T>`
## Nov 1, 2021
[C# Language Design Notes for November 1st, 2021](https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-11-01.md)

View file

@ -60,7 +60,7 @@ var f = [A1, A2][A3] () => { }; // ok
var g = ([A1][A2, A3] int x) => x; // ok
```
Attributes are not supported for _anonymous methods_ declared with `delegate { }` syntax.
Attributes are not supported for anonymous methods declared with `delegate { }` syntax.
```csharp
f = [A] delegate { return 1; }; // syntax error at 'delegate'
f = delegate ([A] int x) { return x; }; // syntax error at '['
@ -86,6 +86,14 @@ The changes proposed here are targeted at the `Delegate` driven scenario.
It should be valid to inspect the `MethodInfo` associated with a `Delegate` instance to determine the signature of the lambda expression or local function including any explicit attributes and additional metadata emitted by the compiler such as default parameters.
This allows teams such as ASP.NET to make available the same behaviors for lambdas and local functions as ordinary methods.
_Open issue: Should default values be supported for lambda expression parameters for completeness?_
### Well-known attributes
_Should `System.Diagnostics.ConditionalAttribute` be disallowed on lambda expressions since there are few scenarios where a lambda expression could be used conditionally?_
```csharp
([Conditional("DEBUG")] static (x, y) => Assert(x == y))(a, b); // ok?
```
## Explicit return type
An explicit return type may be specified before the parenthesized parameter list.
```csharp
@ -116,82 +124,39 @@ Func<object> f1 = string () => null; // error
Func<object?> f2 = object () => x; // warning
```
The parser allows lambda expressions with `ref` return types within expressions without additional parentheses.
The parser should allow ref return types in assignment without parentheses.
```csharp
d = ref int () => x; // d = (ref int () => x)
F(ref int () => x); // F((ref int () => x))
```
`var` cannot be used as an explicit return type for lambda expressions.
```csharp
class var { }
d = var (var v) => v; // error: contextual keyword 'var' cannot be used as explicit lambda return type
d = @var (var v) => v; // ok
d = ref var (ref var v) => ref v; // error: contextual keyword 'var' cannot be used as explicit lambda return type
d = ref @var (ref var v) => ref v; // ok
Delegate d1 = (ref int () => x); // ok
Delegate d2 = ref int () => x; // ok
```
## Natural (function) type
An [_anonymous function_ expression](../../spec/expressions.md#anonymous-function-expressions) (a _lambda expression_ or an _anonymous method_) has a natural type if the parameters types are explicit and the return type is either explicit or can be inferred (see [inferred return type](../../spec/expressions.md#inferred-return-type)).
A lambda expression has a natural type if the parameters types are explicit and the return type is either explicit or can be inferred (see [inferred return type](../../spec/expressions.md#inferred-return-type)).
A _method group_ has a natural type if all candidate methods in the method group have a common signature. (If the method group may include extension methods, the candidates include the containing type and all extension method scopes.)
A method group has a natural type if all candidate methods in the method group have a common signature. (If the method group may include extension methods, the candidates include the containing type and all extension method scopes.)
The natural type of an anonymous function expression or method group is a _function_type_.
The natural type of a lambda expression or method group is a _function_type_.
A _function_type_ represents a method signature: the parameter types and ref kinds, and return type and ref kind.
Anonymous function expressions or method groups with the same signature have the same _function_type_.
_Function_types_ are used in a few specific contexts only:
- implicit and explicit conversions
- [method type inference](../../spec/expressions.md#type-inference) and [best common type](../../spec/expressions.md#finding-the-best-common-type-of-a-set-of-expressions)
- `var` initializers
Lambda expressions or method groups with the same signature have the same _function_type_.
A _function_type_ exists at compile time only: _function_types_ do not appear in source or metadata.
_Open issue: Should the function_type be available from the compiler API?_
### Conversions
From a _function_type_ `F` there are implicit _function_type_ conversions:
- To a _function_type_ `G` if the parameters and return types of `F` are variance-convertible to the parameters and return type of `G`
- To `System.MulticastDelegate` or base classes or interfaces of `System.MulticastDelegate`
- To `System.Linq.Expressions.Expression` or `System.Linq.Expressions.LambdaExpression`
Anonymous function expressions and method groups already have _conversions from expression_ to delegate types and expression tree types (see [anonymous function conversions](../../spec/conversions.md#anonymous-function-conversions) and [method group conversions](../../spec/conversions.md#method-group-conversions)). Those conversions are sufficient for converting to strongly-typed delegate types and expression tree types. The _function_type_ conversions above add _conversions from type_ to the base types only: `System.MulticastDelegate`, `System.Linq.Expressions.Expression`, etc.
Lambda expressions and method groups already have _conversions from expression_ to delegate types and expression tree types (see [anonymous function conversions](https://github.com/dotnet/csharplang/blob/main/spec/conversions.md#anonymous-function-conversions) and [method group conversions](https://github.com/dotnet/csharplang/blob/main/spec/conversions.md#method-group-conversions)). Those conversions are sufficient for converting to strongly-typed delegate types and expression tree types. The _function_type_ conversions above add _conversions from type_ to the base types only: `System.MulticastDelegate`, `System.Linq.Expressions.Expression`, etc.
There are no conversions to a _function_type_ from a type other than a _function_type_.
There are no explicit conversions for _function_types_ since _function_types_ cannot be referenced in source.
A conversion to `System.MulticastDelegate` or base type or interface realizes the anonymous function or method group as an instance of an appropriate delegate type.
A conversion to `System.MulticastDelegate` or base type or interface realizes the lambda or method group as an instance of an appropriate delegate type.
A conversion to `System.Linq.Expressions.Expression<TDelegate>` or base type realizes the lambda expression as an expression tree with an appropriate delegate type.
```csharp
Delegate d = delegate (object obj) { }; // Action<object>
Expression e = () => ""; // Expression<Func<string>>
object o = "".Clone; // Func<object>
```
_Function_type_ conversions are not implicit or explicit [standard conversions](../../spec/conversions.md#standard-conversions) and are not considered when determining whether a user-defined conversion operator is applicable to an anonymous function or method group.
From [evaluation of user defined conversions](../../spec/conversions.md#evaluation-of-user-defined-conversions):
> For a conversion operator to be applicable, it must be possible to perform a standard conversion ([Standard conversions](../../spec/conversions.md#standard-conversions)) from the source type to the operand type of the operator, and it must be possible to perform a standard conversion from the result type of the operator to the target type.
```csharp
class C
{
public static implicit operator C(Delegate d) { ... }
}
C c;
c = () => 1; // error: cannot convert lambda expression to type 'C'
c = (C)(() => 2); // error: cannot convert lambda expression to type 'C'
```
A warning is reported for an implicit conversion of a method group to `object`, since the conversion is valid but perhaps unintentional.
```csharp
Random r = new Random();
object obj;
obj = r.NextDouble; // warning: Converting method group to 'object'. Did you intend to invoke the method?
obj = (object)r.NextDouble; // ok
```
### Type inference
The existing rules for type inference are mostly unchanged (see [type inference](../../spec/expressions.md#type-inference)). There are however a **couple of changes** below to specific phases of type inference.
@ -222,19 +187,19 @@ The [first phase](../../spec/expressions.md#the-first-phase) allows an anonymous
> * Otherwise, type inference fails.
### Best common type
[Best common type](../../spec/expressions.md#finding-the-best-common-type-of-a-set-of-expressions) is defined in terms of type inference so the type inference changes above apply to best common type as well.
Best common type is defined in terms of type inference (see [finding the best common type](../../spec/expressions.md#finding-the-best-common-type-of-a-set-of-expressions)) so the changes above apply to best common type as well.
```csharp
var fs = new[] { (string s) => s.Length; (string s) => int.Parse(s) } // Func<string, int>[]
```
### `var`
Anonymous functions and method groups with function types can be used as initializers in `var` declarations.
Lambda expressions and method groups with natural types can be used as initializers in `var` declarations.
```csharp
var f1 = () => default; // error: cannot infer type
var f2 = x => x; // error: cannot infer type
var f3 = () => 1; // System.Func<int>
var f4 = string () => null; // System.Func<string>
var f5 = delegate (object o) { }; // System.Action<object>
var f1 = () => default; // error: cannot infer type
var f2 = x => { }; // error: cannot infer type
var f3 = x => x; // error: cannot infer type
var f4 = () => 1; // System.Func<int>
var f5 = string () => null; // System.Func<string>
static void F1() { }
static void F1<T>(this T t) { }
@ -245,47 +210,52 @@ var f7 = "".F1; // System.Action
var f8 = F2; // System.Action<string>
```
Function types are not used in assignments to discards.
```csharp
d = () => 0; // ok
_ = () => 1; // error
```
### Delegate types
The delegate type for the anonymous function or method group with parameter types `P1, ..., Pn` and return type `R` is:
- if any parameter or return value is not by value, or there are more than 16 parameters, or any of the parameter types or return are not valid type arguments (say, `(int* p) => { }`), then the delegate is a synthesized `internal` anonymous delegate type with signature that matches the anonymous function or method group, and with parameter names `arg1, ..., argn` or `arg` if a single parameter;
The delegate type for the lambda or method group and parameter types `P1, ..., Pn` and return type `R` is:
- if any parameter or return value is not by value, or there are more than 16 parameters, or any of the parameter types or return are not valid type arguments (say, `(int* p) => { }`), then the delegate is a synthesized `internal` anonymous delegate type with signature that matches the lambda or method group, and with parameter names `arg1, ..., argn` or `arg` if a single parameter;
- if `R` is `void`, then the delegate type is `System.Action<P1, ..., Pn>`;
- otherwise the delegate type is `System.Func<P1, ..., Pn, R>`.
The compiler may allow more signatures to bind to `System.Action<>` and `System.Func<>` types in the future (if `ref struct` types are allowed type arguments for instance).
_Open issue: Should the compiler bind to a matching `System.Action<>` or `System.Func<>` type regardless of arity and synthesize a delegate type otherwise? If so, should the compiler warn if the expected delegate types are missing?_
`modopt()` or `modreq()` in the method group signature are ignored in the corresponding delegate type.
If two anonymous functions or method groups in the same compilation require synthesized delegate types with the same parameter types and modifiers and the same return type and modifiers, the compiler will use the same synthesized delegate type.
If two lambda expressions or method groups in the same compilation require synthesized delegate types with the same parameter types and modifiers and the same return type and modifiers, the compiler will use the same synthesized delegate type.
### Overload resolution
Overload resolution already prefers binding to a strongly-typed delegate over `System.Delegate`, and prefers binding a lambda expression to a strongly-typed `System.Linq.Expressions.Expression<TDelegate>` over the corresponding strongly-typed delegate `TDelegate`.
[Better function member](../../spec/expressions.md#better-function-member) is updated to prefer members where none of the conversions and none of the type arguments involved inferred types from lambda expressions or method groups.
Overload resolution will be updated to prefer binding a lambda expression to `System.Linq.Expressions.Expression` over `System.Delegate`. A strongly-typed delegate will still be preferred over the weakly-typed `System.Linq.Expressions.Expression` however.
> #### Better function member
> ...
> Given an argument list `A` with a set of argument expressions `{E1, E2, ..., En}` and two applicable function members `Mp` and `Mq` with parameter types `{P1, P2, ..., Pn}` and `{Q1, Q2, ..., Qn}`, `Mp` is defined to be a ***better function member*** than `Mq` if
>
> 1. **for each argument, the implicit conversion from `Ex` to `Px` is not a _function_type_conversion_, and**
> * **`Mp` is a non-generic method or `Mp` is a generic method with type parameters `{X1, X2, ..., Xp}` and for each type parameter `Xi` the type argument is inferred from an expression or from a type other than a _function_type_, and**
> * **for at least one argument, the implicit conversion from `Ex` to `Qx` is a _function_type_conversion_, or `Mq` is a generic method with type parameters `{Y1, Y2, ..., Yq}` and for at least one type parameter `Yi` the type argument is inferred from a _function_type_, or**
> 2. for each argument, the implicit conversion from `Ex` to `Qx` is not better than the implicit conversion from `Ex` to `Px`, and for at least one argument, the conversion from `Ex` to `Px` is better than the conversion from `Ex` to `Qx`.
```csharp
static void Invoke(Func<string> f) { }
static void Invoke(Delegate d) { }
static void Invoke(Expression e) { }
[Better conversion from expression](../../spec/expressions.md#better-conversion-from-expression) is updated to prefer conversions that did not involve inferred types from lambda expressions or method groups.
static string GetString() => "";
static int GetInt() => 0;
> #### Better conversion from expression
>
> Given an implicit conversion `C1` that converts from an expression `E` to a type `T1`, and an implicit conversion `C2` that converts from an expression `E` to a type `T2`, `C1` is a ***better conversion*** than `C2` if:
> 1. **`C1` is not a _function_type_conversion_ and `C2` is a _function_type_conversion_, or**
> 2. `E` is a non-constant _interpolated\_string\_expression_, `C1` is an _implicit\_string\_handler\_conversion_, `T1` is an _applicable\_interpolated\_string\_handler\_type_, and `C2` is not an _implicit\_string\_handler\_conversion_, or
> 3. `E` does not exactly match `T2` and at least one of the following holds:
> * `E` exactly matches `T1` ([Exactly matching Expression](../../spec/expressions.md#exactly-matching-expression))
> * `T1` is a better conversion target than `T2` ([Better conversion target](../../spec/expressions.md#better-conversion-target))
Invoke(GetString); // Invoke(Func<string>) [unchanged]
Invoke(GetInt); // Invoke(Delegate) [new]
Invoke(() => ""); // Invoke(Func<string>) [unchanged]
Invoke(() => 0); // Invoke(Expression) [new]
```
_Inferring a delegate type for lambdas and method groups will result in some breaking changes in overload resolution: see [issues/4674](https://github.com/dotnet/csharplang/issues/4674)._
## Direct invocation
Lambda expressions may be invoked directly.
The compiler will generate a call to the underlying method without generating a delegate instance or synthesizing a delegate type.
Directly invoked lambda expressions do not require explicit parameter types.
```csharp
int zero = ((int x) => x)(0); // ok
int one = (x => x)(1); // ok
```
_Direct invocation will be addressed separately since the feature does not depend on other changes in this proposal: see [issues/4748](https://github.com/dotnet/csharplang/issues/4748)._
## Syntax
@ -306,15 +276,12 @@ lambda_parameter
;
```
## Open issues
## Design meetings
Should default values be supported for lambda expression parameters for completeness?
Should `System.Diagnostics.ConditionalAttribute` be disallowed on lambda expressions since there are few scenarios where a lambda expression could be used conditionally?
```csharp
([Conditional("DEBUG")] static (x, y) => Assert(x == y))(a, b); // ok?
```
Should the _function_type_ be available from the compiler API, in addition to the resulting delegate type?
Currently, the inferred delegate type uses `System.Action<>` or `System.Func<>` when parameter and return types are valid type arguments _and_ there are no more than 16 parameters, and if the expected `Action<>` or `Func<>` type is missing, an error is reported. Instead, should the compiler use `System.Action<>` or `System.Func<>` regardless of arity? And if the expected type is missing, synthesize a delegate type otherwise?
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-03-03.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-04-12.md#lambda-improvements
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-04-21.md#inferred-types-for-lambdas-and-method-groups
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-05-10.md
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-06-02.md#lambda-return-type-parsing
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-06-21.md#open-questions-for-lambda-return-types
- https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-07-12.md

View file

@ -1,4 +1,4 @@
# Parameter Null Checking
# Simplified Null Argument Checking
## Summary
This proposal provides a simplified syntax for validating method arguments are not `null` and throwing
@ -16,17 +16,17 @@ of it. The syntax can be used independent of `#nullable` directives.
## Detailed Design
### Null validation parameter syntax
The bang-bang operator, `!!`, can be positioned after a parameter name in a parameter list and this will cause the C#
compiler to emit `null` checking code for that parameter. This is referred to as `null` validation parameter
The bang operator, `!`, can be positioned after a parameter name in a parameter list and this will cause the C#
compiler to emit standard `null` checking code for that parameter. This is referred to as `null` validation parameter
syntax. For example:
``` csharp
void M(string name!!) {
void M(string name!) {
...
}
```
Will be translated into code similar to the following:
Will be translated into:
``` csharp
void M(string name) {
@ -37,12 +37,8 @@ void M(string name) {
}
```
The implementation behavior must be that if the parameter is null, it creates and throws an `ArgumentNullException` with the parameter name as a constructor argument. The implementation is free to use any strategy that achieves this. This could result in observable differences between different compliant implementations, such as whether calls to helper methods are present above the call to the method with the null-checked parameter in the exception stack trace.
We make these allowances because parameter null checks are used frequently in libraries with tight performance and size constraints. For example, to optimize code size, inlining, etc., the implementation may use helper methods to perform the null check a la the [ArgumentNullException.ThrowIfNull](https://github.com/dotnet/runtime/blob/1d08e154b942a41e72cbe044e01fff8b13c74496/src/libraries/System.Private.CoreLib/src/System/ArgumentNullException.cs#L56-L69) methods.
The generated `null` check will occur before any developer authored code in the method. When multiple parameters contain
the `!!` operator then the checks will occur in the same order as the parameters are declared.
the `!` operator then the checks will occur in the same order as the parameters are declared.
``` csharp
void M(string p1, string p2) {
@ -57,12 +53,12 @@ void M(string p1, string p2) {
```
The check will be specifically for reference equality to `null`, it does not invoke `==` or any user defined operators.
This also means the `!!` operator can only be added to parameters whose type can be tested for equality against `null`.
This also means the `!` operator can only be added to parameters whose type can be tested for equality against `null`.
This means it can't be used on a parameter whose type is known to be a value type.
``` csharp
// Error: Cannot use !! on parameters who types derive from System.ValueType
void G<T>(T arg!!) where T : struct {
// Error: Cannot use ! on parameters who types derive from System.ValueType
void G<T>(T arg!) where T : struct {
}
```
@ -77,7 +73,7 @@ For example:
``` csharp
class C {
string field = GetString();
C(string name!!): this(name) {
C(string name!): this(name) {
...
}
}
@ -105,7 +101,7 @@ parameter syntax that lacks parens.
``` csharp
void G() {
// An identity lambda which throws on a null input
Func<string, string> s = x!! => x;
Func<string, string> s = x! => x;
}
```
@ -115,7 +111,7 @@ or `async` iterators.
``` csharp
class Iterators {
IEnumerable<char> GetCharacters(string s!!) {
IEnumerable<char> GetCharacters(string s!) {
foreach (var c in s) {
yield return c;
}
@ -128,7 +124,7 @@ class Iterators {
}
```
The `!!` operator can only be used for parameter lists which have an associated method body. This
The `!` operator can only be used for parameter lists which have an associated method body. This
means it cannot be used in an `abstract` method, `interface`, `delegate` or `partial` method
definition.
@ -157,16 +153,16 @@ is instantiated as a value type the code will be evaluated as `false`. For cases
code will do a proper `is null` check.
### Intersection with Nullable Reference Types
Any parameter which has a `!!` operator applied to it's name will start with the nullable state being not `null`. This is
Any parameter which has a `!` operator applied to it's name will start with the nullable state being not `null`. This is
true even if the type of the parameter itself is potentially `null`. That can occur with an explicitly nullable type,
such as say `string?`, or with an unconstrained type parameter.
When a `!!` syntax on parameters is combined with an explicitly nullable type on the parameter then a warning will
When a `!` syntax on parameters is combined with an explicitly nullable type on the parameter then a warning will
be issued by the compiler:
``` csharp
void WarnCase<T>(
string? name!!, // Warning: combining explicit null checking with a nullable type
string? name!, // Warning: combining explicit null checking with a nullable type
T value1 // Okay
)
```
@ -178,7 +174,7 @@ None
### Constructors
The code generation for constructors means there is a small, but observable, behavior change when moving from standard
`null` validation today and the `null` validation parameter syntax (`!!`). The `null` check in standard validation
`null` validation today and the `null` validation parameter syntax (`!`). The `null` check in standard validation
occurs after both field initializers and any `base` or `this` calls. This means a developer can't necessarily migrate
100% of their `null` validation to the new syntax. Constructors at least require some inspection.
@ -187,7 +183,7 @@ logical that the `null` check run before any logic in the constructor does. Can
are discovered.
### Warning when mixing ? and !
There was a lengthy discussion on whether or not a warning should be issued when the `!!` syntax is applied to a
There was a lengthy discussion on whether or not a warning should be issued when the `!` syntax is applied to a
parameter which is explicitly typed to a nullable type. On the surface it seems like a nonsensical declaration by
the developer but there are cases where type hierarchies could force developers into such a situation.
@ -207,7 +203,7 @@ abstract class C2 : C1 {
// Assembly3
abstract class C3 : C2 {
protected override void M(object o!!) {
protected override void M(object o!) {
...
}
}
@ -236,7 +232,7 @@ following to eliminate it:
``` csharp
// Assembly3
abstract class C3 : C2 {
protected override void M(object? o!!) {
protected override void M(object? o!) {
...
}
}
@ -245,15 +241,15 @@ abstract class C3 : C2 {
At this point the author of Assembly3 has a few choices:
- They can accept / suppress the warning about `object?` and `object` mismatch.
- They can accept / suppress the warning about `object?` and `!!` mismatch.
- They can just remove the `null` validation check (delete `!!` and do explicit checking)
- They can accept / suppress the warning about `object?` and `!` mismatch.
- They can just remove the `null` validation check (delete `!` and do explicit checking)
This is a real scenario but for now the idea is to move forward with the warning. If it turns out the warning happens
more frequently than we anticipate then we can remove it later (the reverse is not true).
### Implicit property setter arguments
The `value` argument of a parameter is implicit and does not appear in any parameter list. That means it cannot be a
target of this feature. The property setter syntax could be extended to include a parameter list to allow the `!!`
target of this feature. The property setter syntax could be extended to include a parameter list to allow the `!`
operator to be applied. But that cuts against the idea of this feature making `null` validation simpler. As such the
implicit `value` argument just won't work with this feature.

View file

@ -1,150 +0,0 @@
# `params Span<T>`
## Summary
Avoid heap allocation for implicit allocation of arrays in specific scenarios with `params` arguments.
## Motivation
`params` array parameters provide a convenient way to call a method that takes an arbitrary length list of arguments.
However, using an array type for the parameter means the compiler must implicitly allocate an array on the heap at each call site.
If we extend `params` types to include the `ref struct` types `Span<T>` and `ReadOnlySpan<T>`, where values of those types cannot escape the call stack, the array at the call site may be created on the stack instead.
And if we're extending `params` to other types, we could also allow `params IEnumerable<T>` to avoid allocating and copying collections at call sites that have an `IEnumerable<T>` rather than `T[]`.
The benefits of `params ReadOnlySpan<T>` and `params Span<T>` are primarily for new APIs. Existing commonly used APIs such as `Console.WriteLine()` and `StringBuilder.AppendFormat()` already have overloads that avoid array allocations for common cases and those overloads would need to be retained for backward compatibility.
```csharp
public static class Console
{
public static void WriteLine(string value);
public static void WriteLine(string format, object arg0);
public static void WriteLine(string format, object arg0, object arg1);
public static void WriteLine(string format, object arg0, object arg1, object arg2);
public static void WriteLine(string format, params object[] arg);
}
```
## Detailed design
### Extending `params`
`params` parameters will be supported with types `Span<T>`, `ReadOnlySpan<T>`, and `IEnumerable<T>`.
A call in [_expanded form_](../spec/expressions.md#applicable-function-member) to a method with a `params T[]` or `params IEnumerable<T>` parameter will result in an array `T[]` allocated on the heap.
A call in [_expanded form_](../spec/expressions.md#applicable-function-member) to a method with a `params ReadOnlySpan<T>` or `params Span<T>` parameter will result in an array `T[]` created on the stack _if the `params` array is within limits (if any) set by the compiler_.
Otherwise the array will be allocated on the heap.
```csharp
Console.WriteLine(fmt, x, y, z); // WriteLine(string format, params ReadOnlySpan<object?> arg)
```
The compiler will report an error when compiling the method declaring the `params` parameter if the `ReadOnlySpan<T>` or `Span<T>` parameter value is returned from the method or assigned to an `out` parameter.
That ensures call-sites can create the underlying array on the stack and reuse the array across call-sites without concern for aliases.
A `params` parameter must be last parameter in the method signature.
Two overloads cannot differ by `params` modifier alone.
`params` parameters will be marked in metadata with a `System.ParamArrayAttribute` regardless of type.
### Overload resolution
Overload resolution will continue to prefer overloads that are applicable in [_normal form_](../spec/expressions.md#applicable-function-member) rather than [_expanded form_](../spec/expressions.md#applicable-function-member).
For overloads that are applicable in _expanded form_, [better function member](../spec/expressions.md#better-function-member) will be updated to prefer `params` types in a specific order:
> When performing this evaluation, if `Mp` or `Mq` is applicable in its expanded form, then `Px` or `Qx` refers to a parameter in the expanded form of the parameter list.
>
> In case the parameter type sequences `{P1, P2, ..., Pn}` and `{Q1, Q2, ..., Qn}` are equivalent (i.e. each `Pi` has an identity conversion to the corresponding `Qi`), the following tie-breaking rules are applied, in order, to determine the better function member.
>
> * If `Mp` is a non-generic method and `Mq` is a generic method, then `Mp` is better than `Mq`.
> * ...
> * **Otherwise, if both methods have `params` parameters and are applicable only in their expanded forms, and the `params` types are distinct types with equivalent element type (there is an identity conversion between element types), the more specific `params` type is the first of:**
> * **`ReadOnlySpan<T>`**
> * **`Span<T>`**
> * **`T[]`**
> * **`IEnumerable<T>`**
> * Otherwise if one member is a non-lifted operator and the other is a lifted operator, the non-lifted one is better.
> * Otherwise, neither function member is better.
### Array creation expressions
Array creation expressions that are target-typed to `ReadOnlySpan<T>` or `Span<T>` will be created on the stack _if the length of the array is a constant value within limits (if any) set by the compiler_.
Otherwise the array will be allocated on the heap.
```csharp
Span<int> s = new[] { i, j, k }; // int[] on the stack
WriteLine(fmt, new[] { x, y, z }); // object[] on the stack for WriteLine(string fmt, ReadOnlySpan<object> args);
```
### Array re-use
The compiler _may_ reuse an implicitly allocated array across multiple uses within a single thread executing a method:
- At the same call-site (within a loop) or
- At distinct call-sites if the lifetime of the spans do not overlap, and the array length is sufficient, and
- the element types are managed types that are considered identical by the runtime, or
- the element types are unmanaged types of the same size.
An implicitly allocated array may be reused regardless of whether the array was created on the stack or the heap.
### Lowering implicit allocation
For the `params` and array creation cases above that are target typed to `Span<T>` or `ReadOnlySpan<T>`, the compiler will lower the creation of spans using an efficient approach, specifically avoiding heap allocations when possible.
The exact details are still to be determined and may differ based on the target framework and runtime.
The guarantee the compiler gives is the span will be the expected size and will contain the expected items at any point in user code.
## Open issues
### Is `params Span<T>` necessary?
Is there a reason to support `params` parameters of type `Span<T>` in addition to `ReadOnlySpan<T>`? Is allowing mutation within the `params` method useful?
### Is `params IEnumerable<T>` necessary?
If the compiler allows `params ReadOnlySpan<T>`, then new APIs that require `params` could use `params ReadOnlySpan<T>` instead of `params T[]` because `T[]` is implicitly convertible to `ReadOnlySpan<T>`. And existing APIs could add a `params ReadOnlySpan<T>` overload where the existing `params T[]` simply delegates to the new overload.
There is no conversion from `IEnumerable<T>` to `ReadOnlySpan<T>` however, so allowing `params IEnumerable<T>` is essentially asking APIs to provide two overloads for `params` methods: `params ReadOnlySpan<T>` and `params IEnumerable<T>`.
Are scenarios for `params IEnumerable<T>` sufficiently compelling to justify that?
### Array limits
The compiler may use heuristics to determine when to fallback to heap allocation for the underlying data for spans.
If heuristics are necessary, experimentation should establish the limits we agree on.
### Lowering approach
We need to determine the particular approach used to lower `params` and array creation expressions to avoid heap allocation.
For instance, one potential approach to represent a `Span<T>` of constant length `N` is to synthesize a `struct` with `N` fields of type `T`
where the layout and alignment of the fields matches the alignment of elements in `T[]`, and create the `Span<T>` from a `ref` to the first field of the `struct`.
With that approach, `Console.WriteLine(fmt, x, y, z);` would be emitted as:
```csharp
[StructLayout(LayoutKind.Sequential)]
internal struct __ValueArray3<T> { public T Item1, Item2, Item3; };
var values = new __ValueArray3<object>() { Item1 = x, Item2 = y, Item3 = z };
var span = MemoryMarshal.CreateSpan(ref values.Item1, 3);
Console.WriteLine(fmt, (ReadOnlySpan<object>)span); // WriteLine(string format, params ReadOnlySpan<object?> arg)
```
Alternative approaches may require runtime support.
### Explicit `stackalloc`
Should we allow explicit stack allocation of arrays of managed types with `stackalloc` as well?
```csharp
public static ImmutableArray<TResult> Select<TSource, TResult>(this ImmutableArray<TSource> source, Func<TSource, TResult> map)
{
int n = source.Length;
Span<TResult> result = n <= 16 ? stackalloc TResult[n] : new TResult[n];
for (int i = 0; i < n; i++)
result[i] = map(source[i]);
return ImmutableArray.Create(result); // requires ImmutableArray.Create<T>([DoesNotEscape] ReadOnlySpan<T> items)
}
```
This would require runtime support for stack allocation of arrays of non-constant length and any type, and GC tracking of the elements.
Direct runtime support for stack allocation of arrays of managed types might be useful for lowering implicit allocation as well.
The GC does not currently track the lifetime of a `stackalloc` array so if the contents of the array have a shorter lifetime than the method, the compiler will need to zero the contents of the array so the lifetime of elements matches expectations.
### Opting out
Should we allow opt-ing out of _implicit allocation_ on the call stack?
Perhaps an attribute that can be applied to a method, type, or assembly.
## Related proposals
- https://github.com/dotnet/csharplang/issues/1757
- https://github.com/dotnet/csharplang/blob/main/proposals/format.md#extending-params

View file

@ -4392,7 +4392,7 @@ GroupJoin(orders, c => c.CustomerID, o => o.CustomerID,
(c, co) => new { c, co }).
Select(x => new { x, n = x.co.Count() }).
Where(y => y.n >= 10).
Select(y => new { y.x.c.Name, OrderCount = y.n })
Select(y => new { y.x.c.Name, OrderCount = y.n)
```
where `x` and `y` are compiler generated identifiers that are otherwise invisible and inaccessible.