代码之家  ›  专栏  ›  技术社区  ›  bastibe

为什么使用初始化方法而不是构造函数?

  •  45
  • bastibe  · 技术社区  · 14 年前

    我刚进入一家新公司,很多代码库使用初始化方法而不是构造函数。

    struct MyFancyClass : theUberClass
    {
        MyFancyClass();
        ~MyFancyClass();
        resultType initMyFancyClass(fancyArgument arg1, classyArgument arg2, 
                                    redundantArgument arg3=TODO);
        // several fancy methods...
    };
    

    他们告诉我这和时间有关。有些事情必须要做 之后 在构造函数中失败的构造。但是大多数构造函数都是空的,我真的不认为有什么理由不使用构造函数。

    10 回复  |  直到 14 年前
        1
  •  71
  •   Steve Jessop    14 年前

    既然他们说“计时”,我猜是因为他们希望自己的init函数能够调用对象上的虚函数。这并不总是在构造函数中起作用,因为在基类的构造函数中,对象的派生类部分“还不存在”,尤其是不能访问派生类中定义的虚拟函数。相反,如果已定义,则调用函数的基类版本。如果没有定义(意味着函数是纯虚拟的),则会得到未定义的行为。

    使用init函数的另一个常见原因是希望避免异常,但这是一种非常古老的编程风格(这是否是一个好主意完全取决于它自己)。它与不能在构造函数中工作的东西无关,而是与如果某个东西失败,构造函数不能返回错误值这一事实有关。既然你的同事给了你真正的理由,我想这不是原因。

        2
  •  33
  •   Matthieu M.    14 年前

    是的,我能想到几个,但总的来说这不是个好主意。

    然而,在正确设计的OO代码中,构造函数负责建立类不变量。通过允许一个默认构造函数,您就允许一个空类,因此您必须修改不变量,这样“null”类和“有意义”类都可以接受。。。每次使用类都必须首先确保对象已正确构建。。。太粗鲁了。

    那么现在,让我们揭穿“理由”:

    • 我需要一个 virtual
    • 有很多工作要做:那又怎样,工作无论如何都要做,就在构造器里做吧
    • 我想保留部分初始化的对象:在构造函数中使用try/catch并在对象字段中设置错误原因,不要忘记 assert
    • 我想重新初始化我的对象:从构造函数中调用初始化方法,这样可以避免重复代码,同时仍然有一个完全初始化的对象
    • 我想重新初始化我的对象(2):使用 operator= (如果编译器生成的版本不适合您的需要,请使用复制和交换习惯用法来实现它)。

    private 并使用生成器方法。NRVO同样有效。。。你可以回来 boost::optional<FancyObject> 以防施工失败。

        3
  •  17
  •   Péter Török    14 年前

    其他人列出了许多可能的原因(以及为什么这些通常不是好主意的适当解释)。我来举一个例子 .

    在以前的一个项目中,我们有很多服务类和对象,它们都是层次结构的一部分,并且以各种方式相互交叉引用。因此,通常,为了创建ServiceA,您需要一个父服务对象,而父服务对象又需要一个服务容器,该容器在初始化时已经依赖于某些特定服务(可能包括ServiceA本身)的存在。原因是,在初始化过程中,大多数服务向其他服务注册,作为特定事件的侦听器,和/或向其他服务通知初始化成功的事件。如果在发出通知时另一个服务不存在,则注册不会发生,因此该服务在应用程序使用期间不会在以后接收重要消息。为了 打破循环依赖链 ,我们必须使用独立于构造函数的显式初始化方法,因此 使全局服务初始化成为一个两阶段的过程 .

    因此,虽然这个成语不应该被普遍遵循,但它有一些有效的用法。但是,最好将其使用限制到最低限度,尽可能使用构造函数。在我们的例子中,这是一个遗留项目,我们还没有完全理解它的体系结构。至少init方法的使用仅限于服务类-常规类是通过构造函数初始化的。我相信有一种方法可以重构该体系结构,以消除对服务初始化方法的需求,但至少我没有看到如何做到这一点(坦率地说,在我参与项目时,我们有更紧迫的问题要处理)。

        4
  •  8
  •   gspr    14 年前

    有两个原因我想得很清楚:

    • 有时您可能需要在对象创建很久之后重新初始化它。这样,只需再次调用初始化方法,而无需销毁和重新创建对象。
        5
  •  5
  •   Manoj R    14 年前

    可以在对象池中再次使用这种初始化。基本上,您只需从池中请求对象。该池将已经创建了一些空的N个对象。现在是调用者可以调用他/她喜欢的任何方法来设置成员。一旦调用者处理完对象,它就会告诉池销毁它。这样做的好处是,在对象被使用之前,内存将被保存,调用者可以使用自己合适的成员方法初始化对象。一个对象可能有很多用途,但调用者可能不需要全部,也可能不需要初始化对象的所有成员。

        6
  •  5
  •   TRISAbits    10 年前

    当编译器不支持异常,或者目标应用程序不能使用堆(通常使用堆来创建和销毁异常)时,init()函数是很好的。

    当需要定义构造顺序时,init()例程也很有用。也就是说,如果全局分配对象,则不会定义调用构造函数的顺序。例如:

    [file1.cpp]
    some_class instance1; //global instance
    
    [file2.cpp]
    other_class must_construct_before_instance1; //global instance
    

    本标准不保证 的构造函数将在 实例1 的构造函数。当它与硬件联系在一起时,事物初始化的顺序是至关重要的。

        7
  •  1
  •   Tarik    14 年前

    我还想附上一个代码样本来回答#1--

    因为msdn也说:

    调用虚方法时 执行方法的实际类型 构造函数调用一个虚拟方法,它 有可能 调用方法的实例

    例子: 下面的示例演示了违反此规则的效果。测试应用程序创建DerivedType的实例,从而执行其基类(BadlyConstructedType)构造函数。BadlyConstructedType的构造函数错误地调用了虚拟方法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();
            }
        }
    }
    

    输出:

    导出的剂量测量称为-初始化?不

        8
  •  1
  •   Kajetan Abt    14 年前

    更特殊的情况是:如果您创建了一个侦听器,您可能希望让它在某个地方注册自己(例如使用单例或GUI)。如果在构造函数中这样做,它会泄漏一个指向自身的指针/引用,这还不安全,因为构造函数还没有完成(甚至可能完全失败)。 假设singleton收集所有侦听器,并在发生事件时向它们发送事件receives和event,然后循环遍历侦听器列表(其中一个就是我们正在讨论的实例),向每个侦听器发送一条消息。但是这个实例在其构造函数中仍然处于中间位置,因此调用可能会以各种不好的方式失败。在这种情况下,将注册放在一个单独的函数中是有意义的,您显然是这样做的 从构造函数本身调用(这将完全破坏目的),但在构造完成后从父对象调用。

    但这是一个具体的情况,而不是一般的情况。

        9
  •  1
  •   Christopher    12 年前

        10
  •  0
  •   Swiss Frank    5 年前

    还有几个案例:

    例如,假设我们有一个初始值设定项,它接受一个常规arg的列表。我们有另一个初始值设定项,它接受一个名=值对的字典。第二个可以在字典中查询第一个初始值设定项接受的参数,并用它们调用第一个参数。

    当初始值设定项是init方法时,这很好,但当初始值设定项是构造函数时则不行。

    鸡肉还是鸡蛋

    我们可能有一个car类,其初始值设定项必须有一个指向motor对象的指针,motor类初始值设定项必须有一个指向其car对象的指针。这对于构造函数来说是不可能的,但是对于init方法来说却是微不足道的。

    分解ARG列表

    可能有大量的arg 能够

    同样,分解一个构造函数是不可能的,但分解一个初始值设定项是微不足道的。

        11
  •  -2
  •   Alexander Rafferty    14 年前

    如果在创建类之后需要调用初始化器,则可以使用初始化方法而不是构造函数。因此,如果类A被创建为:

    A *a = new A;
    

    A *a = new A;
    a->init();