【问题标题】:Dealing with nested "using" statements in C#处理 C# 中的嵌套“使用”语句
【发布时间】:2013-10-13 14:34:11
【问题描述】:

我注意到最近在我的代码中嵌套的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);
    // ... 
}

[更新] 在 cmets 中有很多有效点,即嵌套 using 语句可确保在每个对象上调用 Dispose,即使某些内部 Dispose 调用抛出.然而,有一个有点模糊的问题:除了最外层的之外,所有可能由处理嵌套的“使用”帧引发的嵌套异常都将丢失。更多关于这个here

【问题讨论】:

  • 您可以尝试方法提取等技术。我的意思是尝试将这个特定的方法分成小的独立部分并将它们移动到方法中。这样您就可以将这多个 using 块移动到不同的方法中。
  • 通常,如果您使用多个,比如说,2 个嵌套的using 语句,那么您的方法无论如何都有点过于复杂,因此无论如何都需要重构。如果您或多或少地遵循“干净代码”原则,您通常不会最终嵌套太多 using 语句。 @MuctadirDinar:同样的想法!
  • 我通常发现 3 是我将嵌套的最多的,而且我发现正常的嵌套缩进非常可读,并且比您提出的任何其他替代方案都更清晰。也许在 4 或 5 之后它可能会变得有点棘手,但即便如此,当我阅读代码时,我宁愿有明显的代码,而不是非标准模式来研究。现在的显示器一般都很宽,所以我不会太担心水平空间。
  • 是的。考虑在极端情况下进行重构。无论如何,代码通常都需要 cmets,而重构(拆分为方法)是注释代码并使其更具可读性的理想方式。
  • 3 显然很糟糕:它需要特别努力才能在编写代码时不要忘记处理对象,并产生难以阅读的异常代码。旁注:如问题 3 所示,如果早先的 Dispose 抛出异常,则变体可能永远不会处理某些对象。

标签: c#


【解决方案1】:

您的最后一个建议隐藏了 abc 应该明确处置的事实。这就是它丑陋的原因。

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

【讨论】:

  • 我倾向于同意,但请详细说明这与我的第二个代码片段(嵌套 using 每个都带有花括号)有何不同?您的意思是分离使处置更加明确吗?
  • 尽量不要使用技巧来避免嵌套。问问自己:“它可读吗?我的同事会不问就理解代码吗?”如果在这两种情况下的答案都是“是”,那么一切都很好。如果您倾向于评论您的代码,请将其拆分。这种技术在大多数情况下避免了过多的嵌套级别。
【解决方案2】:

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

【讨论】:

    【解决方案3】:

    另一种选择是简单地使用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();
    }
    

    【讨论】:

    • 你应该使用using 我不推荐这个。这是避免嵌套的技巧,即个人品味。不要使用技巧。实际上嵌套本身并没有什么不好。
    • +0。您的建议与问题中的选项 3 存在相同的问题 - 它需要特别努力才能在编写代码时不要忘记处理对象并产生更难阅读的异常代码(此外,您的建议鼓励复制粘贴,这会增加更多风险错误)。如果早期的Dispose 抛出异常,这两个版本都可能永远不会处理某些对象。
    • @alzaimar 为什么“应该”使用using 声明?它只是为方便起见而提供的 try-finally 块的简短硬形式。使用任何一种都是个人喜好,我并不认为嵌套是一件坏事,我只是在回应 OP 这样做的愿望。
    • 这对我来说是最好的选择。因为它清楚地显示了使用了什么,需要处理什么。此外 Using 语句只是 try & finally 块的捷径。它只是比 try & finally 提高了编码的可读性。所以对于他来说,什么是容易阅读的取决于程序员,这对每个程序员来说都是不同的。
    • -1:如果先前对Dispose 的调用之一引发异常,则您的方法无法清理资源。你已经破坏了using 声明的全部目的。
    【解决方案4】:

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

    • 它清楚地表明了您对这些对象的意图
    • 您不必乱用 try-finally 块。它容易出错,而且您的代码可读性较差。
    • 您可以稍后重构嵌入的 using 语句(将它们提取到方法中)
    • 您不会通过创建自己的逻辑来混淆其他程序员,包括新的抽象层

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

      • 感谢关于聚合异常和一段简洁的代码的重要观点,尤其是因式分解。
      • 有趣的是,聚合异常与嵌套using 的标准行为不同,其中some exceptions may apparently get lost
      • @Noseratio 我并不感到惊讶。这是spec 中定义的行为。这通常很难调试,因为您看到的异常不是问题的原因。我认为上述行为实际上会使调试此类问题变得更加容易。
      • @Noseratio 实际上是我的最后一句话。 AggregateException 仍然可以隐藏问题的根源。由于我们无法访问导致我们进入列表的Dispose 的异常,因此您需要决定是否值得吞下Dispose 异常。我同意 Marc Gravell 的声明 here,通常原始异常是有趣的异常。无论如何,我至少会尝试记录它们。
      • Here 是我试图避免吞下或替换内部异常的尝试,@mikez。
      猜你喜欢
      • 2014-04-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多