缩小的问题:
我需要实现可重用的功能,初始化临时文件,运行用户提供的一些自定义逻辑,然后删除文件。我可以通过将用户的逻辑作为委托的静态实用程序方法,或通过实现IDisposable
以在Dispose
期间执行清理的类来执行此操作。这两种方法的用法如下:
// Delegate-parameterized method approach:
TempFile.Using(filePath =>
{
// use temporary file here
});
// IDisposable implementation approach:
using (var tempFile = new TempFile())
{
// use temporary file here
}
每种方法的优点和缺点是什么?我应该使用哪一个?
原始问题:
我正在开发一些通用功能,需要一个固定的“初始化 - 自定义逻辑 - 清理”序列。清理必须作为用于执行操作的相同构造的一部分来执行;我不想赋予用户调用Cleanup
或Close
方法的责任。
例如,我可能希望提供自动创建和删除临时文件的功能。实现此目的的最简单方法是通过一个采用Action<T>
委托的方法:
public static void UsingTempFile(Action<string> action)
{
// initialization
string tempFilePath = Path.GetTempFileName();
try
{
action(tempFilePath);
}
finally
{
// clean-up
File.Delete(tempFilePath);
}
}
这可以这样使用:
UsingTempFile(filePath =>
{
File.WriteAllText(filePath, "Hello world");
string text = File.ReadAllText(filePath);
});
但是,这种方法通常需要我实现四个方法重载来支持返回的结果和匿名函数:
public static void UsingTempFile(Action<string> action) { /* ... */ }
public static TResult UsingTempFile<TResult>(Func<string, TResult> func) { /* ... */ }
public static async Task UsingTempFile(Func<string, Task> asyncAction) { /* ... */ }
public static async Task<TResult> UsingTempFile<TResult>(Func<string, Task<TResult>> asyncFunc) { /* ... */ }
前三个可以实现为调用最后一个重载的简单包装器。但是,在公共API中,它们仍然需要进行文档化和单元测试,从而使我的代码库变得非常混乱。
另一种方法是设计一个可实例化的类,它实现IDisposable
来表示操作。该类将在其构造函数中执行初始化,并在其Dispose
方法中进行清理。然后可以这样消费:
using (var tempFile = new TempFile())
{
File.WriteAllText(tempFile.FilePath, "Hello world");
string text = File.ReadAllText(tempFile.FilePath);
}
这种方法的优点是C#编译器自动处理我的所有四种情况 - 用户可以在return
块中指定await
和using
个关键字。
但是,我经常需要能够从我的清理逻辑中抛出异常 - 如果IOException
被File.Delete
抛出,如果临时文件仍在上面的例子中使用的话。 Dispose Pattern个州的MSDN文档:
X AVOID 从
Dispose(bool)
内抛出异常,除非在包含进程已损坏的严重情况下(泄漏,不一致的共享状态等)。用户希望拨打
Dispose
不会引发异常。如果
Dispose
可能引发异常,则最终块清除逻辑将不会执行。要解决这个问题,用户需要在try块中包含对Dispose
(在finally块中!)内的每次调用,这会导致非常复杂的清理处理程序。
related question更强有力地说明了从finally
块中抛出异常的缺点:
- 抛出第一个异常
- 由于第一个异常而执行finally块
- finally块调用Dispose()方法
- Dispose()方法抛出第二个异常
醇>[...]您丢失了信息,因为.NET无法用第二个异常替换第一个异常。因此,调用堆栈上某处的catch块将永远不会出现第一个异常。然而,人们通常对第一个例外更感兴趣,因为这通常会为事情开始出错提供更好的线索。
这个论点具有优点 - 如果用户的自定义逻辑抛出异常,我不希望它被清理中抛出的任何异常隐藏(并丢失)。有一些解决方案,例如IDisposable
Dispose
对象(吞下所有FileStream
例外),但这会给用户带来更多责任。另一方面,.NET Framework类库本身似乎忽略了这个规则 - Dispose
如果无法将缓冲区的内容刷新到磁盘,则可以从其IDisposable
方法中抛出异常。
这两种方法中的哪一种(委托参数化方法vs public static void UsingTempFile(Action<string> action)
{
// initialization
string tempFilePath = Path.GetTempFileName();
bool isSuccess = false;
try
{
// main logic
action(tempFilePath);
isSuccess = true;
}
finally
{
try
{
// clean-up
File.Delete(tempFilePath);
}
catch
{
// only propagate exceptions from clean-up if there were
// no unhandled exceptions from the main logic
if (isSuccess)
throw;
}
}
}
)对于这些通用实现是值得推荐的?除了上面提到的那些之外还有什么影响吗?
更新:这是委托参数化方法如何阻止吞噬主逻辑异常的示例:
{{1}}