如果 Fluent 方法链中发生抛出,则带有 Fluent API 的 using 语句将不会调用 Dispose

using statement with fluent api will not call dispose if throw occurs in fluent method chain

提问人:David L 提问时间:8/3/2022 最后编辑:David L 更新时间:8/9/2022 访问量:144

问:

将 using 语句与可能引发的 Fluent API 结合使用时,降低的代码将永远不会正确调用 dispose。

如果我有以下公开流畅接口的类:

public class Wrapper : IDisposable
{   
    private bool _isAdded;
    
    public Wrapper Add()
    {
        _isAdded = true;
        return this;
    }

    public void Dispose() => Console.WriteLine("dispose called");

    public Wrapper ThrowIfAdded() => _isAdded ? throw new Exception() : this;
}

我用以下方式称呼它:

using var willNotDispose = new Wrapper().Add().ThrowIfAdded();

降低的代码将导致在 Fluent 方法链完成后发生 Dispose 调用。

Wrapper willNotDispose = new Wrapper().Add().ThrowIfAdded();
try
{
}
finally
{
    if (willNotDispose != null)
    {
        ((IDisposable)willNotDispose).Dispose();
    }
}

或者,如果调用是在初始声明之外完成的,.ThrowIfAdded()using

using var willDispose = new Wrapper().Add();
willDispose.ThrowIfAdded();

降低的代码将按预期生成。

Wrapper willDispose = new Wrapper().Add();
try
{
    willDispose.ThrowIfAdded();
}
finally
{
    if (willDispose != null)
    {
        ((IDisposable)willDispose).Dispose();
    }
}

虽然我理解为什么会发生这种情况,但这是不可取的。有没有办法将前者初始化强制编译为后者?理想情况下,它将是编译器提示的属性或形式,这将导致:

Wrapper willDispose = default;
try
{
    willDispose = new Wrapper().Add().ThrowIfAdded();
}
finally
{
    if (willDispose != null)
    {
        ((IDisposable)willDispose).Dispose();
    }
}

我本来希望原始示例首先编译成。

C# Fluent Using-语句

评论

0赞 madreflection 8/3/2022
如果没有某种后处理,我看不到“强制前者初始化编译为后者”的方法。假设这是真的,让问题无法回答,你愿意接受什么样的解决方案?
0赞 madreflection 8/3/2022
显然,你可以在链条之后做一些事情,而这些都是在try/catch范围内。看起来必须在那里做一些事情,然后一种解决方案是尽量减少它是什么并使其保持一致,就像做.如果通过对这些操作进行排队将执行推迟到该调用,则将按预期方式进行处置。但是,这是否符合您的需求是另一回事。wrapper.Complete();AddThrowIfAdded
0赞 David L 8/3/2022
@madreflection完全公平的。我在我的问题中添加了一个示例,即预期和首选的结果是什么。当然,您可以手动编写长 try/finally 表单,但这是不可取的。
0赞 madreflection 8/3/2022
如果你打算把初始化放在 try/catch 中,那么你说的是语言的改变,或者说是后处理。AOP 框架也许能够为您做到这一点。
0赞 David L 8/3/2022
@madreflection那是我的恐惧。如果是这种情况,它可能是我在语言上打开的一个项目。这是误导性的。

答:

0赞 David L 8/9/2022 #1

正如注释中指出的,预先存在的指南是,当构造函数中引发异常时,应显式处理该异常并清理资源。

这延伸到以下分析CA2000

当仅受一个异常处理程序保护的构造函数是 嵌套在 using 语句的 采集部分,在 外部构造函数可以生成由嵌套的 构造函数永远不会关闭。在以下示例中,故障 StreamReader 构造函数可能导致 FileStream 对象永远不会 正在关闭。在这种情况下,CA2000 会标记违反规则的行为。

using (StreamReader sr = new StreamReader(new FileStream("C:/myfile.txt", FileMode.Create)))
{ ... }

虽然抛出异常的 Fluent API 不是显式抛出异常的构造函数或嵌套构造函数,但应将其视为相同,因为对象将在 try/finally 块之外创建和变异。

因此,任何可以引发的方法都必须先调用 dispose,然后才能允许异常传播。

public class Wrapper : IDisposable
{
    private bool _isDisposed;
    private bool _isAdded;

    public Wrapper Add()
    {
        _isAdded = true;
        return this;
    }

    public void Dispose()
    {
        if (_isDisposed)
        {
            return;
        }

        _isDisposed = true;
        Console.WriteLine("dispose called");
    }

    public Wrapper ThrowIfAdded()
    {
        if (_isAdded)
        {
            Dispose();
            throw new Exception();
        }

        return this;
    }
}

这正确地确保了在被调用的情况下,将在投掷之前处理掉。.Added().ThrowIfAdded()

如果未调用,则实例将按预期在块末尾释放。.Added()