有没有一种优雅的方法来避免最终嵌套?

Jul*_*ARD 5 c#

我开发的程序的功能之一涉及更改外部世界(例如修改文件),然后启动一个进程,等待其完成,然后撤消最初所做的更改。

这些更改可能会以不可预见的方式失败,在这种情况下,我们会中止一切并仅撤消已完成的操作。有一些更改需要进行。这会产生以下类型的代码:

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 的更改会影响 [...] 的进行,但之后应始终将其恢复。

dbc*_*dbc 6

如果您总是要撤消更改,我建议更改方法的签名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)

这种模式的优点是:

  1. 通过返回,IDisposable您可以鼓励下游开发人员通过声明正确撤消更改using

  2. 保证以与应用程序相反的顺序撤消更改。

  3. 如何撤消每个更改的知识封装在更改方法本身内。

  4. 如果某些内部“撤消”方法抛出异常,外部“撤消”方法仍然会被调用(如果所有撤消方法都添加到某个简单的 lambda 列表中,则可能无法保证)。

演示小提琴在这里