-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add doc about ref-struct lifetime variance #9418
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
Open
agocke
wants to merge
3
commits into
dotnet:main
Choose a base branch
from
agocke:lifetime-variance
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
|
|
||
| # Lifetime variance | ||
|
|
||
| This doc is meant as an addendum to https://github.com/dotnet/csharplang/blob/main/proposals/csharp-11.0/low-level-struct-improvements.md. | ||
|
|
||
| The primary purpose is to more formally ground the variance of lifetime parameters and the corresponding correctness rules. | ||
|
|
||
| In the above doc we treat ref variables as their own class of variables. However, by using a struct syntax we can unify the treatment of ref structs and by-ref variables. | ||
|
|
||
| First, we will define a primitive type to represent a by-ref variable: | ||
|
|
||
| ```csharp | ||
| ref struct ByRef<$a, T> | ||
| { | ||
| public T Value { get; set; } | ||
| } | ||
| ``` | ||
|
|
||
| This is the syntactic equivalent of `ref<$a> T`. One problem with this formulation is that C# currently defines all struct type parameters as invariant. However, this is not necessary. Structs are merely aggregations of their field data. That is, simple use of any struct type parameter `T` as a field type should not carry any additional variance restrictions. We do not currently have a syntax in C# to represent bivariance, but we can invent one: `inout`. | ||
|
|
||
| With this syntax we can alter the definition to | ||
|
|
||
| ```csharp | ||
| ref struct ByRef<inout $a, T> | ||
| { | ||
| public T Value { get; set; } | ||
| } | ||
| ``` | ||
|
|
||
| Note that `T` still remains invariant. This is for two reasons. First, because the intent of this doc is only to change struct variance for lifetimes, not for non-lifetime type parameters. Second, `ByRef` is a very special type of struct: it contains a pointer. Variance restrictions are due to a combination of two factors: mutability and aliasing. When a variable may be aliased but only allows reading, it is covariant. When it may be aliased but only allows writing, it is invariant. When it may be aliased and it allows both reading and writing, it is invariant. When it does not allow aliasing, it is bivariant, regardless of access. Because `T` exists behind a read/write pointer, it is invariant. | ||
|
|
||
| However, `$a` is not behind the pointer -- it is a property of the `ByRef`, not the target. This means it remains `inout`. | ||
|
|
||
| We can also extend this definition to handle `ref readonly`: | ||
|
|
||
| ```csharp | ||
| ref struct ByRefReadonly<inout $a, out T> | ||
| { | ||
| public T Value { get; } | ||
| } | ||
| ``` | ||
|
|
||
| Note that, once again, the lifetime is behind the pointer and therefore the variance of the lifetime is unchanged. The only change is to the `T` variable -- `out` instead of invariant. | ||
|
|
||
| Having defined the base case of `ByRef`, we can extend the lifetime variance rules to all ref structs. Ref structs don't differ from structs except in allow ref structs as fields, so the only change to variance is in their ref-struct fields. Specifically, after inference every ref struct will be of the form | ||
|
|
||
| ```csharp | ||
| ref struct S<$a, $a2, ... $an, T1, T2, ... Tn> { ... } | ||
| ``` | ||
|
|
||
| All lifetime variables should also be used by at least one of the fields. We can then proceed to assign variance annotations based on the usage of the variable with the most restrictive variance. That is, for each lifetime variable `$an`, its lifetime variance is: | ||
|
|
||
| 1. `inout`, if every field that references it does so in an `inout` context | ||
| 2. `out`, if every field that references it does so in either an `inout` or `out` context | ||
| 3. `in`, if every field that references it does so in either an `inout` or `in` context | ||
| 3. invariant, if any field references it in an invariant context, or appears in both `out` and `in` contexts | ||
|
|
||
| Here are some examples of each of these situations: | ||
|
|
||
| ```csharp | ||
| ref struct S<inout $a> | ||
| { | ||
| public ByRef<$a, int> Field; | ||
| } | ||
| ``` | ||
|
|
||
| In the above, `$a` only appears as a lifetime argument to `ByRef<inout $a, T>`. Because that is an `inout` context, the `$a` variable in `S` is also in an `inout` context. | ||
|
|
||
| ```csharp | ||
| ref struct S1<inout $a, T> | ||
| { | ||
| public ByRef<$a, T> Field; | ||
| } | ||
| ref struct S2<$a, $b> | ||
| { | ||
| public ByRef<$a, S1<$b, int>> Field; | ||
| } | ||
| ``` | ||
|
|
||
| In this case, `S2.$a` is invariant. This is because it appears in the _second_ type argument of `ByRef<inout $a, T>`, which is invariant. | ||
|
|
||
| ```csharp | ||
| ref struct S1<inout $a, T> | ||
| { | ||
| public ByRef<$a, T> Field; | ||
| } | ||
| ref struct S2<out $a> | ||
| { | ||
| public Task<S1<$a, int>> Field; | ||
| } | ||
| ``` | ||
|
|
||
| Here `$a` appears in a type argument to `Task<out T>`, meaning `$a` must also be restricted to `out`. | ||
|
|
||
| ```csharp | ||
| ref struct S1<inout $a, T> | ||
| { | ||
| public ByRef<$a, T> Field; | ||
| } | ||
| ref struct S2<in $a> | ||
| { | ||
| public Action<S1<$a, int>> Field; | ||
| } | ||
| ``` | ||
|
|
||
| Here `$a` appears in a type argument to `Action<in T>`, meaning `$a` must also be restricted to `in`. | ||
|
|
||
|
|
||
| Note that this formulation differs slightly from the existing one, in that there is no special lifetime variable for `$this`. There are two reasons: | ||
|
|
||
| 1. Self-referential variables are harder to analyze | ||
| 2. There is no specific corresponding field, which also makes things more complicated | ||
|
|
||
| Overall, it's simpler to not have a special `$this` variable. Instead, we'll move the handling to instance methods themselves. Rather than hiding the receiver, we'll instead write it out explicitily: | ||
|
|
||
| ```csharp | ||
| ref struct S<...> | ||
| { | ||
| void M() { ... } | ||
| } | ||
| ``` | ||
|
|
||
| becomes | ||
|
|
||
| ```csharp | ||
| ref struct S<...> | ||
| { | ||
| static void M(ref S<...> this) { ... } | ||
| } | ||
| ``` | ||
|
|
||
| It's now clear that the receiver type is a ref-variable. Therefore, we will rewrite it like we have for other refs, using the `ByRef` syntax: | ||
|
|
||
| ```csharp | ||
| ref struct S<...> | ||
| { | ||
| static void M<$this>(ByRef<$this, S<...>> this) { ... } | ||
| } | ||
| ``` | ||
|
|
||
| On the type-checking side, this is very simple -- there are no special cases. On the inference side, since every instance method is invoked with an obvious receiver, we can always automatically infer the lifetime variable using the receiver lifetime. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.