构造函数通常不应调用方法


12

我向同事描述了为什么调用方法的构造函数可以是反模式。

示例(在我生锈的C ++中)

class C {
public :
    C(int foo);
    void setFoo(int foo);
private:
    int foo;
}

C::C(int foo) {
    setFoo(foo);
}

void C::setFoo(int foo) {
    this->foo = foo
}

我想通过您的额外贡献更好地激发这一事实。如果您有示例,书籍参考,博客页面或原则名称,将非常欢迎他们。

编辑:我一般来说,但我们在python中编码。


这是一般规则还是特定于特定语言?
克里斯·

哪种语言?在C ++中,它不仅仅是一种反模式:parashift.com/c++
faq

@ Lenny222,OP谈论“类方法”,至少对我来说,这意味着非实例方法。因此,这不能是虚拟的。
彼得Török

3
@Alb在Java中完全可以。但是,您不应该做的是将其显式传递this给您从构造函数调用的任何方法。
biziclop 2011年

3
@Stefano Borini:如果您使用Python编写代码,为什么不使用Python而不是生锈的C ++显示示例?另外,请解释为什么这是一件坏事。我们一直在做。
S.Lott

Answers:


26

您尚未指定语言。

在C ++中,构造函数在调用虚拟函数时必须当心,因为它调用的实际函数是类实现。如果它是没有实现的纯虚拟方法,则将是访问冲突。

构造函数可以调用非虚拟函数。

如果您的语言是Java,默认情况下函数通常是虚拟的,那么您必须格外小心。

C#似乎以您期望的方式处理这种情况:您可以在构造函数中调用虚拟方法,并且它会调用最最终的版本。因此,在C#中不是反模式。

从构造函数调用方法的常见原因是,您有多个构造函数要调用通用的“ init”方法。

请注意,析构函数将与虚拟方法存在相同的问题,因此您不能拥有位于析构函数之外的虚拟“清理”方法,并期望它被基类析构函数调用。

Java和C#没有析构函数,它们有终结器。我不知道Java的行为。

在这方面,C#似乎可以正确处理清理。

(请注意,尽管Java和C#具有垃圾回收,但仅管理内存分配。析构函数需要执行的其他清理操作不会释放内存)。


13
这里有一些小错误。默认情况下,C#中的方法不是虚拟的。在构造函数中调用虚拟方法时,C#的语义不同于C ++。最派生类型的虚拟方法将被调用,而不是当前正在构造的部分类型的虚拟方法。C#确实将其终结方法称为“析构函数”,但您说得对,它们具有终结器的语义是正确的。在C#析构函数中调用的虚拟方法的工作方式与在构造函数中调用的方法相同。最派生的方法称为。
埃里克·利珀特

@Péter:我打算使用实例方法。对困惑感到抱歉。
Stefano Borini

1
@埃里克·利珀特 感谢您在C#方面的专业知识,我对答案做了相应的编辑。我对这种语言一无所知,我非常了解C ++,不太了解Java。
CashCow 2011年

5
别客气。请注意,在C#中的基类构造函数中调用虚拟方法仍然不是一个好主意。
埃里克·利珀特

如果从构造函数中调用Java中的(虚拟)方法,它将始终调用派生最多的重写。但是,您所谓的“期望方式”就是我所说的令人困惑的地方。因为尽管Java确实调用了最派生的重写,但是该方法将仅看到已处理的归档初始化程序,而不会运行其自己的类的构造函数。在尚未建立其不变式的类上调用方法可能很危险。因此,我认为C ++在这里是更好的选择。
5gon12eder 2016年

18

好的,既然有关类方法实例方法的混淆已经消除,我可以给出一个答案:-)

问题不在于通常从构造函数调用实例方法。调用虚拟方法(直接或间接)。主要原因是,在构造函数内部时,对象尚未完全构造。特别是在执行基类构造函数时,根本不会构造其子类部分。因此,其内部状态在语言依赖方式上是不一致的,这可能会导致使用不同语言产生不同的细微错误。

其他人已经讨论过C ++和C#。在Java中,将调用派生类型最多的虚拟方法,但是该类型尚未初始化。因此,如果该方法使用派生类型中的任何字段,则这些字段可能在该时间点尚未正确初始化。在Effecive Java 2nd Edition,第17项:继承的设计和文档中详细讨论了此问题,否则将禁止它

请注意,这是过早发布对象引用的一般问题的特例。实例方法具有隐式this参数,但是this显式传递给方法会导致类似的问题。特别是在并发程序中,如果对象引用过早地发布到另一个线程,则该线程可以在第一个线程的构造函数完成之前调用它的方法。


3
(+1)“在构造函数内部时,对象尚未完全构造。” 与“类方法与实例”相同。一些编程语言家认为它是在进入构造器时构造的,就像程序员在其中将值分配给构造器一样。
umlcat 2011年

7

我不认为这里的方法调用本身就是反模式,更多是代码气味。如果类提供了一种reset方法,该方法将对象返回其原始状态,则reset()在构造函数中的调用为DRY。(我没有对复位方法发表任何声明)。

这是一篇可以帮助您满足权威要求的文章:http : //misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

它并不是真正地关于调用方法,而是关于做太多事情的构造函数。恕我直言,在构造函数中调用方法是一种气味,可能表明构造函数太重。

这与测试代码的难易程度有关。原因包括:

  1. 单元测试涉及大量的创建和销毁-因此构建应该很快。

  2. 根据那些方法的作用,可能不依赖于构造函数中设置的某些(可能无法测试的)前提条件而难以测试离散的代码单元(例如,从网络获取信息)。


3

从哲学上讲,构造函数的目的是将原始的内存块转换为实例。在执行构造函数时,该对象尚不存在,因此调用其方法是一个坏主意。您可能根本不知道它们在内部做什么,并且当它们被调用时,他们可能理所当然地认为该对象至少存在(duh!)。

从技术上讲,这可能没有什么问题,在C ++中,尤其是在Python中,请务必谨慎。

实际上,您应该将调用限制为仅初始化类成员的此类方法。


2

这不是一个通用的问题。这在C ++中是一个问题,特别是在使用继承和虚拟方法时,因为对象构造是向后发生的,并且vtable指针会在继承层次结构中的每个构造函数层重置,所以如果您调用虚拟方法,则可能不会最终得到一个实际上与您要创建的类相对应的类,这违背了使用虚拟方法的全部目的。

在具有健全的OOP支持的语言中,从一开始就正确设置了vtable指针,则不存在此问题。


2

调用方法有两个问题:

  • 调用虚拟方法,该方法可以执行意外的操作(C ++)或使用尚未初始化的部分对象
  • 调用一个公共方法(应强制执行类不变式),因为该对象不一定是完整的(因此它的不变式可能不成立)

调用辅助函数没有任何问题,只要在前两种情况下都没有。


1

我不买这个。在面向对象的系统中,调用方法几乎是您唯一可以做的事情。实际上,这或多或少是“面向对象” 的定义。所以,如果一个构造函数不能调用任何方法,那么什么可以做什么呢?


初始化对象。
Stefano Borini

@Stefano Borini:怎么样?在面向对象的系统中,您唯一可以做的就是调用方法。或者从相反的角度来看它:任何事情都可以通过调用方法来完成。而且“任何”显然都包括对象初始化。因此,如果为了初始化对象而需要调用方法,而构造函数无法调用方法,那么构造函数如何初始化对象?
约尔格W¯¯米塔格

唯一不可以做的就是调用方法,这是绝对不正确的。您可以直接对对象内部进行任何调用而无需初始化任何状态...构造函数的重点是使对象处于一致状态。如果调用其他方法,这些方法可能会在部分状态下处理对象时遇到麻烦,除非它们是专门从构造函数调用的方法(通常是辅助方法)
Stefano Borini

@Stefano Borini:“您可以直接对对象的内部进行初始化而无需任何调用。” 可悲的是,当涉及一种方法时,您会怎么做?复制并粘贴代码?
S.Lott

1
@ S.Lott:不,我叫它,但我尝试将其保留为模块函数而不是对象方法,并让它提供返回的数据,我可以将其放入构造函数中的对象状态。如果确实需要对象方法,则将其设为私有,并阐明它是用于初始化的,例如为其指定适当的名称。但是,我绝不会调用公共方法来从构造函数中设置对象状态。
Stefano Borini

0

在OOP理论中,这无关紧要,但是在实践中,每种OOP编程语言都会处理不同的构造函数。我不经常使用静态方法。

在C ++和Delphi中,如果必须为某些属性(“字段成员”)赋予初始值,并且代码已非常扩展,我将添加一些辅助方法作为构造函数的扩展。

而且不要调用其他方法来做更复杂的事情。

对于属性“ getters”和“ setters”方法,我通常使用私有/受保护的变量来存储其状态,以及“ getters”和“ setters”方法。

在构造函数中,我将“默认”值分配给属性状态字段,而无需调用“访问器”。

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.