我开发的程序的功能之一涉及更改外部世界(例如修改文件),然后启动一个进程,等待其完成,然后撤消最初所做的更改。
这些更改可能会以不可预见的方式失败,在这种情况下,我们会中止一切并仅撤消已完成的操作。有一些更改需要进行。这会产生以下类型的代码:
DoChange1();
try {
DoChange2();
try {
DoChange3();
try {
[...]
}
finally {
UndoChange3();
}
finally {
UndoChange2();
}
finally {
UndoChange1();
}
Run Code Online (Sandbox Code Playgroud)
这太丑了。这嵌套在每个更改中,并且撤消更改的代码与应用该更改的代码以相反的顺序相距甚远 - 非常类似于将组操作应用于使用 完成的语言中的结构func(structure, lambda),而不是 LINQ 风格。
即使以函数调用的方式组织,我也更愿意能够做这样的事情:
DoChange1();
finally_onwards {
UndoChange1();
}
DoChange2();
finally_onwards {
UndoChange2();
}
DoChange3();
finally_onwards {
UndoChange3();
}
[...]
Run Code Online (Sandbox Code Playgroud)
一旦遇到finally,它就会变得活跃,并在作用域退出时以遇到的相反顺序调用。
遗憾的是,这不是 C# 构造 - 在正确撤消更改的同时,防止 try-finally 嵌套到无穷大的好解决方案是什么?
请注意,应始终撤消更改。X 的更改会影响 [...] 的进行,但之后应始终将其恢复。
如果您总是要撤消更改,我建议更改方法的签名DoChangeX()以返回IDisposable撤消更改的方法。
例如,如果您介绍以下内容:
public sealed class UndoDisposable : IDisposable
{
Action? undo;
public UndoDisposable(Action undo) => this.undo = undo ?? throw new ArgumentNullException(nameof(undo));
public void Dispose() => Interlocked.Exchange(ref this.undo, null)?.Invoke();
}
Run Code Online (Sandbox Code Playgroud)
您可以DoChangeX()按如下方式重写您的方法:
public IDisposable DoChange1()
{
// The code to do change 1
return new UndoDisposable(() => UndoChange1());
}
void UndoChange1()
{
// The code to undo change 1
}
Run Code Online (Sandbox Code Playgroud)
那么你的主要代码将如下所示:
using (DoChange1())
using (DoChange2())
using (DoChange3())
{
Console.WriteLine("All changes active.");
}
Run Code Online (Sandbox Code Playgroud)
这种模式的优点是:
通过返回,IDisposable您可以鼓励下游开发人员通过声明正确撤消更改using。
保证以与应用程序相反的顺序撤消更改。
如何撤消每个更改的知识封装在更改方法本身内。
如果某些内部“撤消”方法抛出异常,外部“撤消”方法仍然会被调用(如果所有撤消方法都添加到某个简单的 lambda 列表中,则可能无法保证)。
演示小提琴在这里。