只要有相应的“开始”呼叫,就执行“结束”呼叫

时间:2010-04-27 21:56:27

标签: c# business-logic

假设我想执行一条规则:

  

每次在函数中调用“StartJumping()”时,必须在返回之前调用“EndJumping()”。

当开发人员编写代码时,他们可能只是忘记调用EndSomething - 所以我想让它易于记忆。

我只能想到一种方法:它滥用“使用”关键字:

class Jumper : IDisposable {
    public Jumper() {   Jumper.StartJumping(); }
    public void Dispose() {  Jumper.EndJumping(); }

    public static void StartJumping() {...}
    public static void EndJumping() {...}
}

public bool SomeFunction() {
    // do some stuff

    // start jumping...
    using(new Jumper()) {
        // do more stuff
        // while jumping

    }  // end jumping
}

有更好的方法吗?

12 个答案:

答案 0 :(得分:13)

基本上问题是:

  • 我有全球状态......
  • 我想改变那个全球状态......
  • 但我想确保我改回来。

你发现在你这样做时会伤害。我的建议是,而不是试图找到一种方法让它减少伤害,试图找到一种方法,不要首先做痛苦的事情。

我很清楚这有多难。当我们在v3中将lambdas添加到C#时,我们遇到了一个大问题。请考虑以下事项:

void M(Func<int, int> f) { }
void M(Func<string, int> f) { }
...
M(x=>x.Length);

我们如何成功地绑定它?好吧,我们做的是尝试两者(x是int,或x是字符串)并查看哪些(如果有的话)给我们一个错误。不给出错误的那些成为重载解析的候选者。

编译器中的错误报告引擎是全局状态。在C#1和2中,从来没有一种情况我们不得不说“绑定整个方法体以确定它是否有任何错误但不报告错误”。毕竟,在这个程序中你做想要得到错误“int没有名为Length的属性”,你希望它发现它,记下它,而不是报告它

所以我所做的就是你所做的。开始抑制错误报告,但不要忘记停止抑制错误报告。

太可怕了。我们真正应该做的是重新设计编译器,以便错误是输出语义分析器,而不是编译器的全局状态。但是,很难通过依赖于全局状态的数十万行现有代码来解决这个问题。

无论如何,还有别的想法。您的“使用”解决方案具有在抛出异常时停止跳转的效果。 这是正确的做法吗?可能不是。毕竟,抛出了意外的,未处理的异常。整个系统可能大量不稳定。在这种情况下,您的内部状态不变量都不会实际上是不变的。

以这种方式看待它:我改变了全球状态。然后我得到了一个意外的,未处理的异常。我知道,我想我会再次改变全球状态!那会有所帮助!看起来像一个非常非常糟糕的主意。

当然,这取决于对全球状态的突变。如果它是“开始向用户再次报告错误”,就像它在编译器中一样,那么正确要对未处理的异常做的事情就是开始再次向用户报告错误:毕竟,我们'需要报告编译器刚出现未处理异常的错误!

另一方面,如果突变为全局状态是“解锁资源并允许它被不值得信任的代码观察和使用”,那么自动解锁它可能是一个非常糟糕的想法。这个意外的,未处理的异常可能是您的代码上的攻击的证据,来自攻击者,他非常希望您将解锁对全局状态的访问,因为它处于易受攻击的,不一致的形式。

答案 1 :(得分:9)

我不同意埃里克:什么时候这样做取决于具体情况。有一次,我正在重新设计一个大型代码库,以包含对自定义图像类的所有访问的获取/发布语义。图像最初分配在不动的内存块中,但我们现在能够将图像放入允许移动的块中(如果尚未获取的话)。在我的代码中,对于一块内存已经过了解锁是一个严重的错误。

因此,执行此操作至关重要。我创建了这个类:

public class ResourceReleaser<T> : IDisposable
{
    private Action<T> _action;
    private bool _disposed;
    private T _val;

    public ResourceReleaser(T val, Action<T> action)
    {
        if (action == null)
            throw new ArgumentNullException("action");
        _action = action;
        _val = val;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~ResourceReleaser()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (_disposed)
            return;

        if (disposing)
        {
            _disposed = true;
            _action(_val);
        }
    }
}

允许我做这个子类:

public class PixelMemoryLocker : ResourceReleaser<PixelMemory>
{
    public PixelMemoryLocker(PixelMemory mem)
        : base(mem,
        (pm =>
            {
                if (pm != null)
                    pm.Unlock();
            }
        ))
    {
        if (mem != null)
            mem.Lock();
    }

    public PixelMemoryLocker(AtalaImage image)
        : this(image == null ? null : image.PixelMemory)
    {
    }
}

反过来让我编写这段代码:

using (var locker = new PixelMemoryLocker(image)) {
    // .. pixel memory is now locked and ready to work with
}

这是我需要的工作,快速搜索告诉我,我需要它在186个地方,我可以保证永远不会解锁。而且我必须能够做出这样的保证 - 否则可能会冻结我客户端堆中的大量内存。我做不到。

但是,在我处理PDF文档加密的另一种情况下,所有字符串和流都是用PDF字典加密的,除非它们不是。真。有一些边缘情况,加密或解密字典是不正确的,所以在流出一个对象时,我这样做:

if (context.IsEncrypting)
{
    crypt = context.Encryption;
    if (!ShouldBeEncrypted(crypt))
    {
        context.SuspendEncryption();
        suspendedEncryption = true;
    }
}
// ... more code ...
if (suspendedEncryption)
{
    context.ResumeEncryption();
}

那为什么我选择这个而不是RAII方法呢?好吧,在...更多代码中发生的任何异常都意味着你已经死在水中。没有恢复。没有恢复。你必须从一开始就重新开始,并且需要重建上下文对象,所以它的状态无论如何都被冲洗了。相比之下,我只需要执行此代码4次 - 错误的可能性比内存锁定代码少,如果我将来忘记了,生成的文档将立即被破坏(失败)快)。

因此,当你绝对肯定要接受括号内的电话并且不能失败时选择RAII。 如果不这样做,请不要打扰RAII。

答案 2 :(得分:8)

如果你需要控制范围操作,我会添加一个方法,它使Action<Jumper>包含跳线实例所需的操作:

public static void Jump(Action<Jumper> jumpAction)
{
    StartJumping();
    Jumper j = new Jumper();
    jumpAction(j);
    EndJumping();
}

答案 3 :(得分:6)

在某些情况下可以使用的替代方法(即,当所有操作都在最后发生时)将是创建一系列具有流畅接口和一些最终Execute()方法的类。

var sequence = StartJumping().Then(some_other_method).Then(some_third_method);
// forgot to do EndJumping()
sequence.Execute();

执行()可以向下链接并强制执行任何规则(或者您可以在构建开始序列时构建结束序列)。

此技术优于其他技术的一个优点是您不受范围规则的限制。例如如果你想根据用户输入或其他异步事件构建序列,你可以这样做。

答案 4 :(得分:4)

我实际上并不认为这是对using的滥用;我在不同的语境中使用这个成语,从来没有遇到过问题......特别是考虑到using只是一个语法糖。我使用它的一种方法是在我使用的第三方库之一中设置一个全局标志,以便在完成操作时恢复更改:

class WithLocale : IDisposable {
    Locale old;
    public WithLocale(Locale x) { old = ThirdParty.Locale; ThirdParty.Locale = x }
    public void Dispose() { ThirdParty.Locale = old }
}

请注意,您不需要在using子句中分配变量。这就足够了:

using(new WithLocale("jp")) {
    ...
}

我在这里略微错过了C ++的RAII习语,其中总是调用析构函数。我想,using是您可以在C#中获得的最接近的。

答案 5 :(得分:4)

杰夫,

您要实现的目标通常称为Aspect Oriented Programming(AOP)。使用C#中的AOP范例进行编程并不容易 - 或者可靠......直接构建在CLR和.NET框架中的一些功能使得AOP成为可能是某些狭隘的案例。例如,当您从ContextBoundObject派生类时,您可以使用ContextAttribute在CBO实例上的方法调用之前/之后注入逻辑。你可以在这里看到examples of how this is done

推导CBO课程既烦人又有限制 - 还有另一种选择。您可以使用PostSharp之类的工具将AOP应用于任何C#类。 PostSharp比CBO灵活得多,因为它基本上在后编译步骤中重写您的IL代码。虽然这看起来有点可怕,但它非常强大,因为它允许您以几乎任何您能想象的方式编写代码。这是一个基于您的使用场景构建的PostSharp示例:

using PostSharp.Aspects;

[Serializable]
public sealed class JumperAttribute : OnMethodBoundaryAspect
{
  public override void OnEntry(MethodExecutionArgs args) 
  { 
    Jumper.StartJumping();
  }     

  public override void OnExit(MethodExecutionArgs args) 
  { 
    Jumper.EndJumping(); 
  }
}

class SomeClass
{
  [Jumper()]
  public bool SomeFunction()  // StartJumping *magically* called...          
  {
    // do some code...         

  } // EndJumping *magically* called...
}

PostSharp通过重写已编译的IL代码来实现 magic ,以包含运行您在JumperAttribute类'OnEntry和{{1}中定义的代码的说明}} 方法。

在你的情况下,PostSharp / AOP是否比“重新调整”使用声明更好的选择我不清楚。我倾向于同意@Eric Lippert的使用关键词模糊了你的重要语义代码并对使用块末尾的OnExit符号施加副作用和语义限制 - 这是意外的。但这与将AOP属性应用于代码有何不同?它们还隐藏了声明性语法背后的重要语义......但这有点像AOP。

我非常同意Eric的一点是,重新设计代码以避免这样的全局状态(如果可能)可能是最好的选择。它不仅避免了强制执行正确使用的问题,而且还可以帮助避免未来的多线程挑战 - 全球状态非常容易受到影响。

答案 6 :(得分:2)

我们几乎完全按照您的建议完成了在我们的应用程序中添加方法跟踪日志记录的方法。节拍必须进行2次记录呼叫,一次用于输入,一次用于退出。

答案 7 :(得分:1)

拥有抽象基类会有帮助吗?基类中的方法可以调用StartJumping(),即子类将实现的抽象方法的实现,然后调用EndJumping()。

答案 8 :(得分:1)

我喜欢这种风格并且经常在我想要保证一些拆卸行为的时候实现它:通常它比读取终端要干净得多。我不认为你应该为声明和命名引用j而烦恼,但我认为你应该避免两次调用EndJumping方法,你应该检查它是否已被处理掉。并参考了您的非托管代码注释:它是一个通常为此实现的终结器(尽管通常调用Dispose和SuppressFinalize以更快地释放资源。)

答案 9 :(得分:1)

我在这里评论了一些关于IDisposable是什么和不是什么的答案,但我将重申IDisposable是否能够进行确定性清理,但不保证确定性清理。即,不能保证被调用,只有在与using块配对时才能保证。

// despite being IDisposable, Dispose() isn't guaranteed.
Jumper j = new Jumper();

现在,我不打算评论你对using的使用,因为Eric Lippert做得更好。

如果你确实有一个IDisposable类而不需要终结器,我看到的用于检测人们何时忘记调用Dispose()的模式是添加一个在DEBUG构建中有条件编译的终结器,以便您可以记录某些内容每当你的终结者被召唤时。

一个现实的例子是一个以某种特殊方式封装写入文件的类。由于MyWriter包含对FileStream IDisposable的引用,因此我们还应该IDisposable实现礼貌。

public sealed class MyWriter : IDisposable
{
    private System.IO.FileStream _fileStream;
    private bool _isDisposed;

    public MyWriter(string path)
    {
        _fileStream = System.IO.File.Create(path);
    }

#if DEBUG
    ~MyWriter() // Finalizer for DEBUG builds only
    {
        Dispose(false);
    }
#endif

    public void Close()
    {
        ((IDisposable)this).Dispose();
    }

    private void Dispose(bool disposing)
    {
        if (disposing && !_isDisposed)
        {
            // called from IDisposable.Dispose()
            if (_fileStream != null)
                _fileStream.Dispose();

            _isDisposed = true;
        }
        else
        {
            // called from finalizer in a DEBUG build.
            // Log so a developer can fix.
            Console.WriteLine("MyWriter failed to be disposed");
        }
    }

    void IDisposable.Dispose()
    {
        Dispose(true);
#if DEBUG
        GC.SuppressFinalize(this);
#endif
    }

}

哎哟。这很复杂,但这是人们在看到IDisposable时的期望。

该类甚至没有做任何事情,只是打开一个文件,但这就是你使用IDisposable得到的,并且日志记录非常简化。

    public void WriteFoo(string comment)
    {
        if (_isDisposed)
            throw new ObjectDisposedException("MyWriter");

        // logic omitted
    }

终结器很昂贵,上面的MyWriter不需要终结器,所以在DEBUG构建之外添加一个是没有意义的。

答案 10 :(得分:1)

使用模式我可以使用grep (?<!using.*)new\s+Jumper查找可能存在问题的所有地方。

使用StartJumping我需要手动查看每个调用,以查明异常,return,break,continue,goto等是否有可能导致无法调用EndJumping。

答案 11 :(得分:0)

  1. 我不认为你想让这些方法变得静止
  2. 如果已经调用了结束跳跃,你需要检查处理。
  3. 如果我打电话再次开始跳跃会发生什么?
  4. 您可以使用引用计数器或标志来跟踪“跳跃”的状态。有些人会说IDisposable仅用于非托管资源,但我认为这没关系。否则,您应该将start和end跳转为私有,并使用析构函数来使用构造函数。

    class Jumper
    {
        public Jumper() {   Jumper.StartJumping(); }
        public ~Jumper() {  Jumper.EndJumping(); }
    
        private void StartJumping() {...}
        public void EndJumping() {...}
    }