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

时间:2013-10-07 05:37:35

标签: c#

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

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

使用相邻的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,即使某些内部{ {1}}调用throw。但是,有一个模糊的问题:除了最外层的框架之外,所有嵌套的“使用”框架可能抛出的嵌套异常都将丢失。有关here的更多信息。

5 个答案:

答案 0 :(得分:16)

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

  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());
        // ... 
    }
    

答案 1 :(得分:6)

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

答案 2 :(得分:6)

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

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

答案 3 :(得分:1)

您的上一个建议隐藏了abc应明确处理的事实。这就是为什么它很难看。

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

答案 4 :(得分: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();
}
相关问题