Disposable Context Object pattern

2019-08-02 15:39发布

问题:

Introduction

I just thought of a new design pattern. I'm wondering if it exists, and if not, why not (or why I shouldn't use it).

I'm creating a game using an OpenGL. In OpenGL, you often want to "bind" things -- i.e., make them the current context for a little while, and then unbind them. For example, you might call glBegin(GL_TRIANGLES) then you draw some triangles, then call glEnd(). I like to indent all the stuff inbetween so it's clear where it starts and ends, but then my IDE likes to unindent them because there are no braces. Then I thought we could do something clever! It basically works like this:

using(GL.Begin(GL_BeginMode.Triangles)) {
   // draw stuff
}

GL.Begin returns a special DrawBind object (with an internal constructor) and implements IDisposable so that it automatically calls GL.End() at the end of the block. This way everything stays nicely aligned, and you can't forget to call end().

Is there a name for this pattern?

Usually when I see using used, you use it like this:

using(var x = new Whatever()) {
   // do stuff with `x`
}

But in this case, we don't need to call any methods on our 'used' object, so we don't need to assign it to anything and it serves no purpose other than to call the corresponding end function.


Example

For Anthony Pegram, who wanted a real example of code I'm currently working on:

Before refactoring:

public void Render()
{
    _vao.Bind();
    _ibo.Bind(BufferTarget.ElementArrayBuffer);
    GL.DrawElements(BeginMode.Triangles, _indices.Length, DrawElementsType.UnsignedInt, IntPtr.Zero);
    BufferObject.Unbind(BufferTarget.ElementArrayBuffer);
    VertexArrayObject.Unbind();
}

After refactoring:

public void Render()
{
    using(_vao.Bind())
    using(_ibo.Bind(BufferTarget.ElementArrayBuffer))
    {
        GL.DrawElements(BeginMode.Triangles, _indices.Length, DrawElementsType.UnsignedInt, IntPtr.Zero);
    }
}

Notice that there's a 2nd benefit that the object returned by _ibo.Bind also remembers which "BufferTarget" I want to unbind. It also draws your atention to GL.DrawElements, which is really the only significant statement in that function (that does something noticeable), and hides away those lengthy unbind statements.

I guess the one downside is that I can't interlace Buffer Targets with this method. I'm not sure when I would ever want to, but I would have to keep a reference to bind object and call Dispose manually, or call the end function manually.


Naming

If no one objects, I'm dubbing this Disposable Context Object (DCO) Idiom.


Problems

JasonTrue raised a good point, that in this scenario (OpenGL buffers) nested using statements would not work as expected, as only one buffer can be bound at a time. We can remedy this, however, by expanding on "bind object" to use stacks:

public class BufferContext : IDisposable
{
    private readonly BufferTarget _target;
    private static readonly Dictionary<BufferTarget, Stack<int>> _handles;

    static BufferContext()
    {
        _handles = new Dictionary<BufferTarget, Stack<int>>();
    }

    internal BufferContext(BufferTarget target, int handle)
    {
        _target = target;
        if (!_handles.ContainsKey(target)) _handles[target] = new Stack<int>();
        _handles[target].Push(handle);
        GL.BindBuffer(target, handle);
    }

    public void Dispose()
    {
        _handles[_target].Pop();
        int handle = _handles[_target].Count > 0 ? _handles[_target].Peek() : 0;
        GL.BindBuffer(_target, handle);
    }
}

Edit: Just noticed a problem with this. Before if you didn't Dispose() of your context object there wasn't really any consequence. The context just wouldn't switch back to whatever it was. Now if you forget to Dispose of it inside some kind of loop, you're wind up with a stackoverflow. Perhaps I should limit the stack size...

回答1:

A similar tactic is used with Asp.Net MVC with the HtmlHelper. See http://msdn.microsoft.com/en-us/library/system.web.mvc.html.formextensions.beginform.aspx (using (Html.BeginForm()) {....})

So there's at least one precedent for using this pattern for something other than the obvious "need" for IDisposable for unmanaged resources like file handles, database or network connections, fonts, and so on. I don't think there's a special name for it, but in practice, it seems to be the C# idiom that serves as the counterpart to the C++ idiom, Resource Acquisition is Initialization.

When you're opening a file, you're acquiring, and guaranteeing the disposal of, a file context; in your example, the resource you're acquiring is a is a "binding context", in your words. While I've heard "Dispose pattern" or "Using pattern" used to describe the broad category, essentially "deterministic cleanup" is what you're talking about; you're controlling the lifetime the object.

I don't think it's really a "new" pattern, and the only reason it stands out in your use case is that apparently the OpenGL implementation you're depending on didn't make a special effort to match C# idioms, which requires you to build your own proxy object.

The only thing I'd worry about is if there are any non-obvious side effects, if, for example, you had a nested context where there were similar using constructs deeper in your block (or call stack).



回答2:

ASP.NET/MVC uses this (optional) pattern to render the beginning and ending of a <form> element like this:

@using (Html.BeginForm()) {
    <div>...</div>
}

This is similar to your example in that you are not consuming the value of your IDisposable other than for its disposable semantics. I've never heard of a name for this, but I've used this sort of thing before in other similar scenarios, and never considered it as anything other than understanding how to generally leverage the using block with IDisposable similar to how we can tap into the foreach semanatics by implementing IEnumerable.



回答3:

I would this is more an idiom than a pattern. Patterns usually are more complex involving several moving parts, and idioms are just clever ways to do things in code.

In C++ it is used quite a lot. Whenever you want to aquire something or enter a scope you create an automatic variable (i.e. on the stack) of a class that begins or creates or whatever you need to be done on entry. When you leave the scope where the automatic variable is declared the destructor is called. The destructor should then end or delete or whatever is required to clean up.

class Lock {
private:

  CriticalSection* criticalSection;

public:

  Lock() {
    criticalSection = new CriticalSection();
    criticalSection.Enter();
  }

  ~Lock() {
    criticalSection.Leave();
    delete criticalSection;
  }

}

void F() {
  Lock lock();

  // Everything in here is executed in a critical section and it is exception safe.
}