一次性上下文对象模式

本文关键字:模式 对象 上下文 一次性 | 更新日期: 2023-09-27 18:20:14

简介

我刚刚想到了一种新的设计模式。我想知道它是否存在,如果不存在,为什么不(或者为什么我不应该使用它)。

我正在使用OpenGL创建一个游戏。在OpenGL中,您通常希望"绑定"事物,即使它们在当前上下文中保持一段时间,然后解除绑定。例如,您可以调用glBegin(GL_TRIANGLES),然后绘制一些三角形,然后调用glEnd()。我喜欢在中间缩进所有的东西,这样就可以清楚地知道它的起点和终点,但我的IDE喜欢取消缩进,因为没有大括号。然后我想我们可以做一些聪明的事!它基本上是这样工作的:

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

GL.Begin返回一个特殊的DrawBind对象(带有内部构造函数)并实现IDisposable,以便它在块的末尾自动调用GL.End()。这样,所有的东西都能很好地对齐,而且你不能忘记调用end()。

这个图案有名字吗?

通常当我看到使用using时,你会这样使用它:

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

但在这种情况下,我们不需要对"used"对象调用任何方法,因此我们不需要将其分配给任何对象,而且它除了调用相应的结束函数之外没有其他用途。


示例

对于Anthony Pegram,他想要一个我目前正在开发的代码的真实示例:

重构前:

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();
}

重构后:

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

请注意,还有第二个好处,_ibo.Bind返回的对象还记得我要解除绑定的"BufferTarget"。它还将您的注意力吸引到GL.DrawElements,它实际上是该函数中唯一重要的语句(它做了一些引人注目的事情),并隐藏了那些冗长的未绑定语句。

我想一个缺点是我不能用这个方法交错缓冲区目标。我不确定什么时候会想要,但我必须保留一个引用来绑定对象并手动调用Dispose,或者手动调用end函数。


命名

如果没有人反对,我将为这个一次性上下文对象(DCO)习语配音


问题

JasonTrue提出了一个很好的观点,即在这种情况下,使用语句嵌套的OpenGL缓冲区不会像预期的那样工作,因为一次只能绑定一个缓冲区。然而,我们可以通过扩展"绑定对象"来使用堆栈来解决这个问题:

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

编辑:刚刚注意到一个问题。以前,如果你没有Dispose()你的上下文对象,实际上没有任何后果。上下文不会切换回原来的样子。现在,如果你忘记在某种循环中处理它,你就会得到一个stackoverflow。也许我应该限制堆栈大小。。。

一次性上下文对象模式

类似的策略也用于带有HtmlHelper的Asp.NetMVC。看见http://msdn.microsoft.com/en-us/library/system.web.mvc.html.formextensions.beginform.aspx(using (Html.BeginForm()) {....}

因此,除了文件句柄、数据库或网络连接、字体等非托管资源明显"需要"IDisposable之外,至少有一个先例可以将此模式用于其他用途。我不认为它有什么特殊的名称,但在实践中,它似乎是C#习惯用法,与C++习惯用法"资源获取即初始化"相对应。

当您打开一个文件时,您正在获取并保证处理一个文件上下文;在您的示例中,您获取的资源是一个"绑定上下文",用您的话说。虽然我听说过"Dispose pattern"或"Using pattern"用于描述广泛的类别,但本质上"确定性清理"就是你所说的;你在控制对象的寿命。

我不认为这真的是一个"新"模式,它在您的用例中脱颖而出的唯一原因是,显然您所依赖的OpenGL实现没有特别努力匹配C#习惯用法,这需要您构建自己的代理对象。

我唯一担心的是,如果有任何不明显的副作用,例如,如果你有一个嵌套的上下文,在你的块(或调用堆栈)的深处有类似的using构造。

ASP.NET/MVC使用此(可选)模式来呈现<form>元素的开头和结尾,如下所示:

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

这与您的示例类似,因为除了IDisposable的一次性语义之外,您没有使用它的值。我从来没有听说过这个名字,但我以前在其他类似的场景中使用过这种东西,除了理解如何使用IDisposable来充分利用using块之外,我从未将其视为其他任何东西,类似于我们如何通过实现IEnumerable来利用foreach语义。

与其说这是一种模式,不如说是一种习惯用法。模式通常更复杂,涉及几个移动部分,而习惯用法只是在代码中做事的聪明方法。

在C++中,它被大量使用。每当您想要获取某个内容或输入一个范围时,您都会创建一个类的自动变量(即在堆栈上),开始创建,或者在输入时需要执行的任何操作。当您离开声明自动变量的作用域时,将调用析构函数。然后,析构函数应结束删除,或进行任何需要清理的操作。

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