代码块后自动调用方法

发布于 2024-12-27 14:26:19 字数 746 浏览 2 评论 0原文

我在游戏中添加了在设定时间间隔后可重复操作的概念。
我有一个类来管理是否可以执行给定的操作。
调用者通过调用 CanDoAction 查询是否可以执行该操作,如果可以,则执行该操作并使用 MarkActionDone 记录他们已完成该操作。

if (WorldManager.CanDoAction(playerControlComponent.CreateBulletActionId))
{
    // Do the action

    WorldManager.MarkActionDone(playerControlComponent.CreateBulletActionId);
}

显然,这可能容易出错,因为您可能忘记调用 MarkActionDone,或者可能忘记调用 CanDoAction 进行检查。

理想情况下,我希望保持类似的界面,而不必传递 Action 或类似的内容,因为我在 Xbox 上运行,并且希望避免传递操作并调用它们。特别是因为必须涉及大量闭包,因为操作通常依赖于周围的代码。

我正在考虑以某种方式(ab)使用 IDisposeable 接口,因为这将确保最后可以调用 MarkActionDone ,但是我不认为我可以跳过如果 CanDoAction 为 false,则使用块。

有什么想法吗?

I'm adding the notion of actions that are repeatable after a set time interval in my game.
I have a class that manages whether a given action can be performed.
Callers query whether they can perform the action by calling CanDoAction, then if so, perform the action and record that they've done the action with MarkActionDone.

if (WorldManager.CanDoAction(playerControlComponent.CreateBulletActionId))
{
    // Do the action

    WorldManager.MarkActionDone(playerControlComponent.CreateBulletActionId);
}

Obviously this could be error prone, as you could forget to call MarkActionDone, or possibly you could forget to call CanDoAction to check.

Ideally I want to keep a similar interface, not having to pass around Action's or anything like that as I'm running on the Xbox and would prefer to avoid passing actions around and invoking them. Particularly as there would have to be a lot of closures involved as the actions are typically dependent on surrounding code.

I was thinking of somehow (ab)using the IDisposeable interface, as that would ensure the MarkActionDone could be called at the end, however i don't think i can skip the using block if CanDoAction would be false.

Any ideas?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

听不够的曲调 2025-01-03 14:26:19

我的首选方法是使用委托模式将此逻辑保留为 WorldManager 的实现细节(因为它定义了有关是否可以执行操作的规则):

public class WorldManager
{
  public bool TryDoAction(ActionId actionId, Action action)
  {
    if (!this.CanDoAction(actionId)) return false;
    try
    {
      action();
      return true;
    }
    finally
    {
      this.MarkActionDone(actionId);
    }
  }

  private bool CanDoAction(ActionId actionId) { ... }
  private void MarkActionDone(ActionId actionId) { ... }
}

这似乎最适合 SOLID 原则,因为它避免了任何其他类必须“了解”WorldManager 的“CanDoAction”、“MarkActionDone”实现细节。

更新

使用 AOP 框架,例如 PostSharp,可能是一个不错的选择,以确保这方面以干净的方式添加到所有必要的代码块中。

My preferred approach would be to keep this logic as an implementation detail of WorldManager (since that defines the rules about whether an action can be performed), using a delegate pattern:

public class WorldManager
{
  public bool TryDoAction(ActionId actionId, Action action)
  {
    if (!this.CanDoAction(actionId)) return false;
    try
    {
      action();
      return true;
    }
    finally
    {
      this.MarkActionDone(actionId);
    }
  }

  private bool CanDoAction(ActionId actionId) { ... }
  private void MarkActionDone(ActionId actionId) { ... }
}

This seems to fit best with SOLID principals, since it avoids any other class having to 'know' about the 'CanDoAction', 'MarkActionDone' implementation detail of WorldManager.

Update

Using an AOP framework, such as PostSharp, may be a good choice to ensure this aspect is added to all necessary code blocks in a clean manner.

枫林﹌晚霞¤ 2025-01-03 14:26:19

如果你想最小化 GC 压力,我建议使用接口而不是委托。如果您使用 IDisposable,则无法避免调用 Dispose,但您可以让 IDisposable 实现使用一个标志来指示 Dispose方法不应该做任何事情。除了委托具有一些内置语言支持这一事实之外,它们实际上没有什么是接口不能做的,但接口比委托有两个优点:

  1. 使用绑定到某些数据的委托通常需要创建一个堆对象用于数据,第二个用于委托本身。接口不需要第二个堆实例。
  2. 在可以使用受限于接口的泛型类型的情况下,而不是直接使用接口类型,可以避免创建任何堆实例,如下所述(因为反勾号格式在列表项中不起作用) 。将静态方法的委托与该方法要使用的数据组合在一起的结构的行为与委托非常相似,而不需要堆分配。

第二种方法需要注意的是:尽管避免 GC 压力是一件好事,但第二种方法最终可能会在运行时创建大量类型。在大多数情况下,创建的类型数量是有限的,但在某些情况下,它可能会无限制地增加。我不确定是否有任何方便的方法来确定在静态分析就足够的情况下程序可以生成的完整类型集(在一般情况下,静态分析不够,确定是否有任何特定的类型)生成的运行时类型相当于停止问题,但正如许多程序实际上可以静态地确定为始终停止或永不停止一样,我希望在实践中人们通常可以识别一组封闭的类型程序可以在运行时生成)。

编辑

上面第#2点中的格式被搞乱了。这是解释,已清理。

定义一个类型 ConditionalCleaner; :IDisposable,它保存一个 T 的实例和一个 Action (两者都在构造函数中提供 - 可能与 Action; 作为第一个参数)。在 IDisposable.Dispose() 方法中,如果 Action 为非 null,则在 T 上调用它。在 SkipDispose() 方法中,将 Action 清空。为了方便起见,您可能还需要类似地定义 ConditionalCleaner: IDisposable (也许还有三参数和四参数版本),并且您可能希望使用泛型定义静态类 ConditionalCleaner CreateCreate 等方法(因此可以说例如 using (var cc = ConditionalCleaner.Create(Console.WriteLine, "ABCDEF") {...}ConditionalCleaner.Create((x) => {Console.WriteLine(x);}, "ABCDEF" ) 在 using 块退出时执行指定的操作,如果使用 Lambda 表达式,则最大的要求是确保 Lambda 表达式不会关闭任何局部变量或参数。调用函数;调用函数想要传递给 lambda 表达式的任何内容都必须是其显式参数,否则系统将定义一个类对象来保存任何封闭变量,以及指向它的新委托。

If you want to minimize GC pressure, I would suggest using interfaces rather than delegates. If you use IDisposable, you can't avoid having Dispose called, but you could have the IDisposable implementation use a flag to indicate that the Dispose method shouldn't do anything. Beyond the fact that delegates have some built-in language support, there isn't really anything they can do that interfaces cannot, but interfaces offer two advantages over delegates:

  1. Using a delegate which is bound to some data will generally require creating a heap object for the data and a second for the delegate itself. Interfaces don't require that second heap instance.
  2. In circumstances where one can use generic types which are constrained to an interface, instead of using interface types directly, one may be able to avoid creating any heap instances, as explained below (since back-tick formatting doesn't work in list items). A struct that combines a delegate to a static method along with data to be consumed by that method can behave much like a delegate, without requiring a heap allocation.

One caveat with the second approach: Although avoiding GC pressure is a good thing, the second approach may end up creating a very large number of types at run-time. The number of types created will in most cases be bounded, but there are circumstances where it could increase without bound. I'm not sure if there would any convenient way to determine the full set of types that could be produced by a program in cases where static analysis would be sufficient (in the general case, where static analysis does not suffice, determining whether any particular run-time type would get produced would be equivalent to the Halting Problem, but just as many programs can in practice be statically determined to always halt or never halt, I would expect that in practice one could often identify a closed set of types that a program could produce at run-time).

Edit

The formatting in point #2 above was messed up. Here's the explanation, cleaned up.

Define a type ConditionalCleaner<T> : IDisposable, which holds an instance of T and an Action<T> (both supplied in the constructor--probably with the Action<T> as the first parameter). In the IDisposable.Dispose() method, if the Action<T> is non-null, invoke it on the T. In a SkipDispose() method, null out the Action<T>. For convenience, you may want to also define ConditionalCleaner<T,U>: IDisposable similarly (perhaps three- and four-argument versions as well), and you may want to define a static class ConditionalCleaner with generic Create<T>, Create<T,U>, etc. methods (so one could say e.g. using (var cc = ConditionalCleaner.Create(Console.WriteLine, "ABCDEF") {...} or ConditionalCleaner.Create((x) => {Console.WriteLine(x);}, "ABCDEF") to have the indicated action performed when the using block exits. The biggest requirement if one uses a Lambda expression is to ensure that the lambda expression doesn't close over any local variables or parameters from the calling function; anything the calling function wants to pass to the lambda expression must be an explicit parameter thereof. Otherwise the system will define a class object to hold any closed-over variables, as well as a new delegate pointing to it.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文