Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clarify some aspects of primary constructor parameters capturing #6855

Merged
merged 3 commits into from
Jan 9, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion proposals/primary-constructors.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Primary constructor parameters in class/struct declarations can be declared `ref

All instance member initializers in the class body will become assignments in the generated constructor.

If a primary constructor parameter is referenced from within an instance member, it is captured into the state of the enclosing type, so that it remains accessible after the termination of the constructor. A likely implementation strategy is via a private field using a mangled name.
If a primary constructor parameter is referenced from within an instance member, and the reference is not inside of a `nameof` argument, it is captured into the state of the enclosing type, so that it remains accessible after the termination of the constructor. A likely implementation strategy is via a private field using a mangled name.

Capturing is not allowed for `ref`, `in` and `out` parameters. This is similar to a limitation for capturing in lambdas.

Expand Down Expand Up @@ -187,6 +187,43 @@ Records produce a warning if a primary constructor parameter isn't read within t
- for an `in` parameter, if the parameter is not read within any instance initializers or base initializer.
- for a `ref` parameter, if the parameter is not read or written to within any instance initializers or base initializer.

### Identical simple names and type names

There is a special language rule for scenarios often referred to as "Color Color" scenarios - [Identical simple names and type names](https://github.com/dotnet/csharpstandard/blob/draft-v7/standard/expressions.md#11762-identical-simple-names-and-type-names).

>In a member access of the form `E.I`, if `E` is a single identifier, and if the meaning of `E` as a *simple_name* ([§11.7.4](expressions.md#1174-simple-names)) is a constant, field, property, local variable, or parameter with the same type as the meaning of `E` as a *type_name* ([§7.8.1](basic-concepts.md#781-general)), then both possible meanings of `E` are permitted. The member lookup of `E.I` is never ambiguous, since `I` shall necessarily be a member of the type `E` in both cases. In other words, the rule simply permits access to the static members and nested types of `E` where a compile-time error would otherwise have occurred.

With respect to primary constructors, the rule affects whether an identifier within an instance member should be treated as a type reference, or as a primary constructor parameter reference, which, in turn, captures the parameter into the the state of the enclosing type. Even though "the member lookup of `E.I` is never ambiguous", when lookup yields a member group, in some cases it is impossible to determine whether a member access refers to a static member or an instance member without fully resolving (binding) the member access. At the same time, capturing a primary constructor parameter changes properties of enclosing type in a way that affects semantic analysis. For example, the type might become unmanaged and fail certain constraints because of that.
There are even scenarios for which binding can succeed either way, depending on whether the parameter is considered captured or not. For example:
``` C#
struct S1(Color Color)
{
public void Test()
{
Color.M1(this);
}
}

class Color
{
public void M1<T>(T x, int y = 0)
{
System.Console.WriteLine("instance");
}

public static void M1<T>(T x) where T : unmanaged
{
System.Console.WriteLine("static");
}
}
```

If we treat receiver ```Color``` as a value, we capture the parameter and 'S1' becomes managed. Then the static method becomes inapplicable due to the constraint and we would call instance method. However, if we treat the receiver as a type, we don't capture the parameter and 'S1' remains unmanaged, then both methods are applicable, but the static method is "better" because it doesn't have an optional parameter. Neither choice leads to an error, but each would result in distinct behavior.

Given this, compiler will produce an ambiguity error for a member access `E.I` when all the following conditions are met:
- Member lookup of `E.I` yields a member group containing instance and static members at the same time. Extension methods applicable to the receiver type are treated as instance methods for the purpose of this check.
- If `E` is treated as a simple name, rather than a type name, it would refer to a primary constructor parameter and would capture the parameter into the state of the enclosing type.
jjonescz marked this conversation as resolved.
Show resolved Hide resolved

## Primary constructors on records

With this proposal, records no longer need to separately specify a primary constructor mechanism. Instead, record (class and struct) declarations that have primary constructors would follow the general rules, with these simple additions:
Expand Down