处理 C# 中的嵌套“using”语句

本文关键字:using 语句 嵌套 处理 | 更新日期: 2023-09-27 18:34:22

我注意到嵌套using语句的级别最近在我的代码中有所增加。原因可能是因为我使用越来越多的async/await模式,这通常会为CancellationTokenSourceCancellationTokenRegistration添加至少一个using

那么,如何减少using的嵌套,让代码看起来不像圣诞树呢?以前在SO上也问过类似的问题,我想总结一下我从答案中学到的东西。

使用相邻using而不带缩进。一个假的例子:

using (var a = new FileStream())
using (var b = new MemoryStream())
using (var c = new CancellationTokenSource())
{
    // ... 
}

这可能有效,但通常using之间有一些代码(例如,创建另一个对象可能还为时过早(:

// ... 
using (var a = new FileStream())
{
    // ... 
    using (var b = new MemoryStream())
    {
        // ... 
        using (var c = new CancellationTokenSource())
        {
            // ... 
        }
    }
}

将相同类型(或投射到IDisposable(的对象组合成单个using,例如:

// ... 
FileStream a = null;
MemoryStream b = null;
CancellationTokenSource c = null;
// ...
using (IDisposable a1 = (a = new FileStream()), 
    b1 = (b = new MemoryStream()), 
    c1 = (c = new CancellationTokenSource()))
{
    // ... 
}

这具有与上述相同的限制,而且更冗长且可读性较差,IMO。

将该方法重构为几个方法。

据我了解,这是一种首选方式然而,我很好奇,为什么以下行为会被认为是一种不好的做法?

public class DisposableList : List<IDisposable>, IDisposable
{
    public void Dispose()
    {
        base.ForEach((a) => a.Dispose());
        base.Clear();
    }
}
// ...
using (var disposables = new DisposableList())
{
    var a = new FileStream();
    disposables.Add(a);
    // ...
    var b = new MemoryStream();
    disposables.Add(b);
    // ...
    var c = new CancellationTokenSource();
    disposables.Add(c);
    // ... 
}

[更新] 注释中有很多有效的观点,嵌套using语句可以确保Dispose每个对象上被调用,即使抛出一些内部Dispose调用也是如此。但是,有一个有点模糊的问题:除了最外部的异常之外,所有可能通过处理嵌套的"using"帧引发的嵌套异常都将丢失。更多关于这个 这里.

处理 C# 中的嵌套“using”语句

在单一方法中,第一个选项将是我的选择。但是,在某些情况下,DisposableList很有用。特别是,如果您有许多需要处理的一次性字段(在这种情况下,您不能使用 using (。给出的实现是一个良好的开端,但它有一些问题(在 Alexei 的评论中指出(:

  1. 要求您记住将项目添加到列表中。(虽然你也可以说你必须记住使用using
  2. 如果其中一个释放
  3. 方法抛出,则中止处置过程,使其余项目保持未释放状态。

让我们解决这些问题:

public class DisposableList : List<IDisposable>, IDisposable
{
    public void Dispose()
    {
        if (this.Count > 0)
        {
            List<Exception> exceptions = new List<Exception>();
            foreach(var disposable in this)
            {
                try
                {
                    disposable.Dispose();
                }
                catch (Exception e)
                {
                    exceptions.Add(e);
                }
            }
            base.Clear();
            if (exceptions.Count > 0)
                throw new AggregateException(exceptions);
        }
    }
    public T Add<T>(Func<T> factory) where T : IDisposable
    {
        var item = factory();
        base.Add(item);
        return item;
    }
}

现在,我们捕获Dispose调用中的任何异常,并在遍历所有项目后抛出新AggregateException。我添加了一个帮助程序Add方法,允许更简单的用法:

using (var disposables = new DisposableList())
{
    var file = disposables.Add(() => File.Create("test"));
    // ...
    var memory = disposables.Add(() => new MemoryStream());
    // ...
    var cts = disposables.Add(() => new CancellationTokenSource());
    // ... 
}

你应该总是参考你的假例子。如果这是不可能的,就像你提到的,那么你很可能可以将内部内容重构为一个单独的方法。如果这也没有意义,你应该坚持你的第二个例子。其他所有内容似乎都不太可读,不太明显,也不太常见的代码。

我会坚持使用块。为什么?

  • 它清楚地显示了你对这些对象的意图
  • 您不必乱用最终尝试块。它容易出错,您的代码可读性降低。
  • 您可以稍后重构嵌入的 using 语句(将它们提取到方法中(
  • 您不会通过创建自己的逻辑来混淆您的程序员同事,包括新的抽象层

您的最后一个建议隐藏了abc应该显式处置的事实。这就是为什么它很丑。

正如我在评论中提到的,如果你使用干净的代码原则,你就不会遇到这些问题(通常(。

另一种选择是简单地使用try-finally块。这可能看起来有点冗长,但它确实减少了不必要的嵌套。

FileStream a = null;
MemoryStream b = null;
CancellationTokenSource c = null;
try
{
   a = new FileStream();
   // ... 
   b = new MemoryStream();
   // ... 
   c = new CancellationTokenSource();
}
finally 
{
   if (a != null) a.Dispose();
   if (b != null) b.Dispose();
   if (c != null) c.Dispose();
}