代码之家  ›  专栏  ›  技术社区  ›  Jeff Meatball Yang

每当有相应的“开始”调用时强制执行“结束”调用

  •  7
  • Jeff Meatball Yang  · 技术社区  · 15 年前

    每次在函数中调用“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
    }
    

    12 回复  |  直到 10 年前
        1
  •  9
  •   plinth    15 年前

    我不同意埃里克的观点:什么时候做还是不做取决于具体情况。有一次,我在修改我的代码库,使之包含对自定义图像类的所有访问的获取/释放语义。图像最初分配在不移动的内存块中,但我们现在能够将图像放入块中,如果没有获取图像,这些块可以移动。在我的代码中,一个内存块滑过解锁是一个严重的错误。

    因此,加强这一点至关重要。我创建了这个类:

    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个地方,我可以保证永远不会失败解锁。我必须保证——否则会冻结客户堆中的大量内存。我不能那样做。

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

    所以,当你绝对必须有括号内的电话,不能失败时,选择RAII。 如果不这样做是琐碎的,就不要为RAII操心。

        2
  •  14
  •   Eric Lippert    15 年前

    基本上问题是:

    • 我有全球状态。。。
    • 但我想确定我把它变异回来了。

    你已经发现了 你那样做很痛 . 我的建议是,与其想办法减轻痛苦,不如想办法一开始就不要做痛苦的事。

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

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

    编译器中的错误报告引擎是 全局状态 但不要报告错误 ". 毕竟,在这个节目里你是这样做的

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

    太可怕了。我们真正应该做的是重新设计编译器,以避免错误 语义分析器的输出 ,不是 编译器的全局状态 . 然而,很难通过依赖于全局状态的数十万行现有代码来实现这一点。

    总之,还有别的事要考虑。您的“使用”解决方案具有在抛出异常时停止跳转的效果。 这样做对吗? 可能不是。毕竟,一个 意外,未处理 已引发异常。整个系统可能

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

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

    攻击 在您的代码上,来自一个攻击者,他非常希望您能够解锁对全局状态的访问,因为它是一个易受攻击的、不一致的形式。

        3
  •  8
  •   Lee    15 年前

    Action<Jumper> 要包含跳线实例上所需的操作,请执行以下操作:

    public static void Jump(Action<Jumper> jumpAction)
    {
        StartJumping();
        Jumper j = new Jumper();
        jumpAction(j);
        EndJumping();
    }
    
        4
  •  6
  •   Ian Mercer    15 年前

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

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

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

        5
  •  5
  •   LBushkin    15 年前

    杰夫,

    你想要达到的目标通常被称为 Aspect Oriented Programming (AOP)。在C#中使用AOP范例编程并不容易,也不可靠。。。但是。CLR和.NET框架中直接内置了一些功能,在某些狭窄的情况下使AOP成为可能。例如,当您从 ContextBoundObject 可以使用ContextAttribute在CBO实例上的方法调用之前/之后注入逻辑。你可以看到 examples of how this is done 在这里。

    派生CBO类既烦人又有限制,还有另一种选择。您可以使用PostSharp这样的工具将AOP应用于任何C#类。 PostSharp 在编译后的步骤中重写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 OnEntry OnExit 方法。

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

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

        6
  •  4
  •   liori    14 年前

    我其实不认为这是滥用权力 using 使用

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

    注意,不需要在中指定变量 条款。这就足够了:

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

    我略微忽略C++的RAIN习语,其中析构函数总是被调用。 我想是你在C#能到的最近的地方了。

        7
  •  2
  •   Rob Goodwin    15 年前

    我们几乎完全按照您的建议在我们的应用程序中添加方法跟踪日志记录。比必须打两个记录电话要好得多,一个用于进入,一个用于退出。

        8
  •  1
  •   Jon Onstott    15 年前

        9
  •  1
  •   Jono    15 年前

    我喜欢这种风格,并且经常在我想要保证一些令人崩溃的行为时实现它:通常它比最后一次尝试要干净得多。我不认为您应该费心声明和命名引用j,但是我认为您应该避免调用EndJumping方法两次,您应该检查它是否已经被释放。关于非托管代码,请注意:它是一个终结器,通常是为此实现的(尽管通常会调用Dispose和SuppressFinalize以更快地释放资源)

        10
  •  1
  •   Robert Paulson    15 年前

    我在这里对一些答案做了一些评论,关于什么 IDisposable 是和不是,但我要重申一点 可识别的 是启用确定性清理,但不保证确定性清理。i、 e.它不能保证被调用,只有在与 using 阻止。

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

    使用

    如果您确实有一个IDisposable类,而不需要终结器,那么我看到了一种模式,用于检测人们何时忘记调用 Dispose()

    一个实际的例子是一个类,它以某种特殊的方式封装了对文件的写入。因为 MyWriter FileStream 这也是 可识别的 要有礼貌。

    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
        }
    
    }
    

    哎哟。这很复杂,但这是人们看到可识别性时所期望的。

    这个类除了打开一个文件什么都不做,但这就是你得到的 可识别的 ,并且日志记录非常简单。

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

    我的作者 上面的代码不需要终结器,所以在调试构建之外添加终结器是没有意义的。

        11
  •  1
  •   adrianm    15 年前

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

        12
  •  0
  •   James Westgate    15 年前
    1. 我认为你不想让这些方法成为静态的
    2. 如果已经调用了end jumping,则需要在dispose中进行检查。

    您可以使用一个参考计数器或一个标志来跟踪“跳跃”的状态。有些人会说IDisposable只适用于非托管资源,但我认为这是可以的。否则,应该将开始和结束跳转设置为私有,并使用析构函数来处理构造函数。

    class Jumper
    {
        public Jumper() {   Jumper.StartJumping(); }
        public ~Jumper() {  Jumper.EndJumping(); }
    
        private void StartJumping() {...}
        public void EndJumping() {...}
    }
    
    推荐文章