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 structlocals andunsafeblocks in iterators and async methods provided they are used in code segments without anyyieldorawait. - Warn about
yieldinsidelock.
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
unsafemodifier 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
yieldstatements (§13.15) is called an iterator block, even if thoseyieldstatements 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
unsafemodifier, the signature is in an unsafe scope but the iterator block used to implement that iterator still defines a safe scope. - The
setaccessor of an iterator property or indexer (i.e., itsgetaccessor 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 structtype, within a method declared with the method_modifierasync, or within an iterator (§15.14).ref structtype acrossawaitexpressions oryield returnstatements. More precisely, the error is driven by the following mechanism: after anawaitexpression (§12.9.8) or ayield returnstatement (§13.15), all ref local variables and variables of aref structtype 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, orrefparameters, or an parameter of aref structtype or a pointer type.
No change in the spec is needed to allow unsafe blocks which do not contain awaits 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, orrefparameters, or any parameter of aref structtype.It is a compile-time error for an unsafe context (§23.2) to contain an
awaitexpression (§12.9.8) or ayield returnstatement (§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 structlocals 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 returninsidelockcould be an error (likeawaitinsidelockis) or a warning-wave warning, but that would be a breaking change: https://github.com/dotnet/roslyn/issues/72443. Note that the newLock-object-basedlockreports compile-time errors foryield returns in its body, because suchlockstatement is equivalent to ausingon aref structwhich disallowsyield returns 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
unsafeblocks insideasyncmethods 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
fixedto 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 usefixedon them. Note that we only propose to consider also hoisted variables as "moveable", but captured variables were already "moveable" andfixedwas not allowed for them.
We could allow
await/yieldinsideunsafeexcept insidefixedstatements (compiler cannot pin variables across method boundaries). That might result in some unexpected behavior, for example aroundstackallocas 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
stackallocin async/iterator methods, because the stack-allocated buffer does not live acrossawait/yieldstatements. It does not feel necessary because unsafe code by design does not prevent "use after free". Note that we could also allow unsafestackallocprovided 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 beingfixedin async/iterator methods. That would discourage using it acrossawait/yieldbut would not match the semantics offixedbecause thestackallocexpression is not a moveable value. (Note that it would not be impossible to use thestackallocresult acrossawait/yieldsimilarly as you can save anyfixedpointer today into another pointer variable and use it outside thefixedblock.)
- 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
unsafemodifier. Alternatively, we could make iterators "inherit" theunsafemodifier 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 returnstatements, such iterators would have to be defined in a separate partial class declaration without theunsafemodifier. - 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
unsafemodifier 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 returns, they could have onlyyield breaks. - Could be an error in
LangVersion >= 13as it is inLangVersion <= 12because 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