临时更改变量的值

5
在我目前正在处理的 API 实现中,需要在执行某些任务之前暂时重复更改某些变量,并在完成任务后将它们改回到以前的状态。目前的代码如下:
var _oldValue = _variable;
_variable = tempValue;
try
{
  doIt();
}
finally
{
  _variable = oldValue;
}

频繁这样做很麻烦,难看,难以维护,并且将实际算法淹没在许多仅是实现工件的杂乱代码中。

在C++中,我会创建一个类,在构造期间将旧值存储在某个地方,并在其析构函数中恢复它:

{
  temp_value tmp(variable_, temp_val);
  do_it();
}

当我试图在C#中做类似的事情时,我失败了,因为显然C#无法在类中存储对其他对象的引用。那么在C#中我该怎么去除这些混乱呢?
P.S.:如有需要请添加其他标签,我没有想到任何合适的。

编写一个包装器静态方法,例如DoItWithRestore(),在任何地方调用它而不是DoIt(),这样这个混乱的代码就只存在于一个地方,并确保您对其进行注释,以便未来的代码读者了解意图和目的。 - Sanjeevakumar Hiremath
你有没有可能找到另一个API来完成相同的任务?听起来这个API非常脆弱。 - vhallac
不能将 doIt() 改为带参数的形式吗? - Jonas Elfström
1
你能更详细地解释一下背景吗?这些变量是全局的吗?像“doit”这样的函数是否都封装在某个类中?我很难理解整个大局。如果你只是想在之后将它们切换回来,为什么“doit”需要影响其作用域外的事物呢? - Jamie Treworgy
@Dysaster,@Jonas和@jamietre:这更像是一个框架而不是 API。您编写一些代码来加载到其中,并通过框架调用以处理数据 - 这是一个经典的插件架构。其中需要使用此类框架的示例是当 doIt() 调用插件的代码时。插件编写者希望执行的某些操作仅在特定上下文中允许,即在调用某些插件函数集时。因此,在调用插件之前,框架会设置上下文,并从插件调用的函数检查是否设置了该上下文。尽管还有其他应用程序可以使用此功能。 - sbi
3个回答

7
虽然我同意Eric Lippert的理想解决方案,但有些情况下我们需要临时更改变量状态并执行一些操作。例如,在SharePoint对象模型中有多个这样的要求,因此无法重新设计代码以避免它。
以下是可用于临时更改值并使用using语句还原的代码。对于此类非释放非托管资源目的,使用using引起争议,因此请根据自己的判断考虑是否使用此方法:
用法示例:
using(TemporaryChange(true, myValue, v => myValue = v))
{
 // code to run while "myValue" is changed to "true"
}

类:

class TemporaryChange<V> : IDisposable
{
    private V original;
    private Action<V> setValue;

    internal TemporaryChange(V value, V currentValue, Action<V> setValue)
    {
        this.setValue = setValue;
        this.original = currentValue;
        this.setValue(value);
    }

    void IDisposable.Dispose()
    {
        this.setValue(this.original);
    }
}

当然,这很像C++的RAII,所以它对我有一定的吸引力。:) 然而,我认为必须拼写出像赋值这样简单的东西是相当麻烦的。不过,这可能是在C#中最好的选择了。谢谢! - sbi

6
为什么不创建一个自动完成此操作的方法,然后将Lambda表达式传递给它呢?
private void SaveGlobalsAndDoSomething(Action doit)
{
    var _oldValue = _variable;
    _variable = tempValue;
    try
    {
        doit();
    }
    finally
    {
        _variable = _oldValue;
    }
}

并且使用它:

SaveGlobalsAndDoSomething(() => { DoSomething(); });

回复评论的编辑:

doit 有时返回一个值并不是问题。我们没有将 DoSomething 传递给该方法,而是将 { DoSomething(); } 传递给该方法。 因此,您可以轻松编写:

int returnValue;
SaveGlobalsAndDoSomething(() => { returnValue = DoSomething(); });

唉,这不是很好看,但可能只是因为我期望的不同。我想这确实可以工作。考虑到我可以通过 ref 传递变量给函数,我甚至可以将要更改的变量传递给函数,这样我就不必为每个这样的变量复制它了。 - sbi
糟糕。我刚刚发现doIt()有时会返回一些东西。我想我可以使用泛型来指定返回类型吗?但其他人没有这样做,而且我有点怀疑我能否像在C++中那样使用返回泛型函数来返回void?那就意味着至少需要两个不同的函数...而且我本以为我可以将其作为通用函数放入实用程序命名空间中。 - sbi
啊,谢谢!虽然它们是我最喜欢的 C# 特性之一,但似乎我还没有完全掌握 Lambda 的全部威力... - sbi

5

由于您正在考虑采取各种可怕的手段来解决这个问题,这表明您首先就不应该处在这种情况下。如果您的代码依赖于修改和撤消状态,那么您的设计存在问题。解决真正的设计问题,而不是试图想出一种聪明的方法来继续使用不良架构。

当我处于这种情况时,我会克隆我的状态。假设您正在执行以下操作:

class Frobber
{
    State state;
    ...
    void M()
    {
         ...
         try
         {
             oldstate = state;
             state = newstate;
             this.DoIt();
         }
         finally
         {
             state = oldstate;
         }
    }

请改为这样做:
class Frobber
{
    State state;
    ...
    void M()
    {
         ...
         Frobber newFrobber = new Frobber(newstate);
         newFrobber.DoIt();
         ...

不要改变已有的变量,而是创建一个新的变量。处理完毕后丢弃新变量即可。旧变量无需改回,因为它从未改变过。


Eric,我很重视你的见解,但请先阅读我的评论,我在问题中解释了为什么这是必要的。即使你是对的,事情也可以重新设计成你建议的方式(我目前看不到),但这是一个相当大的系统(单元测试需要超过一个小时),已经在多个客户端中使用,并且需要平稳的升级路径。我不能随意重新设计,因为那些客户端有需要编译针对此API的代码,并且重构必须逐步完成,以避免一次性破坏太多。 - sbi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接