C# 使用IDisposable检查约束——疯狂还是天才?

C# 使用IDisposable检查约束——疯狂还是天才?,c#,exception,idisposable,C#,Exception,Idisposable,我在今天的代码库中遇到了一个模式,它最初看起来非常聪明,后来让我发疯,现在我想知道是否有一种方法可以拯救聪明的部分,同时最小化疯狂 我们有一组实现IContractObject的对象,还有一个类不变量检查器,如下所示: internal class InvariantChecker : IDisposable { private IContractObject obj; public InvariantChecker(IContractObject obj) {

我在今天的代码库中遇到了一个模式,它最初看起来非常聪明,后来让我发疯,现在我想知道是否有一种方法可以拯救聪明的部分,同时最小化疯狂

我们有一组实现
IContractObject
的对象,还有一个类
不变量检查器
,如下所示:

internal class InvariantChecker : IDisposable
{
    private IContractObject obj;

    public InvariantChecker(IContractObject obj)
    {
        this.obj = obj;
    }

    public void Dispose()
    {
        if (!obj.CheckInvariants())
        {
            throw new ContractViolatedException();
        }
    }
}

internal class Foo : IContractObject
{
    private int DoWork()
    {
        using (new InvariantChecker(this))
        {
            // do some stuff
        }
        // when the Dispose() method is called here, we'll throw if the work we
        // did invalidated our state somehow
    }
}
这用于提供相对轻松的状态一致性运行时验证。我没有写这篇文章,但一开始它似乎是一个很酷的想法

但是,如果
Foo.DoWork
引发异常,则会出现问题。当抛出异常时,我们很可能处于不一致的状态,这意味着
不变量检查器也会抛出异常,从而隐藏原始异常。当异常向上传播到调用堆栈时,这种情况可能会发生多次,每一帧都有一个
不变量检查器
,将异常隐藏在下面的帧中。为了诊断问题,我必须禁用
不变量检查器中的抛出,只有这样我才能看到原始异常


这显然很可怕。然而,是否有任何方法可以在不产生可怕的异常隐藏行为的情况下拯救原始想法的聪明性

不变量检查器
类添加一个属性,该属性允许您抑制检查/抛出

internal class InvariantChecker : IDisposable
{
    private IContractObject obj;

    public InvariantChecker(IContractObject obj)
    {
        this.obj = obj;
    }

    public bool Suppress { get; set; }

    public void Dispose()
    {
        if (!this.Suppress)
        {
            if (!obj.CheckInvariants())
            {
                throw new ContractViolatedException();
            }
        }
    }
}

internal class Foo : IContractObject
{
    private int DoWork()
    {
        using (var checker = new InvariantChecker(this))
        {
            try
            {
                // do some stuff
            }
            catch
            {
                checker.Suppress = true;
                throw;
            }
        }
    }
}

这是对使用模式的可怕滥用。使用模式是为了处理非托管资源,而不是像这样的“聪明”技巧。我建议只写直截了当的代码。

我不喜欢用这种方式来重载
的含义。为什么不使用一个采用委托类型的静态方法呢?所以你会写:

InvariantChecker.Check(this, () =>
{
    // do some stuff
});
或者更好的是,将其作为一种扩展方法:

this.CheckInvariantActions(() =>
{
    // do some stuff
});
public static class InvariantEnforcer {
  public static void EnforceInvariants(this IContractObject obj) {
    if (!obj.CheckInvariants()) {
      throw new ContractViolatedException();
    }
  }
}
(请注意,“this”部分是为了让C#编译器查找适用于
this
的扩展方法而需要的)如果需要,这还允许您使用“normal”方法来实现操作,并使用方法组转换为其创建委托。如果有时希望从主体返回,您可能还希望允许它返回一个值

现在,CheckInvariantActions可以使用以下内容:

action();
if (!target.CheckInvariants())
{
    throw new ContractViolatedException();
}
我还建议,
CheckInvariants
可能应该直接抛出异常,而不是仅仅返回
bool
——这样,异常可以提供有关违反了哪个不变量的信息。

如果您真的想这样做:

internal class InvariantChecker : IDisposable
{
    private IContractObject obj;

    public InvariantChecker(IContractObject obj)
    {
        this.obj = obj;
    }

    public void Dispose()
    {
        if (Marshal.GetExceptionCode() != 0xCCCCCCCC && obj.CheckInvariants())
        {
            throw new ContractViolatedException();
        }
    }
}

如果您当前的问题是获取原始异常-转到调试->异常,并检查所有CLR异常的“抛出”。当抛出异常时,它将中断,因此您将首先看到它。如果从VS的角度来看“非您的代码”引发异常,您可能还需要关闭工具->选项->调试->“仅我的代码”选项。

要做到这一点,需要一种干净的方法来确定调用Dispose时异常是否处于挂起状态。Microsoft应提供一种标准化方法,以便在当前try finally块退出时随时查明哪些异常(如果有)将挂起,或者Microsoft应支持一个扩展的Dispose接口(可能是DisposeEx,它将继承Dispose),该接口将接受一个挂起的异常参数。

而不是:

using (new InvariantChecker(this)) {
  // do some stuff
}
只需这样做(假设你没有从
返回,做一些事情
):

如果您需要从
回来做一些事情,我相信一些是合适的:

DoSomeStuff(); // returns void
this.EnforceInvariants();

...

var result = DoSomeStuff(); // returns non-void
this.EnforceInvariants();
return result;
它更简单,你不会有以前的问题

您只需要一个简单的扩展方法:

this.CheckInvariantActions(() =>
{
    // do some stuff
});
public static class InvariantEnforcer {
  public static void EnforceInvariants(this IContractObject obj) {
    if (!obj.CheckInvariants()) {
      throw new ContractViolatedException();
    }
  }
}

这是恐怖之上的恐怖。我仍然不同意你说的那么糟糕,但我同意Jon的模式更干净,因为这是为了抛出一个异常,而不是实际“清理”某些东西。实际上,这是我所说的“范围”模式的一个小变化,如果使用得当,它是非常有用的。与框架的Transactions命名空间中的TransactionScope对象类似,我认为使用模式可以用于“聪明”的技巧,尽管这不是一个很好的技巧。我喜欢ASP.NETMVC团队使用它来开始/结束表单的方式,尽管我在其他地方也看到过这样做。例如,SharePoint有一个
SPMonitoredScope
对象,用于将执行信息写入使用此模式的日志。@Toby:非常不同
TransactionScope
包装非托管资源,失败时展开事务是其清理的一部分。@Jason:非托管清理存在与此处相同的问题——如果无法满足正在清理的外部实体的类不变量,非托管清理不应让执行继续不受干扰,但是,当清理代码抛出一个掩盖了已经存在的异常的异常时,这很烦人。如果在存在挂起的异常时,不只是扼杀ContractViolatedException,而是将挂起的异常包装在ContractViolatedException中,那就更好了。你能证明这一点吗?@supercat:这很重要,留给读者作为练习:)难道没有一条规则要求析构函数尽其所能避免抛出异常吗?这是IDisposable滥用后果的一个很好的例子。和CA1065,二对一。另外,我认为“使用”更容易调试(委托生成不明显的堆栈,它们跳入和跳出同一个函数,而且这对于出生在LINQ之前的人来说可能很奇怪),并且“使用”版本生成的代码少了两倍(你得到委托开销,你仍然需要在CheckInvariantActions中尝试/最终)。@Alexei:不,你不需要尝试/最终-如果操作引发异常,OP不想执行检查。我读OP帖子时说“我对合同验证很满意,但我想知道失败的原因”,你读的是“我