Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Note
This article is a feature specification. The specification serves as the design document for the feature. It includes proposed specification changes, along with information needed during the design and development of the feature. These articles are published until the proposed spec changes are finalized and incorporated in the current ECMA specification.
There may be some discrepancies between the feature specification and the completed implementation. Those differences are captured in the pertinent language design meeting (LDM) notes.
You can learn more about the process for adopting feature speclets into the C# language standard in the article on the specifications.
Champion issue: https://github.com/dotnet/csharplang/issues/1331
Summary
Unify behavior between iterators and async methods. Specifically:
- Allow
ref
/ref struct
locals andunsafe
blocks in iterators and async methods provided they are used in code segments without anyyield
orawait
. - Warn about
yield
insidelock
.
Motivation
It is not necessary to disallow ref
/ref struct
locals and unsafe
blocks in async/iterator methods
if they are not used across yield
or await
, because they do not need to be hoisted.
async void M()
{
await ...;
ref int x = ...; // error previously, proposed to be allowed
x.ToString();
await ...;
// x.ToString(); // still error
}
Breaking changes
There are no breaking changes in the language specification, but there is one breaking change in the Roslyn implementation (due to a spec violation).
Roslyn violates the part of the spec which states that iterators introduce a safe context (§13.3.1).
For example, if there is an unsafe class
with an iterator method which contains a local function
then that local function inherits the unsafe context from the class,
although it should have been in a safe context per the spec due to the iterator method.
In fact, the whole iterator method inherited the unsafe context in Roslyn,
it was just disallowed to use any unsafe constructs in iterators.
In LangVersion >= 13
, iterators will correctly introduce a safe context
because we want to allow unsafe constructs in iterators.
unsafe class C // unsafe context
{
System.Collections.Generic.IEnumerable<int> M() // an iterator
{
yield return 1;
local();
async void local()
{
int* p = null; // allowed in C# 12; error in C# 13 (breaking change)
await Task.Yield(); // error in C# 12, allowed in C# 13
}
}
}
Note:
- The break can be worked around simply by adding the
unsafe
modifier to the local function. - This does not affect lambdas as they "inherit" the "iterator context" and therefore it was impossible to use unsafe constructs inside them.
Detailed design
The following changes are tied to LangVersion, i.e., C# 12 and lower will continue to disallow
ref-like locals and unsafe
blocks in async methods and iterators,
and C# 13 will lift these restrictions as described below.
However, spec clarifications which match the existing Roslyn implementation should hold across all LangVersions.
A block that contains one or more
yield
statements (§13.15) is called an iterator block, even if thoseyield
statements are contained only indirectly in nested blocks (excluding nested lambdas and local functions).[...]
It is a compile-time error for an iterator block to contain an unsafe context (§23.2). An iterator block always defines a safe context, even when its declaration is nested in an unsafe context.The iterator block used to implement an iterator (§15.14) always defines a safe context, even when the iterator declaration is nested in an unsafe context.
From this spec it also follows:
- If an iterator declaration is marked with the
unsafe
modifier, the signature is in an unsafe scope but the iterator block used to implement that iterator still defines a safe scope. - The
set
accessor of an iterator property or indexer (i.e., itsget
accessor is implemented via an iterator block) "inherits" its safe/unsafe scope from the declaration. - This does not affect partial declarations without implementation as they are only signatures and cannot have an iterator body.
Note that in C# 12 it is an error to have an iterator method marked with the unsafe
modifier,
but that is allowed in C# 13 due to the spec change.
For example:
using System.Collections.Generic;
using System.Threading.Tasks;
class A : System.Attribute { }
unsafe partial class C1
{ // unsafe context
[/* unsafe context */ A]
IEnumerable<int> M1(
/* unsafe context */ int*[] x)
{ // safe context (this is the iterator block implementing the iterator)
yield return 1;
}
IEnumerable<int> M2()
{ // safe context (this is the iterator block implementing the iterator)
unsafe
{ // unsafe context
{ // unsafe context (this is *not* the block implementing the iterator)
yield return 1; // error: `yield return` in unsafe context
}
}
}
[/* unsafe context */ A]
unsafe IEnumerable<int> M3(
/* unsafe context */ int*[] x)
{ // safe context
yield return 1;
}
[/* unsafe context */ A]
IEnumerable<int> this[
/* unsafe context */ int*[] x]
{ // unsafe context
get
{ // safe context
yield return 1;
}
set { /* unsafe context */ }
}
[/* unsafe context */ A]
unsafe IEnumerable<int> this[
/* unsafe context */ long*[] x]
{ // unsafe context (the iterator declaration is unsafe)
get
{ // safe context
yield return 1;
}
set { /* unsafe context */ }
}
IEnumerable<int> M4()
{
yield return 1;
var lam1 = async () =>
{ // safe context
// spec violation: in Roslyn, this is an unsafe context in LangVersion 12 and lower
await Task.Yield(); // error in C# 12, allowed in C# 13
int* p = null; // error in both C# 12 and C# 13 (unsafe in iterator)
};
unsafe
{
var lam2 = () =>
{ // unsafe context, lambda cannot be an iterator
yield return 1; // error: yield cannot be used in lambda
};
}
async void local()
{ // safe context
// spec violation: in Roslyn, this is an unsafe context in LangVersion 12 and lower
await Task.Yield(); // error in C# 12, allowed in C# 13
int* p = null; // allowed in C# 12, error in C# 13 (breaking change in Roslyn)
}
local();
}
public partial IEnumerable<int> M5() // unsafe context (inherits from parent)
{ // safe context
yield return 1;
}
}
partial class C1
{
public partial IEnumerable<int> M5(); // safe context (inherits from parent)
}
class C2
{ // safe context
[/* unsafe context */ A]
unsafe IEnumerable<int> M(
/* unsafe context */ int*[] x)
{ // safe context
yield return 1;
}
unsafe IEnumerable<int> this[
/* unsafe context */ int*[] x]
{ // unsafe context
get
{ // safe context
yield return 1;
}
set { /* unsafe context */ }
}
}
§13.6.2.4 Ref local variable declarations:
It is a compile-time error to declare a ref local variable, or a variable of aIt is a compile-time error to declare and use (even implicitly in compiler-synthesized code) a ref local variable, or a variable of aref struct
type, within a method declared with the method_modifierasync
, or within an iterator (§15.14).ref struct
type acrossawait
expressions oryield return
statements. More precisely, the error is driven by the following mechanism: after anawait
expression (§12.9.8) or ayield return
statement (§13.15), all ref local variables and variables of aref struct
type in scope are considered definitely unassigned (§9.4).
Note that this error is not downgraded to a warning in unsafe
contexts like some other ref safety errors.
That is because these ref-like locals cannot be manipulated in unsafe
contexts without relying on implementation details of how the state machine rewrite works,
hence this error falls outside the boundaries of what we want to downgrade to warnings in unsafe
contexts.
When a function member is implemented using an iterator block, it is a compile-time error for the formal parameter list of the function member to specify any
in
,ref readonly
,out
, orref
parameters, or an parameter of aref struct
type or a pointer type.
No change in the spec is needed to allow unsafe
blocks which do not contain await
s in async methods,
because the spec has never disallowed unsafe
blocks in async methods.
However, the spec should have always disallowed await
inside unsafe
blocks
(it had already disallowed yield
in unsafe
in §13.3.1 as cited above),
so we propose the following change to the spec:
§15.15.1 Async Functions > General:
It is a compile-time error for the formal parameter list of an async function to specify any
in
,out
, orref
parameters, or any parameter of aref struct
type.It is a compile-time error for an unsafe context (§23.2) to contain an
await
expression (§12.9.8) or ayield return
statement (§13.15).
§23.6.5 The address-of operator:
A compile-time error will be reported for taking an address of a local or a parameter in an iterator.
Currently, taking an address of a local or a parameter in an async method is a warning in C# 12 warning wave.
Note that more constructs can work thanks to ref
allowed inside segments without await
and yield
in async/iterator methods
even though no spec change is needed specifically for them as it all falls out from the aforementioned spec changes:
using System.Threading.Tasks;
ref struct R
{
public ref int Current { get { ... }};
public bool MoveNext() => false;
public void Dispose() { }
}
class C
{
public R GetEnumerator() => new R();
async void M()
{
await Task.Yield();
using (new R()) { } // allowed under this proposal
foreach (var x in new C()) { } // allowed under this proposal
foreach (ref int x in new C()) { } // allowed under this proposal
lock (new System.Threading.Lock()) { } // allowed under this proposal
await Task.Yield();
}
}
Alternatives
ref
/ref struct
locals could be allowed only in blocks (§13.3.1) which do not containawait
/yield
:// error always since `x` is declared/used both before and after `await` { ref int x = ...; await Task.Yield(); x.ToString(); } // allowed as proposed (`x` does not need to be hoisted as it is not used after `await`) // but alternatively could be an error (`await` in the same block) { ref int x = ...; x.ToString(); await Task.Yield(); }
yield return
insidelock
could be an error (likeawait
insidelock
is) or a warning-wave warning, but that would be a breaking change: https://github.com/dotnet/roslyn/issues/72443. Note that the newLock
-object-basedlock
reports compile-time errors foryield return
s in its body, because suchlock
statement is equivalent to ausing
on aref struct
which disallowsyield return
s in its body.Variables inside async or iterator methods should not be "fixed" but rather "moveable" if they need to be hoisted to fields of the state machine (similarly to captured variables). Note that this is a pre-existing bug in the spec independent of the rest of the proposal because
unsafe
blocks insideasync
methods were always allowed. There is currently a warning for this in C# 12 warning wave and making it an error would be a breaking change.§23.4 Fixed and moveable variables:
In precise terms, a fixed variable is one of the following:
- A variable resulting from a simple_name (§12.8.4) that refers to a local variable, value parameter, or parameter array, unless the variable is captured by an anonymous function (§12.19.6.2) or a local function (§13.6.4) or the variable needs to be hoisted as part of an async (§15.15) or an iterator (§15.14) method.
- [...]
Currently, we have an existing warning in C# 12 warning wave for address-of in async methods and a proposed error for address-of in iterators reported for LangVersion 13+ (does not need to be reported in earlier versions because it was impossible to use unsafe code in iterators). We could relax both of these to apply only to variables that are actually hoisted, not all locals and parameters.
It could be possible to use
fixed
to get the address of a hoisted or captured variable although the fact that those are fields is an implementation detail so in other implementations it might not be possible to usefixed
on them. Note that we only propose to consider also hoisted variables as "moveable", but captured variables were already "moveable" andfixed
was not allowed for them.
We could allow
await
/yield
insideunsafe
except insidefixed
statements (compiler cannot pin variables across method boundaries). That might result in some unexpected behavior, for example aroundstackalloc
as described in the nested bullet point below. Otherwise, hoisting pointers is supported even today in some scenarios (there is an example below related to pointers as arguments), so there should be no other limitations in allowing this.- We could disallow the unsafe variant of
stackalloc
in async/iterator methods, because the stack-allocated buffer does not live acrossawait
/yield
statements. It does not feel necessary because unsafe code by design does not prevent "use after free". Note that we could also allow unsafestackalloc
provided it is not used acrossawait
/yield
, but that might be difficult to analyze (the resulting pointer can be passed around in any pointer variable). Or we could require it beingfixed
in async/iterator methods. That would discourage using it acrossawait
/yield
but would not match the semantics offixed
because thestackalloc
expression is not a moveable value. (Note that it would not be impossible to use thestackalloc
result acrossawait
/yield
similarly as you can save anyfixed
pointer today into another pointer variable and use it outside thefixed
block.)
- We could disallow the unsafe variant of
Iterator and async methods could be allowed to have pointer parameters. They would need to be hoisted, but that should not be a problem as hoisting pointers is supported even today, for example:
unsafe public void* M(void* p) { var d = () => p; return d(); }
The proposal currently keeps (and extends/clarifies) the pre-existing spec that iterator methods begin a safe context even if they are in an unsafe context. For example, an iterator method is not an unsafe context even if it is defined in a class which has the
unsafe
modifier. Alternatively, we could make iterators "inherit" theunsafe
modifier like other methods do.- Advantage: removes complexity from the spec and implementation.
- Advantage: aligns iterators with async methods (one of the motivations of the feature).
- Disadvantage: iterators inside unsafe classes could not contain
yield return
statements, such iterators would have to be defined in a separate partial class declaration without theunsafe
modifier. - Disadvantage: this would be a breaking change in LangVersion=13 (iterators in unsafe classes are allowed in C# 12).
Instead of an iterator defining a safe context for the body only, the whole signature could be a safe context. That is inconsistent with the rest of the language in that bodies normally do not affect declarations but here a declaration would be either safe or unsafe depending on whether the body is an iterator or not. It would be also a breaking change in LangVersion=13 as in C# 12 iterator signatures are unsafe (they can contain pointer array parameters, for example).
Applying the
unsafe
modifier to an iterator:- Could affect the body as well as the signature. Such iterators would not be very useful though
because their unsafe bodies could not contain
yield return
s, they could have onlyyield break
s. - Could be an error in
LangVersion >= 13
as it is inLangVersion <= 12
because it is not very useful to have an unsafe iterator member as it only allows one to have pointer array parameters or unsafe setters without additional unsafe block. But normal pointer arguments could be allowed in the future.
- Could affect the body as well as the signature. Such iterators would not be very useful though
because their unsafe bodies could not contain
Roslyn breaking change:
- We could preserve the current behavior (and even modify the spec to match it) for example by introducing the safe context in the iterator method but then reverting to the unsafe context in the local function.
- Or we could break all LangVersions, not just 13 and newer.
- It is also possible to more drastically simplify the rules by making iterators
inherit unsafe context like all other methods do. Discussed above.
Could be done across all LangVersions or just for
LangVersion >= 13
.
Design meetings
- 2024-06-03: post-implementation review of the speclet
C# feature specifications