51

应该在构造函数中执行可能需要一些时间的操作,还是应该构造对象然后稍后初始化。

例如,在构造表示目录结构的对象时,应在构造函数中完成对象及其子对象的填充。显然,一个目录可以包含目录,而目录又可以包含目录等等。

什么是优雅的解决方案?

4

18 回答 18

52

总结一下:

  • 至少,您的构造函数需要将对象配置到其不变量为真的程度。

  • 您对不变量的选择可能会影响您的客户。(对象是否承诺随时准备好访问?还是仅在某些状态下?)预先处理所有设置的构造函数可能会使生活更简单为班级的客户。

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

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

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

  • 总的来说,这取决于。

于 2008-11-16T15:35:59.620 回答
26

您通常不希望构造函数进行任何计算。使用该代码的其他人不会期望它所做的不仅仅是基本设置。

对于您正在谈论的目录树,“优雅”的解决方案可能不是在构造对象时构建完整的树。相反,按需构建它。使用您的对象的人可能并不真正关心子目录中的内容,因此首先让您的构造函数列出第一级,然后如果有人想进入特定目录,然后在他们请求时构建树的那部分它。

于 2008-11-16T15:18:34.337 回答
13

所需的时间不应成为不将某些东西放入构造函数的理由。您可以将代码本身放入一个私有函数中,然后从构造函数中调用它,以保持构造函数中的代码清晰。

但是,如果您想要做的事情不需要给对象一个定义的条件,并且您可以在第一次使用时稍后做这些事情,那么这将是一个合理的论据,可以将它放在以后再做。但是不要让它依赖于你的类的用户:这些东西(按需初始化)必须对你的类的用户完全透明。否则,对象的重要不变量可能很容易被破坏。

于 2008-11-16T15:19:52.743 回答
10

这取决于(典型的 CS 答案)。如果您在启动时为长时间运行的程序构造对象,那么在构造函数中做大量工作是没有问题的。如果这是期望快速响应的 GUI 的一部分,则可能不合适。与往常一样,最好的答案是先尝试最简单的方法,然后从那里进行分析和优化。

对于这种特定情况,您可以对子目录对象进行惰性构造。仅为顶级目录的名称创建条目。如果它们被访问,则加载该目录的内容。在用户探索目录结构时再次执行此操作。

于 2008-11-16T15:23:09.897 回答
8

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

于 2008-11-16T16:01:56.213 回答
6

我同意长期运行的构造函数本质上并不坏。但我会争辩说,你几乎总是做错事。我的建议与 Hugo、Rich 和 Litb 的建议类似:

  1. 将您在构造函数中所做的工作保持在最低限度 - 将注意力集中在初始化状态上。
  2. 除非无法避免,否则不要从构造函数中抛出。我尝试只抛出 std::bad_alloc。
  3. 除非您知道它们的作用,否则不要调用操作系统或库 API - 大多数都可以阻止。它们将在您的开发盒和测试机器上快速运行,但在现场它们可能会因为系统忙于做其他事情而被长时间阻塞。
  4. 永远,永远不要在构造函数中进行 I/O - 任何类型的。I/O 通常会受到各种非常长的延迟(数百毫秒到秒)的影响。I/O 包括
    • 磁盘 I/O
    • 任何使用网络的东西(即使是间接的)请记住,大多数资源都可以是现成的。

I/O 问题示例:许多硬盘存在问题,即它们进入不服务读取或写入的状态达 100 毫秒甚至数千毫秒。第一代和第一代固态硬盘经常这样做。用户现在可以知道您的程序只是挂了一点-他们只是认为这是您的错误软件。

当然,长时间运行的构造函数的邪恶取决于两件事:

  1. “长”是什么意思
  2. 在给定时间段内构造具有“长”构造函数的对象的频率。

现在,如果“长”只是几个 100 个额外时钟周期的工作,那么它并不是很长。但是构造函数正在进入 100 微秒的范围,我认为它很长。当然,如果您只实例化其中一个,或者很少实例化它们(比如每隔几秒一次),那么由于持续时间在此范围内,您不太可能看到问题。

频率是一个重要因素,如果您只构建其中的几个,500 us ctor 不是问题:但创建一百万个会带来严重的性能问题。

让我们谈谈您的示例:在“类目录”对象中填充目录对象树。(注意,我假设这是一个带有图形用户界面的程序)。在这里,您的 CTOR 持续时间不取决于您编写的代码 - 它的被告取决于枚举任意大目录树所需的时间。这在本地硬盘上已经够糟糕的了。它在远程(网络)资源上的问题更大。

现在,想象一下在您的用户界面线程上执行此操作 - 您的 UI 将停止在其轨道上几秒钟、几十秒钟甚至几分钟。在 Windows 中,我们称之为 UI 挂起。它们很糟糕(是的,我们有它们……是的,我们努​​力消除它们)。

UI 挂起会让人非常讨厌你的软件。

正确的做法是简单地初始化您的目录对象。在一个可以取消的循环中构建您的目录树并使您的 UI 保持响应状态(取消按钮应该始终有效)

于 2008-11-16T17:15:54.507 回答
4

为了代码维护、测试和调试,我尽量避免将任何逻辑放在构造函数中。如果您更喜欢从构造函数执行逻辑,那么将逻辑放在诸如 init() 之类的方法中并从构造函数调用 init() 会很有帮助。如果您计划开发单元测试,则应避免将任何逻辑放在构造函数中,因为可能难以测试不同的案例。我认为之前的评论已经解决了这个问题,但是......如果您的应用程序是交互式的,那么您应该避免进行导致明显性能下降的单个调用。如果您的应用程序是非交互式的(例如:夜间批处理作业),那么单一的性能影响并不是什么大问题。

于 2008-11-16T15:48:43.557 回答
3

从历史上看,我已经对构造函数进行了编码,以便一旦构造函数方法完成,对象就可以使用了。涉及多少或多少代码取决于对象的要求。

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

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 和主要电话号码,我可能在类上有第二个构造函数,它只检索该信息并将剩余信息留空,或者我可能只是创建一个单独的对象,只保存该信息。这实际上仅取决于如何检索信息以及从何处检索信息。

不管一个类上有多少个构造函数,我个人的目标是做任何必要的处理,为对象准备好可能被强加给它的任何任务。

于 2010-01-05T08:02:49.753 回答
2

至于在构造函数中应该做多少工作,我想说它应该考虑到事情有多慢,你将如何使用这个类,以及你个人对它的总体感受。

在您的目录结构对象上:我最近为我的 HTPC 实现了一个 samba(windows 共享)浏览器,由于速度非常慢,我选择只在目录被触摸时才实际初始化它。例如,首先树只包含一个机器列表,然后每当您浏览到一个目录时,系统会自动从该机器初始化树并获取更深一层的目录列表,依此类推。

理想情况下,我认为您甚至可以编写一个工作线程来扫描目录广度优先,并优先考虑您当前正在浏览的目录,但通常这对于简单的事情来说工作量太大了;)

于 2008-11-16T15:19:30.387 回答
2

确保 ctor 不做任何可能引发异常的事情。

于 2008-11-16T16:29:53.833 回答
2

RAII 是 C++ 资源管理的支柱,所以在构造函数中获取你需要的资源,在析构函数中释放它们。

这是您建立类不变量的时候。如果需要时间,那就需要时间。您拥有的“如果 X 存在则做 Y”构造越少,该类的其余部分的设计就越简单。稍后,如果分析显示这是一个问题,请考虑延迟初始化等优化(在您第一次需要资源时获取资源)。

于 2008-11-19T01:08:55.267 回答
1

尽可能多,仅此而已。

构造函数必须将对象置于可用状态,因此至少应该初始化您的类变量。initted 的含义可以有广泛的解释。这是一个人为的例子。想象一下,你有一个类负责提供 N! 到您的呼叫应用程序。

实现它的一种方法是让构造函数什么都不做,使用带有循环的成员函数来计算所需的值并返回。

另一种实现它的方法是有一个类变量,它是一个数组。构造函数会将所有值设置为 -1,以指示该值尚未计算。成员函数会做惰性求值。它查看数组元素。如果为-1,则计算并存储并返回值,否则只返回数组中的值。

实现它的另一种方法就像最后一种方法一样,只有构造函数会预先计算值并填充数组,因此该方法可以将值从数组中拉出并返回。

实现它的另一种方法是将值保存在文本文件中,并使用 N 作为文件偏移量的基础以从中提取值。在这种情况下,构造函数将打开文件,而析构函数将关闭文件,而该方法将执行某种 fseek/fread 并返回值。

实现它的另一种方法是预先计算值,并将它们存储为类可以引用的静态数组。构造函数将不起作用,并且该方法将进入数组以获取值并返回它。多个实例将共享该数组。

综上所述,要重点关注的是,通常您希望能够调用一次构造函数,然后经常使用其他方法。如果在构造函数中做更多的工作意味着您的方法要做的工作更少,并且运行得更快,那么这是一个很好的权衡。如果你正在构造/破坏很多,比如在一个循环中,那么为你的构造函数设置高成本可能不是一个好主意。

于 2008-11-16T16:16:19.213 回答
1

如果可以在构造函数之外完成某些事情,请避免在构造函数内部进行。稍后,当您知道您的班级在其他方面表现良好时,您可能会冒险在内部进行。

于 2008-11-16T17:57:51.677 回答
0

这实际上取决于上下文,即课程必须解决的问题。例如,它是否应该始终能够显示其内部的当前子项?如果答案是肯定的,则不应将子项加载到构造函数中。另一方面,如果类表示目录结构的快照,则可以在构造函数中加载它。

于 2008-11-16T15:20:09.247 回答
0

我投票支持瘦构造函数,并在这种情况下为您的对象添加额外的“未初始化”状态行为。

原因:如果你不这样做,你就会强制所有用户要么拥有大量的构造函数,要么动态分配你的类。在这两种情况下,它都可能被视为麻烦。

如果这些对象变成静态的,可能很难从这些对象中捕获错误,因为构造函数会在 main() 之前运行,并且调试器更难以跟踪。

于 2009-12-21T10:49:14.023 回答
0

很好的问题:你给出的“目录”对象引用其他“目录”对象的例子也是一个很好的例子。

对于这种特定情况,我将移动代码以在构造函数之外构建从属对象(或者可能按照此处的另一篇文章的建议执行第一级 [直接子级]),并具有单独的“初始化”或“构建”机制)。

否则还有另一个潜在问题——不仅仅是性能——那就是内存占用:如果你最终进行了非常深的递归调用,你也可能会遇到内存问题[因为堆栈将保留所有局部变量的副本直到递归结束]。

于 2009-12-21T11:02:46.153 回答
-2

尝试在那里拥有你认为必要的东西,不要考虑它是慢还是快。预优化是一种浪费时间,因此需要对其进行编码、分析和优化。

于 2008-11-16T15:33:54.320 回答
-2

对象数组将始终使用默认(无参数)构造函数。没有办法解决这个问题。

有“特殊”构造函数:复制构造函数和 operator=()。

你可以有很多构造函数!或者稍后与很多构造函数结束。Bill 时不时地在 la-la 的土地上想要一个带有浮点数而不是双精度数的新构造函数来节省这 4 个糟糕的字节。(买一些 RAM 账单!)

您不能像调用普通方法那样调用构造函数来重新调用该初始化逻辑。

您不能将构造函数逻辑设为虚拟,并在子类中对其进行更改。(尽管如果您从构造函数而不是手动调用 initialize() 方法,则虚拟方法将不起作用。)

.

当构造函数中存在重要的逻辑时,所有这些事情都会造成很多麻烦。(或至少重复代码。)

因此,作为设计选择,我更喜欢使用最少的构造函数(可选地,取决于它们的参数和情况)调用 initialize() 方法。

根据具体情况,initialize() 可能是私有的。或者它可能是公共的并且支持多个调用(例如重新初始化)。

.

最终,这里的选择因情况而异。我们必须灵活,并考虑权衡。没有一种万能的。

我们用来实现一个具有单个单独实例的类的方法,该实例使用线程与一个专用硬件进行通信,并且必须在 1/2 小时内编写,这不一定是我们用来实现一个类的方法表示在数月内编写的可变精度浮点数的数学。

于 2008-11-16T17:57:17.527 回答