构造函数应该完成多少工作?

53

应该在构造函数中执行需要一定时间的操作还是先构造对象再稍后初始化呢?

例如,在构造表示目录结构的对象时,对象及其子文件夹的填充应在构造函数中完成。显然,一个目录可以包含目录,这些目录又可以包含目录,如此循环。

那么,有没有更优雅的解决方案呢?


1
构造函数就像应用程序安装向导,您只需要进行配置。 - Ramiz Uddin
如果实例已准备好对自身执行任何(可能的)操作,这意味着构造函数运行良好。 - Ramiz Uddin
18个回答

52

总之:

  • 至少,你的构造函数需要将对象配置到其不变条件为真的点。

  • 你选择的不变条件可能会影响你的客户端。(对象承诺始终准备好访问吗?还是只在某些状态下才准备好?)一个处理所有设置的构造函数可能会让类的客户端更加简单。

  • 长时间运行的构造函数本质上并不坏,但在某些情况下可能是不好的。

  • 对于涉及用户交互的系统,任何类型的长时间运行的方法都可能导致响应性差,因此应该避免。

  • 将计算延迟到构造函数之后可能是一种有效的优化;可能没有必要执行全部工作。这取决于应用程序,并且不应过早确定。

  • 总的来说,这取决于具体情况。


25

通常情况下,您不希望构造函数进行任何计算。使用代码的其他人也不会期望它做更多的基本设置。

对于像您所说的目录树,"优雅"的解决方案可能不是在对象构建时构建完整的树形结构。相反,按需构建它。使用您的对象的人可能并不关心子目录中有什么,因此首先只让您的构造函数列出第一层级,然后如果有人想要进入特定的目录,则在请求时构建该部分树形结构。


这是纯粹的逻辑,显式优于隐式,但这也可能被称为太主观了。我们还有什么其他的论据来说服别人我们是正确的呢? - Greg Eremeev

13

时间不应该成为不在构造函数中放置代码的理由。您可以将代码本身放入私有函数中,并从构造函数中调用该函数,以保持构造函数的清晰性。

但是,如果您想要执行的任务不需要给对象定义条件,并且可以在首次使用时稍后完成这些任务,则将其放到后面并稍后完成这些任务是一个合理的选择。但不要让它依赖于您的类的用户:这些事情(按需初始化)必须对您的类的用户完全透明。否则,您的对象的重要不变量可能很容易被破坏。


Litb,我不相信你在暗示构造函数应该放入任意长度的事物——比如枚举目录树.... 对吗? - Foredecker
2
Foredecker:这是关于类的语义:MutexLocker可以等待任意长的时间,直到资源可用,因为这构成了MutexLocker的不变量(析构函数被调用的时间=拥有资源的时间)。 - Johannes Schaub - litb
这是我可以实现的。当前,我在我的构造函数中有一些逻辑,我希望将其移除。 - Teej

10

这要看情况而定(典型的计算机科学答案)。如果您正在为一个长时间运行的程序构建对象,则在构造函数中执行大量工作没有问题。如果这是 GUI 的一部分,那么可能不合适。始终如此,最好的答案是首先尝试最简单的方法,进行性能分析并进行优化。

对于这种特定情况,您可以对子目录对象进行懒惰构造,只为顶级目录的名称创建条目。如果它们被访问,则加载该目录的内容。当用户浏览目录结构时再重复这个过程。


9

构造函数最重要的任务是给对象一个初始有效状态。在我看来,对构造函数最重要的期望是构造函数不应该有任何副作用。


3
“No Side Effects” 这个说法有点过于绝对。例如,一个信号量的构造函数可能会阻塞其他实例完成它们的构造,这是构造函数的工作带来的副作用。另一个构造函数可能会自动为对象分配一个唯一的ID,这个ID是从全局或类拥有的递增计数器中取得的,这也是一个合理的副作用。 - Oddthinking
同意@Oddthinking的观点。另一个例子:如果你有一个像ofstream这样的构造函数,你仍然期望“没有副作用”吗? - João Portela
1
我不同意这个答案。构造函数最重要的工作是给我一个可用的对象(RAII)。 - Emily L.

6
我认为长时间运行的构造函数并不是本质上的坏事。但我认为它们几乎总是错误的选择。我的建议与 Hugo、Rich 和 Litb 的建议类似:
1.将你在构造函数中所做的工作最小化-专注于初始化状态。 2.除非无法避免,否则不要从构造函数中抛出异常。我只尝试抛出 std::bad_alloc 异常。 3.不要调用操作系统或库API,除非你知道它们的作用-大多数API可能会阻塞。它们在开发环境和测试机器上运行得很快,但在实际使用中,它们可能会因为系统正在忙于其他任务而被阻塞很长一段时间。 4.永远不要在构造函数中进行任何类型的I/O操作。I/O通常会受到各种非常长的延迟(100毫秒至几秒钟)。I/O包括:
磁盘I/O 任何使用网络的东西(即使间接地)记住,大多数资源可以脱机。
例如:许多硬盘存在一个问题,即它们进入一种状态,不服务于读取或写入,持续100毫秒甚至数千毫秒。第一代固态驱动器经常出现这种情况。用户无法知道你的程序刚刚挂起了一会儿-他们只认为这是你的有缺陷的软件。
当然,长时间运行的构造函数的邪恶程度取决于两个因素:
1.什么是“长” 2.在给定时间内使用具有“长”构造函数的对象的频率
如果“长”只是多出几百个时钟周期的工作,那么它并不算长。但是,如果一个构造函数达到了几百微秒的范围,我建议它相当长。当然,如果你只实例化其中之一,或者很少实例化它们(例如每几秒钟实例化一个),那么你不太可能因为持续时间在这个范围内而遇到问题。
频率是一个重要因素,如果你只构建少量对象,500微秒的构造函数不是问题:但如果创建了一百万个对象,将会导致显著的性能问题。
现在,让我们来谈谈你的例子:在“class Directory”对象中填充目录对象树。(请注意,我假设这是一个带有图形用户界面的程序)。在这里,你的 CTOR 持续时间不取决于你编写的代码-而取决于枚举任意大的目录树所需的时间。这在本地硬盘上已经够糟糕了。在远程(网络)资源上更加棘手。
现在,想象一下在用户界面线程中执行此操作-你的用户界面将停止数秒、十数秒甚至可能几分钟。在 Windows 中,我们称之为 UI hang。它们非常糟糕(是的,我们有它们……是的,我们努力消除它们)。
UI Hangs 是会让人真正讨厌你的软件的东西。

在这里应该做的正确方式是简单地初始化您的目录对象。使用可以取消并保持UI响应状态的循环构建您的目录树(取消按钮应始终起作用)。


1
这个建议在设计 GUI 应用程序时完全适用,但并不是所有东西都适合这种模式。我编写大量的、长时间运行的数值计算代码,这个建议在这种情况下是错误的。 - KeithB
我并不反对 - 我非常清楚地表明了我的评论主要适用于需要响应的用户界面和代码。当然,并不是所有东西都适用于一个模具 - 很少有“唯一正确的答案”。 - Foredecker

4

为了便于代码维护、测试和调试,我尽量避免在构造函数中放置任何逻辑。如果您喜欢从构造函数中执行逻辑,那么将逻辑放入init()等方法中并从构造函数中调用init()将会很有帮助。如果您计划开发单元测试,应该避免在构造函数中放置任何逻辑,因为这可能很难测试不同的情况。我认为之前的评论已经解决了这个问题,但是……如果您的应用程序是交互式的,则应避免调用单个导致明显性能损失的函数。如果您的应用程序是非交互式的(例如:夜间批处理作业),那么单个性能损失并不是很重要。


3

历史上,我编写构造函数时通常会使对象在构造函数方法完成后立即可用。涉及的代码多少取决于对象的要求。

例如,假设我需要在详细视图中显示以下公司类:

public class Company
{
    public int Company_ID { get; set; }
    public string CompanyName { get; set; }
    public Address MailingAddress { get; set; }
    public Phones CompanyPhones { get; set; }
    public Contact ContactPerson { get; set; }
}

由于我想在详细视图中显示有关公司的所有信息,因此我的构造函数将包含填充每个属性所需的所有代码。 鉴于这是一个复杂类型,因此Company构造函数也将触发Address、Phones和Contact构造函数的执行。

现在,如果我正在填充目录列表视图,在那里我可能只需要CompanyName和主要电话号码,我可以在类上拥有第二个构造函数,该构造函数仅检索该信息并将其余信息留空,或者我可以创建一个单独的对象,仅包含该信息。 这实际上取决于从何处检索信息。

无论类上有多少个构造函数,我的个人目标是进行必要的处理,以准备对象承担任何可能加于它的任务。


2

RAII是C++资源管理的支柱,因此在构造函数中获取所需资源,在析构函数中释放它们。这时候你需要建立你的类不变式。如果需要时间,就花时间。你有越少的“如果X存在则执行Y”结构,设计该类就会更简单。后来,如果分析显示这是个问题,请考虑像延迟初始化(在首次需要资源时获取资源)这样的优化。


2
关于构造函数应该完成多少工作,我认为应该考虑到操作的速度、类的使用方式以及个人感受。
关于目录结构对象:我最近为我的HTPC实现了一个samba(Windows共享)浏览器,由于速度非常慢,所以我选择只有在触摸目录时才初始化目录。例如,首先树仅由机器列表组成,然后每当您浏览到目录时,系统将自动从该机器初始化树并获取更深层次的目录列表,依此类推。
理想情况下,我认为甚至可以将其推广到编写一个工作线程,以广度优先方式扫描目录,并优先考虑当前正在浏览的目录,但通常对于简单的事情来说这太麻烦了;)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接