为什么使用初始化方法而不是构造函数?
我刚进入一个新的公司,许多代码库使用初始化方法而不是构造函数。
struct MyFancyClass : theUberClass { MyFancyClass(); ~MyFancyClass(); resultType initMyFancyClass(fancyArgument arg1, classyArgument arg2, redundantArgument arg3=TODO); // several fancy methods... };
他们告诉我,这与时间有关。 有些事情必须在施工之后完成, 在构造函数中失败。 但是大多数构造函数都是空的,我没有看到任何不使用构造函数的理由。
所以我转向你,哦向导的C + +:为什么你会用一个init方法,而不是一个构造函数?
由于他们说“时机”,我想这是因为他们希望他们的init函数能够调用对象的虚函数。 这并不总是在构造函数中工作,因为在基类的构造函数中,对象的派生类部分“还不存在”,特别是不能访问在派生类中定义的虚函数。 相反,函数的基类版本被调用,如果定义的话。 如果没有定义(暗示函数是纯虚函数),则会出现未定义的行为。
初始化函数的另一个常见原因是希望避免exception,但这是一个相当古老的编程风格(是否是一个好主意是它自己的一个完整的论点)。 它与无法在构造函数中工作的东西无关,而是与构造函数在失败时不能返回错误值的事实有关。 所以,如果你的同事给了你真正的理由,我想这不是事实。
是的,我可以想到几个,但通常这不是一个好主意。
大多数情况下,调用的原因是您只通过构造函数中的exception来报告错误(这是正确的),而使用经典方法则可以返回错误代码。
然而,在正确devise的OO代码中,构造函数负责build立类不variables。 通过允许一个默认的构造函数,你允许一个空的类,因此你必须修改不variables,以便接受“null”类和“有意义的”类…每个类的使用必须首先确保对象已经被正确地build立起来了…这很愚蠢。
所以,现在让我们来揭穿“原因”:
- 我需要使用一个
virtual
方法:使用虚拟构造函数成语。 - 有很多工作要做,那么工作将会做什么,只要在构造函数中完成
- 安装程序可能会失败:抛出exception
- 我想保留部分初始化的对象:在构造函数中使用try / catch,并在对象字段中设置错误原因,不要忘记在每个公共方法的开始处
assert
,以确保对象在尝试用它。 - 我想重新初始化我的对象:从构造函数调用初始化方法,你将避免重复的代码,同时仍然有一个完全初始化的对象
- 我想重新初始化我的对象(2):使用
operator=
(如果编译器生成的版本不适合你的需要,使用copy和swap idiom来实现它)。
正如所说的,一般来说,糟糕的主意。 如果你真的想要“无效”构造函数,使他们private
和使用Builder方法。 这与NRVO一样高效…并且在构造失败的情况下,您可以返回boost::optional<FancyObject>
。
其他人列出了很多可能的原因(并解释了为什么大多数这些通常不是一个好主意)。 让我发表一个(或多或less)有效使用init方法的例子,这实际上与时间有关 。
在之前的项目中,我们有很多的Service类和对象,每个类都是层次结构的一部分,并以各种方式交叉引用。 因此,通常,为了创buildServiceA,您需要一个父服务对象,这个对象又需要一个服务容器,它在初始化时已经依赖于某些特定服务(可能包括ServiceA本身)的存在。 原因是在初始化过程中,大多数服务将其他服务注册为特定事件的侦听器,并且/或者通知其他服务有关初始化成功的事件。 如果在通知时其他服务不存在,则注册不会发生,因此在应用程序的使用过程中,该服务将不会收到重要消息。 为了打破循环依赖链 ,我们必须使用与构造函数分离的显式初始化方法,从而有效地使全局服务初始化成为一个两阶段的过程 。
所以,虽然这个习惯用法一般不应该遵循,恕我直言,它有一些有效的用途。 但是,最好尽可能地限制它的使用,尽可能使用构造函数。 在我们的例子中,这是一个遗留项目,我们还没有完全理解它的架构。 至lessinit方法的用法仅限于服务类 – 常规类是通过构造函数初始化的。 我相信可能有一种方法来重构这个架构,以消除对init方法的需求,但至less我没有看到如何去做(坦率地说,当时我们遇到了更紧迫的问题项目的一部分)。
我能想到的两个理由是:
- 说创build一个对象涉及到许多繁琐的工作,可能会导致许多可怕的和微妙的方式失败。 如果你使用一个简短的构造函数来创build不会失败的rudamentary的东西,然后让用户调用一个初始化方法来完成这个大任务,那么至less可以确定你创build了一个对象,即使这个大任务失败。 也许这个对象包含了有关init初始化失败的信息,或者由于其他原因,保留失败的初始化对象也是很重要的。
- 有时您可能希望在创build对象后很长时间重新初始化对象。 这样,只需再次调用初始化方法而不破坏和重新创build对象。
对象池中还可以使用这种初始化。 基本上你只是从池中请求对象。 池中已经创build了一些空白的N个对象。 现在可以调用任何他/她喜欢设置成员的方法。 一旦来电者完成了对象,它会告诉游泳池去除它。 优点是直到对象被使用时,内存将被保存,调用者可以使用它自己合适的初始化对象的成员方法。 一个对象可能有很多用途,但调用者可能不需要全部,也可能不需要初始化对象的所有成员。
通常想到数据库连接。 一个池可以有一堆连接对象,并且调用者可以填充用户名,密码等。
当你的编译器不支持exception,或者你的目标应用程序不能使用堆时,init()函数是很好的(exception通常是用一个堆实现来创build和销毁的)。
当需要定义构造的顺序时,init()例程也很有用。 也就是说,如果全局分配对象,则未定义调用构造函数的顺序。 例如:
[file1.cpp] some_class instance1; //global instance [file2.cpp] other_class must_construct_before_instance1; //global instance
该标准不保证must_construct_before_instance1的构造函数将在instance1的构造函数之前被调用。 当绑定到硬件时,初始化的顺序可能是至关重要的。
而且我也喜欢附上代码示例来回答#1 –
由于msdn也说:
当调用虚拟方法时,直到运行时才会select执行该方法的实际types。 当一个构造函数调用一个虚拟方法时,可能调用该方法的实例的构造函数没有执行。
示例:以下示例演示了违反此规则的效果。 testing应用程序创build一个DerivedType的实例,这会导致其基类(BadlyConstructedType)构造函数执行。 BadlyConstructedType的构造函数错误地调用了虚方法DoSomething。 如输出所示,DerivedType.DoSomething()执行,并在DerivedType的构造函数执行之前执行。
using System; namespace UsageLibrary { public class BadlyConstructedType { protected string initialized = "No"; public BadlyConstructedType() { Console.WriteLine("Calling base ctor."); // Violates rule: DoNotCallOverridableMethodsInConstructors. DoSomething(); } // This will be overridden in the derived type. public virtual void DoSomething() { Console.WriteLine ("Base DoSomething"); } } public class DerivedType : BadlyConstructedType { public DerivedType () { Console.WriteLine("Calling derived ctor."); initialized = "Yes"; } public override void DoSomething() { Console.WriteLine("Derived DoSomething is called - initialized ? {0}", initialized); } } public class TestBadlyConstructedType { public static void Main() { DerivedType derivedInstance = new DerivedType(); } } }
输出:
调用基地ctor。
派生DoSomething被称为 – 初始化? 没有
调用派生的ctor。
更多的特殊情况:如果你创build了一个监听器,你可能想要在某个地方注册(例如使用单例或者GUI)。 如果你在构造函数中这样做了,它会泄漏一个指针/引用给自己,因为构造函数还没有完成,甚至可能完全失败。 假设收集所有侦听器并在事件发生时收到事件并发送事件,然后通过侦听器列表(其中一个是我们正在讨论的实例)循环发送每个消息的单例。 但是这个实例在它的构造函数中仍然是中间的,所以这个调用可能会以各种不好的方式失败。 在这种情况下,在一个单独的函数中注册是有意义的,你显然不会从构造函数本身调用(这将完全破坏目的),而是在构build完成后从父对象中调用。
但这是一个具体的情况,而不是一般的情况。
这对于做资源pipe理很有用。 假设你有一个具有析构函数的类来在对象的生命周期结束时自动释放资源。 假设你也有一个拥有这些资源类的类,并且在这个上层类的构造函数中启动它们。 当你使用赋值操作符来启动这个更高级的类时会发生什么? 一旦内容被复制,旧的高级类就会脱离上下文,并且为所有的资源类调用析构函数。 如果这些资源类具有在赋值过程中被复制的指针,那么所有这些指针现在都是坏指针。 如果在上层类的初始化函数中启动资源类,则完全绕过资源类的析构函数,因为赋值操作符不需要创build和删除这些类。 我相信这是“时间”要求的意思。
如果初始化程序需要在创build类之后调用,则使用初始化方法而不是构造函数。 所以如果A类被创build为:
A *a = new A;
而A类的初始化程序需要设置,那么显然你需要这样的东西:
A *a = new A; a->init();