1

I am curious to why async methods returning an IAsyncEnumerable compile down to a state machine, which is defined as a class instead of a struct as it usually is. See the following example:

public async IAsyncEnumerable<int> MethodOne() 
{ 
    await Task.Delay(10);
    yield return 0;
}

// Compiled version

[CompilerGenerated]
private sealed class <MethodOne>d__0 : IAsyncEnumerable<int>, IAsyncEnumerator<int>,
    IAsyncDisposable, IValueTaskSource<bool>, IValueTaskSource, IAsyncStateMachine
{
    // Omitted for brevity 
}

Sharplab.io

public async Task<int> MethodTwo() 
{ 
    await Task.Delay(10);
        
    return 0;
}

// Compiled version

[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct <MethodTwo>d__0 : IAsyncStateMachine
{
    // Omitted for brevity 
}

Sharplab.io

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Twenty
  • 5,234
  • 4
  • 32
  • 67
  • 1
    Who knows what microsoft did in their last frameworks, but they wanted to exclude heap allocations for async operations. maybe it is the reason. – Ivan Khorin Feb 21 '21 at 00:34
  • Probably because the async statemachine not always continues on the same thread? So bennefits of a struct over a class, are not used here. – Jeroen van Langen Feb 21 '21 at 00:40

2 Answers2

3

The state machine itself implements IAsyncEnumerable<T>, and is newed up and returned as the IAsyncEnumerable<T> instance. If it was a struct type, it would be immediately boxed by the conversion to an interface type anyway.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • One could say the same thing for `IAsyncStateMachine` for the async method example. I don't see how this guess (and let's be clear: you _are_ just guessing here, when it comes to answering "why did Microsoft do it this way?") actually explains the difference. – Peter Duniho Feb 21 '21 at 01:00
  • 1
    @PeterDuniho: The state machine for `async Task` methods is only boxed when necessary; for methods that can complete synchronously, it is never boxed and a completed `Task` instance is returned. Since the state machine for `async IAsyncEnumerable` methods *is* the returned instance, it would *always* have to be boxed. And I guess quite often. ;) – Stephen Cleary Feb 21 '21 at 01:46
  • That's a reasonable point to support your speculation, but it belongs in the answer, not the comments. – Peter Duniho Feb 21 '21 at 02:32
0

You can compare it with the state machine generated for an IEnumerable<T>, which is also a class:

public IEnumerable<int> MethodThree() 
{ 
    yield return 0;
}

// Compiled version

[CompilerGenerated]
private sealed class <MethodThree>d__0 : IEnumerable<int>, IEnumerable,
    IEnumerator<int>, IDisposable, IEnumerator
{
    // Omitted for brevity 
}

[IteratorStateMachine(typeof(<MethodThree>d__0))]
public IEnumerable<int> MethodThree()
{
    return new <MethodThree>d__0(-2);
}

Sharplab.io

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Based on the other answer, since `IEnumerable` / `IAsynEnumberable` doesn't start until `MoveNext` is called, a struct would always need to be boxed anyway. If a `Task` completes synchronously, the box allocation can sometimes be avoided. – Jeremy Lakeman Feb 22 '21 at 05:22
  • @JeremyLakeman yeap, indeed. Btw the first step at starting an (async) enumerable is to call its `Get(Async)Enumerator` method, which instantiates a state machine. The `MoveNext(Async)` comes next, and sets the state machine in motion. After realizing that, the second part of my answer seems now wrong. – Theodor Zoulias Feb 22 '21 at 05:33