处理 C# 中的嵌套“using”语句
本文关键字:using 语句 嵌套 处理 | 更新日期: 2023-09-27 18:34:22
我注意到嵌套using
语句的级别最近在我的代码中有所增加。原因可能是因为我使用越来越多的async/await
模式,这通常会为CancellationTokenSource
或CancellationTokenRegistration
添加至少一个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"帧引发的嵌套异常都将丢失。更多关于这个 这里.
在单一方法中,第一个选项将是我的选择。但是,在某些情况下,DisposableList
很有用。特别是,如果您有许多需要处理的一次性字段(在这种情况下,您不能使用 using
(。给出的实现是一个良好的开端,但它有一些问题(在 Alexei 的评论中指出(:
- 要求您记住将项目添加到列表中。(虽然你也可以说你必须记住使用
using
。
如果其中一个释放 - 方法抛出,则中止处置过程,使其余项目保持未释放状态。
让我们解决这些问题:
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 语句(将它们提取到方法中(
- 您不会通过创建自己的逻辑来混淆您的程序员同事,包括新的抽象层
您的最后一个建议隐藏了a
、b
和c
应该显式处置的事实。这就是为什么它很丑。
正如我在评论中提到的,如果你使用干净的代码原则,你就不会遇到这些问题(通常(。
另一种选择是简单地使用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();
}