强制每当有一个相应的“开始”叫

问题描述:

比方说,我想执行规则的“结束”拨打:强制每当有一个相应的“开始”叫

每次你打电话“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 
} 

有没有更好的方式来做到这一点?

+3

我非常同意这是一种滥用。查看http://*.com/questions/2101524/ – 2010-04-27 22:27:23

+3

@Eric:这样不断弹出一遍又一遍,这种模式很有用。没有IDisposable的行李可以考虑作为语言扩展的东西?我偏爱“with”关键字,尽管我不喜欢IWithable :) – 2010-04-27 23:57:29

+2

嗯 - 如果@Eric Lippert认为它的滥用应该注意!我倾向于同意它仅仅因为“Dispose”的意图是围绕资源清理而滥用使用陈述的意图。问题是,如果你担心'EndJumping'不会被调用,如果编码器不用'using'语句编写代码,那么这不是同一个问题吗? – 2010-04-28 00:17:06

我不同意Eric:什么时候这样做取决于具体情况。有一次,我重新修改了我的一个庞大的代码库,在所有对自定义图像类的访问中包含获取/释放语义。图像最初是以不移动的内存块分配的,但我们现在有能力将图像分块,如果它们没有被获取就可以移动。在我的代码中,对于一块内存块滑过解锁状态是一个严重的错误。

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

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,如果它是微不足道的做。

+0

当我们的班级对不可恢复的全球资源(如记忆)负责时,我们是否可以缩小“取决于具体情况”这个命题? – 2010-04-28 15:51:02

+0

@JeffSternal:你可以指向微软实际上“定义”术语“资源”的任何地方吗?我会假设一个对象在任何时候要求某个其他实体代表它做某件事情,直到进一步通知为止,这会损害它自己或其他实体。内存不是一个非常令人信服的资源示例,因为它是可替代的。像锁或文件(它实际上包含锁)等事情是一个更重要的例子,但最根本的是对象有一些其他实体将其状态保持在一个不太理想的状态。 – supercat 2014-09-28 18:29:18

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

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

您可以使用引用计数器或标记来跟踪“跳跃”的状态。有些人会说IDisposable只适用于非托管资源,但我认为这很好。否则,你应该使开始和结束跳到私人并使用析构函数去与构造函数。

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

    private void StartJumping() {...} 
    public void EndJumping() {...} 
} 
+0

感谢James的反馈。这些方法是静态的,因为它实际上只是在单例类中设置一个标志,并且EndJumping是幂等的(只是取消标志)。我也可以使静态方法是私有的,所以唯一的方法就是使用using()子句来调用start/end。如果你打电话再次开始跳跃,你需要第二次结束跳转呼叫......实质上是嵌套的呼叫。 – 2010-04-27 22:07:19

+2

如果(ab)使用'IDisposable'来满足OP的要求是错误的,那么使用析构函数来做到这一点是非常糟糕的!此外,与'IDisposable'不同,终结代码不会被确定性地调用,因此这不符合*必须在返回*之前调用“EndJumping()”的要求。 – LukeH 2010-04-27 22:15:03

+0

如果你依赖EndJumping的析构函数,它不会是规范的;这可能只会在多个垃圾收集之后在程序的生命周期中稍后调用。 – Jono 2010-04-27 22:17:18

如果你需要控制一个范围的操作,我想补充其采取Action<Jumper>包含上的跳线实例所需的操作方法:

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

这使得不可能在不同的UI回调中调用StartJumping/EndJumping,或者通常响应来自环境的两个不同事件。 – liori 2010-04-27 22:10:10

+0

我喜欢这样,但它很难分解现有的函数,所以它们可以通过静态跳转来调用 - 就像依赖注入(?)一样。 – 2010-04-27 22:24:06

+0

这是一个很好的解决方案,但它需要您从内向外C(B(A))构建序列,而不是按照您希望事情发生的顺序进行。流畅的界面可以帮助解决这个问题:A.B.C()。 – 2010-04-28 00:07:16

其实我并不认为这是滥用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#的用户。

+0

太棒了。我不知道我不需要设置参考j,甚至不需要使用“新”关键字! – 2010-04-27 22:21:12

+0

事实上,在这种情况下,您确实需要'new'关键字。 – 2011-04-20 17:09:57

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

+1

FWIW - IDisposable.Dispose()应该可以被多次调用而不会产生副作用。 http://msdn.microsoft.com/en-us/library/system.idisposable.dispose。aspx – 2010-04-27 22:15:55

+0

太棒了。我不知道我不需要设置参考j。 – 2010-04-27 22:20:40

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

+0

美丽。这就是我们的“跳跃”......跟踪,当有人跳到应用程序的另一部分。 – 2010-04-27 22:24:53

本质的问题是:

  • 我有全局状态...
  • ,我想发生变异,全球状态...
  • 但我要确保我变异回来。

你已经发现当你这样做的时候会伤害。我的建议并不是试图找到一种让自己不那么受伤的方法,而是试图找到一种方法,不要让痛苦的事情摆在首位。

我很清楚这是多么困难。当我们在v3中将lambda添加到C#时,我们遇到了一个很大的问题。考虑以下内容:

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

我们究竟如何将此成功绑定?那么,我们所做的就是试试两个(x是int,或者x是字符串),看看哪个(如果有的话)会给我们一个错误。那些不会出现错误的人成为重载解决方案的候选人。

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

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

这太可怕了。我们真正应该做的是重新设计编译器,以便错误是输出的语义分析器,而不是编译器的全局状态。但是,很难通过成千上万的依赖于全局状态的现有代码来进行线程化。

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

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

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

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

+3

我已经给出了这个+1,但问题是它没有提供任何有用的替代方法:-( – 2010-04-28 01:38:51

+1

C#语言包括这种模式的三个示例:'lock','foreach'和'using'。在一个错误条件下修改某些状态的关注,这仍然是一个非常有用的模式,否则它不会被构建到三个语言中,因为我们不能修改语言本身, Dispose'是程序员在我们自己的代码中模拟模式的唯一选择(我想我们可以做一个自定义的预处理器等等,但是哎呀!)我并不是说''using'应该像这样被滥用,但是有很好的理由,这样做的诱惑力很强! – 2010-04-28 13:36:06

+3

@杰弗里:我不太确定我是否同意你的分析,“使用”声明旨在帮助及时处理宝贵的系统资源。从某种意义上说“变异的全球状态”,但这是该计划的外部状态,这是出于礼貌,而不是必要。 foreach行为只是使用行为的特例。与开始/结束行为唯一真正的类比是锁定语句,这是C#*中最难得到的单一语句,因为它操纵全局状态。 – 2010-04-28 14:06:24

我已经评论了一些关于什么是IDisposable的答案,但我会重申IDisposable是为了启用确定性清理,但并不保证确定性清理。即不保证被叫,只有在与using块配对时才有所保证。

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

现在,我不会对你使用的using因为埃里克利珀做它一个更好的工作发表意见。

如果你有没有需要终结器IDisposable的类,模式我已经看到了检测,当人们忘记调用Dispose()是添加多数民众赞成有条件地DEBUG编译构建,使您可以登录的东西,只要您的终结终结叫做。

一个现实的例子是一个类,是封装在一些特殊的方式写入文件。由于MyWriterFileStream的引用也是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 
    } 

} 

Ouch。这很复杂,但这是人们看到IDisposable时所期望的。

类甚至没有做任何事情,但打开一个文件,但这是你IDisposable得到什么,以及日志记录是非常简单的。

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

     // logic omitted 
    } 

终结是昂贵的,并且MyWriter以上不需要终结,所以没有点增加一个外的调试版本。

另一种方法,将在某些情况下工作(即,当动作都可以发生在年底)是创建一系列的类与流畅的界面和一些最后的execute()方法。执行()可以链接下行并执行任何规则(或者可以在构建开始序列时构建闭合序列)。

的一个优势,该技术具有对他人是你没有被作用域规则的限制。例如如果你想建立基于用户输入或其他异步事件的序列,你可以做到这一点。

+0

有趣的想法。这也允许匿名代表(lambda)。 – 2010-04-28 16:52:23

+0

它比使用()有另一个优点,即使用()不提供强制实际使用它在using()中。 – 2010-04-28 18:04:28

杰夫,

你想达到什么是通常被称为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代码,包括指令来运行你已经在JumperAttributeOnEntryOnExit方法定义的代码实现了魔术

无论您的情况PostSharp/AOP是比“重新调整用途”更好的选择,使用声明对我来说还不清楚。我倾向于赞同@Eric Lippert的使用关键字混淆了代码的重要语义,并对使用块末尾的}符号施加了副作用和语义标记 - 这是意外的。但是,这与将AOP属性应用于代码有什么不同?它们还隐藏了声明语法背后的重要语义......但这就是AOP的一个重点。

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

+0

感谢您指向PostSharp的指针。是的,正如我在文章中所说,我完全理解我滥用语言功能。我希望有一个更简单的方法来完成这个任务,但我并没有使用真正的全局内存 - 更像是Start/EndJumping向远程进程发出命令。 – 2010-04-29 02:59:01

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

使用StartJumping我需要手动查看每个调用,以确定是否有可能发生异常,返回,中断,继续,转到等可能导致EndJumping不被调用。