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

假设我想强制执行一条规则:

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

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

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

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 } 

有一个更好的方法吗?

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

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

 public class ResourceReleaser : IDisposable { private Action _action; private bool _disposed; private T _val; public ResourceReleaser(T val, Action 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 { 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方法呢? 好吧,在…更多代码中发生的任何exception都意味着你已经死在水中。 没有恢复。 没有恢复。 你必须从一开始就重新开始,并且需要重建上下文对象,所以它的状态无论如何都被冲洗了。 相比之下,我只需要执行此代码4次 – 错误的可能性比内存锁定代码少,如果我将来忘记了,生成的文档将立即被破坏(失败)快速)。

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

基本上问题是:

  • 我有全球状态……
  • 我想改变这个全球状态……
  • 但是我想确保我把它变回来。

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

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

 void M(Func f) { } void M(Func f) { } ... M(x=>x.Length); 

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

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

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

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

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

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

当然,这取决于对全球状态的突变。 如果它是“开始向用户再次报告错误”,那么对于未处理的exception, 正确的做法就是再次向用户报告错误:毕竟,我们需要报告错误,编译器只有一个未处理的exception!

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

如果您需要控制范围操作,我会添加一个方法,该方法将Action包含在跳线实例上的所需操作:

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

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

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

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

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

杰夫,

你想要实现的目标通常被称为面向方面编程 (AOP)。 使用C#中的AOP范例进行编程并不容易 – 或者可靠…… 直接在CLR和.NET框架中构建的一些function使得AOP成为可能是某些狭隘的案例。 例如,从ContextBoundObject派生类时,可以使用ContextAttribute在CBO实例上方法调用之前/之后注入逻辑。 您可以在此处查看如何完成此操作的示例 。

推导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代码来实现魔术 ,包括运行您在JumperAttribute类的OnEntryOnExit方法中定义的代码的指令。

在你的情况下,PostSharp / AOP是比“重新利用”使用声明更好的选择,我不清楚。 我倾向于同意@Eric Lippert的看法,using关键字会混淆代码的重要语义,并对使用块末尾的}符号施加副作用和语义限制 – 这是意料之外的。 但这与将AOP属性应用于您的代码有何不同? 它们还隐藏了声明性语法背后的重要语义……但这有点像AOP。

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

我实际上并不认为这是滥用; 我在不同的环境中使用这个成语,从来没有遇到过问题……特别是考虑到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#中获得的最接近的。

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

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

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

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

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

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

如果你确实有一个IDisposable类而不需要终结器,我看到的用于检测人们何时忘记调用Dispose()是添加一个在DEBUG构建中有条件编译的终结器,这样你就可以在调用终结器时记录一些东西。

一个现实的例子是一个以某种特殊方式封装写入文件的类。 因为MyWriter持有对也是IDisposableFileStream的引用,所以我们也应该实现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构建之外添加一个是没有意义的。

使用模式我可以使用grep (?来查找可能存在问题的所有地方。

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

  1. 我不认为你想让这些方法保持静态
  2. 如果结束跳跃已被调用,您需要检查处置。
  3. 如果我打电话再开始跳,会发生什么?

您可以使用引用计数器或标志来跟踪“跳跃”的状态。 有些人会说IDisposable仅用于非托管资源,但我认为这没关系。 否则,您应该将start和end跳转为私有,并使用析构函数来使用构造函数。

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