我已经用ActionOnAsyncDispose结构实现了IAsyncDisposable,如下所示。我的理解是,当它处于异步使用语句时,编译器不会将其装箱:
ActionOnDisposeAsync x = ...;
await using (x) {
...
}
正确吗?到目前为止还不错。我的问题是,当我这样配置等待时:
ActionOnDisposeAsync x = ...;
await using (x.ConfigureAwait()) {
...
}
x会被装箱吗?如果我把ConfigureAwait放在一个方法中呢,Caf((:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static public ConfiguredAsyncDisposable Caf(this ActionOnDisposeAsync disposable)
=> disposable.ConfigureAwait(false);
ActionOnDisposeAsync x = ...;
await using (x.Caf()) {
...
}
那样的话我能避免拳击吗?我找不到关于我的using变量究竟需要实现什么以获得ConfigureAwait效果的文档。似乎也没有任何公共方式来构建ConfiguredAsyncDisposable。
以下是ActionOnDisposeSync:
public readonly struct ActionOnDisposeAsync : IAsyncDisposable, IEquatable<ActionOnDisposeAsync>
{
public ActionOnDisposeAsync(Func<Task> actionAsync)
{
this.ActionAsync = actionAsync;
}
public ActionOnDisposeAsync( Action actionSync)
{
this.ActionAsync = () => { actionSync(); return Task.CompletedTask; };
}
private Func<Task> ActionAsync { get; }
public async ValueTask DisposeAsync()
{
if (this.ActionAsync != null) {
await this.ActionAsync();
}
}
...
}
是的,struct
一次性用品上的ConfigureAwait
会导致装箱。以下是这种行为的实验演示:
MyDisposableStruct value = new();
const int loops = 1000;
var mem0 = GC.GetTotalAllocatedBytes(true);
for (int i = 0; i < loops; i++)
{
await using (value.ConfigureAwait(false)) { }
}
var mem1 = GC.GetTotalAllocatedBytes(true);
Console.WriteLine($"Allocated: {(mem1 - mem0) / loops:#,0} bytes per 'await using'");
其中MyDisposableStruct
是这个简单结构:
readonly struct MyDisposableStruct : IAsyncDisposable
{
public ValueTask DisposeAsync() => default;
}
输出:
Allocated: 24 bytes per 'await using'
现场演示。
为了防止装箱的发生,您必须创建一个自定义的类似ConfiguredAsyncDisposable
的结构,它是专门为您的结构定制的。以下是如何做到这一点:
readonly struct MyConfiguredAsyncDisposable
{
private readonly MyDisposableStruct _parent;
private readonly bool _continueOnCapturedContext;
public MyConfiguredAsyncDisposable(MyDisposableStruct parent,
bool continueOnCapturedContext)
{
_parent = parent;
_continueOnCapturedContext = continueOnCapturedContext;
}
public ConfiguredValueTaskAwaitable DisposeAsync()
=> _parent.DisposeAsync().ConfigureAwait(_continueOnCapturedContext);
}
static MyConfiguredAsyncDisposable ConfigureAwait(
this MyDisposableStruct source, bool continueOnCapturedContext)
{
return new MyConfiguredAsyncDisposable(source, continueOnCapturedContext);
}
现在运行与以前相同的实验,而不对代码进行任何更改,不会导致分配。输出为:
Allocated: 0 bytes per 'await using'
现场演示。
如果编译器能够检测到实际类型(结构(,则不需要装箱。如果它只通过接口工作,则在处理时会工作。我用ILSpy之类的东西检查编译后的代码,你会发现dispose语句是在类上完成的(接口也是如此(,还是在值类型(/struct(上完成的。
我不确定在处理异步时使用结构是否会给你带来很大好处,也不确定它是否值得付出努力,但你应该在做出决定之前衡量一下。