为什么我应该使用单独的初始化和清理方法,而不是将逻辑放入引擎组件的构造函数和析构函数中?


9

我正在开发自己的游戏引擎,目前正在设计经理。我已经读过,对于内存管理,使用Init()CleanUp()函数比使用构造函数和析构函数更好。

我一直在寻找C ++代码示例,以查看这些功能如何工作以及如何将其实现到引擎中。如何Init()CleanUp()工作,我如何能实现他们进入我的引擎?



对于C ++,请参见stackoverflow.com/questions/3786853/…使用Init()的主要原因如下:1)使用辅助函数防止构造函数中的异常和崩溃2)能够使用派生类中的虚拟方法3)规避循环依赖项4)作为避免代码重复的专用方法
brita_

Answers:


12

实际上很简单:

而不是由构造器来完成您的设置,

// c-family pseudo-code
public class Thing {
    public Thing (a, b, c, d) { this.x = a; this.y = b; /* ... */ }
}

...让您的构造函数几乎不执行任何操作,或者根本不执行任何操作,并编写一个称为.init或的方法.initialize,该方法将执行您的构造函数通常会执行的操作。

public class Thing {
    public Thing () {}
    public void initialize (a, b, c, d) {
        this.x = a; /*...*/
    }
}

所以现在不只是这样:

Thing thing = new Thing(1, 2, 3, 4);

你可以走了:

Thing thing = new Thing();

thing.doSomething();
thing.bind_events(evt_1, evt_2);
thing.initialize(1, 2, 3, 4);

这样做的好处是您现在可以在系统中更轻松地使用依赖项注入/控制反转。

而不是说

public class Soldier {
    private Weapon weapon;

    public Soldier (name, x, y) {
        this.weapon = new Weapon();
    }
}

你可以建立士兵,给他装备的方法,在这里你的手他的武器,然后调用所有的构造函数的休息。

因此,现在,与其将一个士兵拿着手枪,另一名士兵拿着步枪,另一名士兵拿着shot弹枪的敌人分类,这是唯一的区别,您可以说:

Soldier soldier1 = new Soldier(),
        soldier2 = new Soldier(),
        soldier3 = new Soldier();

soldier1.equip(new Pistol());
soldier2.equip(new Rifle());
soldier3.equip(new Shotgun());

soldier1.initialize("Bob",  32,  48);
soldier2.initialize("Doug", 57, 200);
soldier3.initialize("Mike", 92,  30);

同样处理破坏。如果您有特殊需要(删除事件侦听器,从数组中删除实例/正在使用的任何结构等),则可以手动调用它们,以便准确了解程序中发生的时间和位置。

编辑


正如Kryotan在下面指出的那样,这回答了原始帖子的“如何”,但实际上并没有很好地完成“为什么”的工作。

正如您可能在上面的答案中看到的,它们之间可能没有太大区别:

var myObj = new Object();
myObj.setPrecondition(1);
myObj.setOtherPrecondition(2);
myObj.init();

和写作

var myObj = new Object(1,2);

同时具有更大的构造函数。
对于具有15或20个前提条件的对象,有一个论点是,这会使构造函数非常非常难以使用,并且通过将这些东西拉出接口,可以使事情变得更容易看到和记住。 ,这样您就可以看到实例化的工作原理,更高一级。

对象的可选配置是对此的自然扩展。在使对象运行之前,可以选择在接口上设置值。
JS为这个想法提供了一些很棒的捷径,在更强类型的类似c的语言中似乎显得格格不入。

就是说,如果您要处理构造函数中很长的参数列表,则可能是您的对象太大而不能做太多事情。再说一遍,这是个人喜好,并且有很多例外,但是如果您将20件事传递到一个对象中,则很有可能找到一种方法,通过使对象变小来减少该对象的工作。

一个更相关的原因,一个广泛应用的原因是,对象的初始化依赖于当前没有的异步数据。

您知道您需要该对象,因此无论如何都要创建它,但是为了使其正常工作,它需要来自服务器或现在需要加载的另一个文件中的数据。

同样,无论您是将所需的数据传递给一个巨大的init还是建立一个接口对这个概念并不是很重要,这对对象的接口和系统的设计至关重要。

但是就构建对象而言,您可以执行以下操作:

var obj_w_async_dependencies = new Object();
async_loader.load(obj_w_async_dependencies.async_data, obj_w_async_dependencies);

async_loader 可能会传递文件名或资源名称等来加载该资源-可能加载声音文件或图像数据,或者加载保存的字符统计信息...

...然后它将这些数据反馈回obj_w_async_dependencies.init(result);

这种动态现象经常出现在网络应用中。
对于高层应用程序,不一定是在对象的构造中:例如,画廊可能会立即加载和初始化,然后在流进照片时显示照片,这实际上不是异步初始化,但是在大多数情况下会是在JavaScript库中。

一个模块可能依赖于另一个模块,因此该模块的初始化可能会推迟到依赖项的加载完成之前。

就特定于游戏的实例而言,请考虑一个实际的Game类。

我们为什么不能打电话.start.run在构造函数?
需要加载资源-其余所有内容都已经定义好并且可以使用,但是如果我们尝试在没有数据库连接,没有纹理,模型,声音或级别的情况下运行游戏,那将是不可能的。一个特别有趣的游戏...

...那么,我们所看到的典型值之间的区别是什么Game,除了我们给它的“ gogo”方法取一个比.init(或相反,将初始化进一步分开,以分开加载,设置已加载的内容,并在完成所有设置后运行程序)。


2
然后您将手动调用它们,以便您确切地知道程序中发生的时间和位置。 ” C ++中唯一隐式调用析构函数的时间是针对堆栈对象(或全局对象)。堆分配的对象需要显式销毁。因此,始终可以清楚地知道何时释放对象。
Nicol Bolas

6
说您需要这种单独的方法来启用不同武器类型的注入并不是完全准确,或者这是避免子类扩散的唯一方法。您可以通过构造函数传递武器实例!所以这对我来说是-1,因为这不是一个引人注目的用例。
Kylotan

1
-1也来自我,原因与Kylotan差不多。您不会提出非常有说服力的论据,所有这些都可以通过构造函数完成。
Paul Manta

是的,可以使用构造函数和析构函数来完成。他询问了一种技术的用例以及原因和方式,而不是它们如何工作或为什么这样做。拥有一个基于组件的系统,其中具有设置器/绑定方法,而DI的构造函数传递的参数实际上全部取决于您要如何构建接口。但是,如果您的对象需要20个IOC组件,是否要将它们全部放入构造函数中?你可以吗?当然可以。你应该?也许吧,也许不是。如果您选择这样做,那么您是否需要.init,也许不需要,但是可能。嗯,有效的情况。
Norguard

1
@Kylotan我实际上编辑了问题的标题以询问原因。OP仅询问“如何”。我将问题扩展为包括“为什么”,因为“如何”对于任何对编程一无所知的人都是微不足道的(“只需将将要包含在ctor中的逻辑移到一个单独的函数中并调用它”)和“为什么”比较有趣/一般。
2013年

17

无论您读了什么说Init和CleanUp更好,都应该告诉您原因。不证明其主张的文章不值得阅读。

拥有单独的初始化和关闭函数可以使建立和销毁系统变得更加容易,因为您可以选择调用它们的顺序,而构造函数在创建对象时被准确调用,而销毁对象时被销毁。当您在2个对象之间具有复杂的依赖关系时,通常需要在设置它们之前先将它们都存在-但这通常表明在其他地方设计不良。

某些语言没有您可以依赖的析构函数,因为引用计数和垃圾回收使您更难知道何时销毁对象。在这些语言中,几乎总是需要使用shutdown / cleanup方法,并且有些人喜欢为对称添加init方法。


谢谢,但是我主要是在寻找示例,因为本文没有这些示例。如果我的问题不清楚,我深感抱歉,但我现在对其进行了编辑。
Friso

3

我认为最好的原因是:允许合并。
如果您具有Init和CleanUp,则可以在对象被杀死时调用CleanUp,然后将该对象推入相同类型的对象堆栈中:“池”。
然后,无论何时需要新对象,都可以从池中弹出一个对象,或者如果池为空-太糟糕了,则必须创建一个新对象。然后,在此对象上调用Init。
好的策略是在游戏开始时先填充大量“好的”对象来预先填充池,因此您在游戏过程中不必创建任何池化的对象。
另一方面,如果您使用“ new”,并且在对对象无用时停止引用该对象,则会创建必须在某个时间重新收集的垃圾。对于像Javascript这样的单线程语言,这种重新收集尤其是一件坏事,垃圾收集器在评估需要重新收集不再使用的对象的内存时会停止所有代码。游戏会在几毫秒内挂起,并且会破坏游戏体验。
-您已经很好地理解了-:如果您将所有对象合并在一起,则不会发生任何回忆,因此不会再出现随机减速。

调用来自池的对象的init比分配内存+初始化新对象要快得多。
但是,提高速度的重要性不大,因为对象创建通常不是性能瓶颈...除了少数例外,例如疯狂游戏,粒子引擎或使用密集2D / 3d向量进行计算的物理引擎。在这里,使用池极大地提高了速度和垃圾创建。

Rq:如果Init()重设了所有内容,则可能不需要为合并的对象提供CleanUp方法。

编辑:回复这篇文章促使我完成了我写的有关在Javascript中合并的小文章。
如果您有兴趣,可以在这里找到:http :
//gamealchemist.wordpress.com/


1
-1:您不需要这样做就只是拥有一个对象池。您可以通过显式析构函数调用仅通过将new和place的分配与构造分离,将之与delete的释放分配分离来实现。因此,这不是将构造函数/析构函数与某些初始化方法分开的有效理由。
Nicol Bolas

新的展示位置是C ++特有的,也有些深奥。
Kylotan

+1可能以c +的其他方式执行。但这不是其他语言...这可能是我在游戏对象上使用Init方法的唯一原因。
Kikaimaru

1
@Nicol Bolas:我认为你反应过度。还有其他方法可以进行池化(您提到了一种复杂的方法,特定于C ++),这一事实并没有使使用单独的Init是一种实现多种语言的池化的简单好方法的事实无效。在GameDev上,我更喜欢通用的答案。
GameAlchemist 2013年

@VincentPiel:在C ++中如何使用新的布局以及类似的“复杂”元素?另外,如果您使用的是GC语言,则对象包含基于GC的对象的可能性很高。那么,他们也将不得不对每个人进行投票吗?因此,创建新对象将涉及从池中获取一堆新对象。
Nicol Bolas

0

您的问题已被逆转...从历史上讲,更相关的问题是:

为什么 建设+ intialisation合二为一,即为什么,我们做这些步骤分开?当然这与SoC背道而驰吗?

对于C ++,RAII的目的是将资源的获取和释放直接与对象生存期相关联,以希望这可以确保资源的释放。可以?部分地。它是在基于堆栈的/自动变量的上下文中100%满足的,在这种情况下,离开关联的范围会自动调用析构函数/释放这些变量(因此是qualifier ),同时将构造与初始化结合在一起,这对以下方面产生负面影响:automatic)。但是对于堆变量,这个非常有用的模式令人遗憾地崩溃了,因为您仍然被迫显式调用delete以便运行析构函数,并且如果您忘记这样做,您仍然会被RAII试图解决的问题所束缚。在堆分配变量的情况下,然后,C ++提供对C有限的益处(deleteVSfree()

强烈建议使用C构建游戏/模拟的对象系统,因为它将通过更深入地理解C ++和后来的经典OO语言所做出的假设,为RAII和其他此类以OO为中心的模式提供很多启示。 (请记住,C ++最初是用C内置的OO系统)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.