在构造函数中应该做多少工作?

Posted

技术标签:

【中文标题】在构造函数中应该做多少工作?【英文标题】:How much work should be done in a constructor? 【发布时间】:2010-09-22 13:54:00 【问题描述】:

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

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

什么是优雅的解决方案?

【问题讨论】:

contructor 就像应用程序设置向导,您只需在其中进行配置。 如果实例准备好对自己采取任何(可能的)行动,这意味着构造函数做得很好。 【参考方案1】:

总结一下:

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

您选择的不变量可能会影响您的客户。(该对象是否承诺随时可以访问?还是仅在某些状态下?)负责所有设置的构造函数-front 可以让班级客户的生活变得更简单。

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

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

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

总的来说,这取决于。

【讨论】:

【参考方案2】:

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

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

【讨论】:

这是直截了当的逻辑,显式比隐式好,但它可能被称为过于主观。我们还需要哪些其他论据才能让别人相信我们是对的?【参考方案3】:

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

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

【讨论】:

Litb,我不相信你是在暗示任意长的东西——比如枚举目录树应该放在构造函数中......对吗? Foredecker:这与类的语义有关:MutexLocker 可以等待任意长时间,直到资源可用,因为这构成了 MutexLocker 的不变量(调用析构函数的时间) = 拥有的资源)。 这是我可以实现的。现在,我的构造函数中有我希望删除的逻辑。【参考方案4】:

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

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

【讨论】:

【参考方案5】:

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

【讨论】:

“无副作用”太强了。例如:信号量的构造函数可能会阻止其他实例完成它们的构造。这种副作用就是它的工作。另一个构造函数可能会自动为对象分配一个来自(全局或类拥有的)递增计数器的唯一 ID。这也是一个合理的副作用。 同意@Oddthinking。另一个例子:如果你有一个类似ofstream 的构造函数,你还期望“没有副作用”吗? 我不同意这个答案。构造函数最重要的工作是给我一个可用的对象 (RAII)。【参考方案6】:

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

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

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

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

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

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

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

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

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

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

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

【讨论】:

这个建议在设计 GUI 应用程序时是完全合适的,但并不是所有的东西都适合这个模式。我编写了大型、大量运行的数字运算代码,而这个建议在这种情况下是错误的。 我不反对——我很清楚我的 cmets 主要适用于需要响应的 UI 和代码。当然,不是所有东西都适合一个模子——很少有“唯一正确的答案”。【参考方案7】:

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

【讨论】:

【参考方案8】:

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

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

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

【讨论】:

【参考方案9】:

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

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

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

【讨论】:

【参考方案10】:

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

【讨论】:

使用 C++ 和 RAII 模型,预计 ctor 会抛出异常(例如,如果文件不存在)。通常,不应该抛出异常的是 dtors。 C++ 定义了构造函数的顺序,所以这是很好的定义。 即使 C++ 允许,但这并不意味着让构造函数抛出异常是一种好习惯。 抛出异常是非常好的做法;整个异常机制都是围绕这个概念设计的。例如。使用基本构造函数和派生构造函数自动正确传播和清理。 如果你没有在构造函数中抛出异常,你怎么告诉创建对象的代码出错了? ctor 用于初始化您的对象。为您的结构分配内存,设置初始值等。除了 OOM 之外,没有太多理由抛出异常。将任何逻辑放在 ctor 中被认为是不好的做法。【参考方案11】:

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

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

【讨论】:

【参考方案12】:

尽可能多,仅此而已。

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

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

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

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

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

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

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

【讨论】:

【参考方案13】:

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

【讨论】:

【参考方案14】:

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

【讨论】:

【参考方案15】:

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

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

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

【讨论】:

最好有一个精简的私有构造函数和另一个负责构建完全初始化的对象的函数/对象,这样如果你有一个,它就可以安全使用。【参考方案16】:

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

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

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

【讨论】:

【参考方案17】:

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

【讨论】:

“预优化”与作为性能错误的架构或设计选择之间存在巨大差异。性能是一种特性——就像精美的 UI、正确性、可维护性和可靠性一样。建筑师和设计。 为性能而设计,但不要过度设计。让你的设计过于复杂,因为你认为某些东西可能很慢,这可能会更糟。最好的建议是保持设计的灵活性,以便在出现问题时解决问题。这也可以让您满足不断变化的需求。【参考方案18】:

对象数组将始终使用默认(无参数)构造函数。没有办法。

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

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

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

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

.

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

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

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

.

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

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

【讨论】:

以上是关于在构造函数中应该做多少工作?的主要内容,如果未能解决你的问题,请参考以下文章

扩展控制器构造函数没有User实例

在进行构造函数或设置器注入时,依赖项的正确粒度是多少?

自动递增对象id JS构造函数(静态方法和变量)

析构函数为啥能释放对象内存?

为啥我们需要复制构造函数以及何时应该在 java 中使用复制构造函数

为啥不在构造函数中调用可覆盖的方法?