30

我注意到using最近在我的代码中嵌套语句的级别有所增加。原因可能是因为我使用的pattern越来越多,经常为orasync/await增加至少一个。usingCancellationTokenSourceCancellationTokenRegistration

那么,如何减少 , 的嵌套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调用抛出。然而,有一个有点模糊的问题:除了最外层的之外,所有可能由处理嵌套的“使用”帧引发的嵌套异常都将丢失。更多关于这个here

4

5 回答 5

16

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

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

让我们解决这些问题:

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());
    // ... 
}
于 2013-10-07T07:04:05.040 回答
6

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

于 2013-10-07T06:03:49.370 回答
6

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

  • 它清楚地表明了您对这些对象的意图
  • 你不必乱用 try-finally 块。它很容易出错,并且您的代码变得不那么可读。
  • 您可以稍后使用语句重构嵌入的语句(将它们提取到方法中)
  • 您不会通过创建自己的逻辑来混淆其他程序员,包括新的抽象层
于 2013-10-07T06:48:19.297 回答
1

Your last suggestion hides the fact that a, b and c should be disposed explicitly. That`s why it's ugly.

As mentioned in my comment, if you'd use clean code principles you wouldn't run into these problems (usually).

于 2013-10-07T05:46:14.000 回答
1

另一种选择是简单地使用一个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();
}
于 2013-10-07T06:17:42.373 回答