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

Jef*_*ang 8 c# business-logic

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

每次在函数中调用"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
}
Run Code Online (Sandbox Code Playgroud)

有一个更好的方法吗?

Eri*_*ert 14

基本上问题是:

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

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

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

void M(Func<int, int> f) { }
void M(Func<string, int> f) { }
...
M(x=>x.Length);
Run Code Online (Sandbox Code Playgroud)

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

编译器中的错误报告引擎是全局状态.在C#1和2还没有过,我们不得不说"绑定这整个身体的方法来确定是否有任何错误的目的的情况,但不报告错误 ".毕竟,在这个程序,你就不会想要得到的错误"诠释不具有的属性称为长度",你希望它发现,做的一份说明,并没有举报.

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

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

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

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

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

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

  • 我给了这个+1,但问题是它没有提供任何有用的替代品:-( (3认同)
  • @Jeffrey:我不太确定我同意你的分析."使用"声明*旨在*以便于及时处理宝贵的系统资源.是的,这在某种意义上是"改变全球状态",但它是程序外部的状态,它是出于礼貌而非必要性.foreach行为只是使用行为的一个特例.开始/结束行为的唯一真正类比是lock语句,这是C#*中唯一最难说明的语句,因为*它操纵全局状态. (3认同)
  • @Jeff:它确实有一个更普遍的对应物; 锁定和使用都是try-finally的特例. (3认同)
  • @Eric Lippert:我不确定为什么你认为iDisposable应该只用于珍贵的"系统"资源; 在我看来,如果一个类的对象拥有任何东西(无论是否珍贵),它的使用是正确的,它将存在或具有超出这些对象的使用寿命的意义.虽然异常有时会以Dispose无法帮助的方式破坏系统状态,但它们也可能以一种必要且足以修复Dispose的方式来破坏状态(特别是在事务中很常见). (3认同)
  • @sgorozco:我个人不会使用`using`.那根本不是资源. (2认同)

pli*_*nth 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这允许我做这个子类:

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)
    {
    }
}
Run Code Online (Sandbox Code Playgroud)

这反过来让我写这个代码:

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

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

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

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

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

因此,当你绝对肯定要有括号内的电话并且不能失败时选择RAII.如果做其他事情是微不足道的话,不要打扰RAII.


Lee*_*Lee 8

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

public static void Jump(Action<Jumper> jumpAction)
{
    StartJumping();
    Jumper j = new Jumper();
    jumpAction(j);
    EndJumping();
}
Run Code Online (Sandbox Code Playgroud)


Ian*_*cer 6

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

var sequence = StartJumping().Then(some_other_method).Then(some_third_method);
// forgot to do EndJumping()
sequence.Execute();
Run Code Online (Sandbox Code Playgroud)

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

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


LBu*_*kin 5

杰夫

您试图实现的目标通常称为面向方面的编程(AOP)。在C#中使用AOP范例进行编程并不容易-还是可靠的...。在某些狭窄情况下,CLR和.NET框架中直接内置了一些功能,这些功能使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...
}
Run Code Online (Sandbox Code Playgroud)

PostSharp 通过重写已编译的IL代码以包含运行在类和方法中定义的代码的指令来实现魔力JumperAttributeOnEntryOnExit

对于您而言,PostSharp / AOP是否比“重新使用” using语句更好的选择对我来说还不清楚。我倾向于同意@Eric Lippert的观点,using关键字混淆了代码的重要语义,并}在using块的末尾对符号施加了副作用和语义指导-这是意外的。但这与将AOP属性应用于代码有什么不同吗?它们还在声明性语法后面隐藏了重要的语义……但这就是AOP的重点。

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