应该在构造函数中执行可能需要一些时间的操作,还是应该构造对象然后稍后进行初始化。
例如,当构造表示目录结构的对象时,应该在构造函数中完成对象及其子对象的填充。显然,目录可以包含目录,而目录又可以包含目录等等。
对此有什么优雅的解决方案?
答案 0 :(得分:48)
总结:
至少,构造函数需要将对象配置为其不变量为真的点。
您选择的不变量可能会影响您的客户。(对象是否承诺随时可以访问?或仅在某些状态下?)构建器负责所有设置-front可以让班级的客户生活更简单。
长时间运行的构造函数本身并不坏,但在某些情况下可能会很糟糕。
对于涉及用户交互的系统,任何类型的长时间运行方法都可能导致响应能力差,应该避免使用。
延迟计算直到构造函数可能是有效的优化之后;可能结果是没有必要执行所有工作。这取决于应用程序,不应过早确定。
总的来说,这取决于。
答案 1 :(得分:24)
您通常不希望构造函数执行任何计算。使用该代码的其他人不会期望它比基本设置更多。
对于你正在谈论的目录树,“优雅”的解决方案可能不是在构造对象时构建完整的树。相反,按需构建它。使用你的对象的人可能并不真正关心子目录中的内容,所以首先让你的构造函数列表第一级,然后如果有人想要下降到特定目录,那么在他们请求时构建树的那一部分它
答案 2 :(得分:13)
所需的时间不应成为不将某些东西放入构造函数的理由。您可以将代码本身放入私有函数中,并从构造函数中调用它,只是为了使构造函数中的代码保持清晰。
但是,如果你想要做的事情不需要给对象一个定义的条件,并且你可以在第一次使用后做这些事情,这将是一个合理的论据,可以把它拿出来并在以后再做。但是不要让它依赖于你的类的用户:这些东西(按需初始化)必须对你的类的用户完全透明。否则,对象的重要不变量可能很容易破坏。
答案 3 :(得分:10)
取决于(典型的CS答案)。如果您在启动时为长时间运行的程序构造对象,那么在构造函数中执行大量工作就没有问题。如果这是预期快速响应的GUI的一部分,则可能不合适。与往常一样,最好的答案是首先以最简单的方式尝试,分析和优化。
对于这种特定情况,您可以对子目录对象进行延迟构造。仅为顶级目录的名称创建条目。如果访问它们,则加载该目录的内容。当用户演示目录结构时再次执行此操作。
答案 4 :(得分:7)
构造函数最重要的工作是为对象提供初始有效状态。在我看来,构造函数最重要的期望是构造函数应该没有副作用。
答案 5 :(得分:4)
为了代码维护,测试和调试,我尽量避免在构造函数中添加任何逻辑。如果您更喜欢从构造函数执行逻辑,那么将逻辑放在诸如init()和从构造函数调用init()的方法中会很有帮助。如果您计划开发单元测试,则应避免在构造函数中添加任何逻辑,因为可能难以测试不同的情况。我认为以前的评论已经解决了这个问题,但是......如果您的应用程序是交互式的,那么您应该避免单次调用会导致性能明显下降。如果您的应用程序是非交互式的(例如:夜间批处理作业),那么单个性能影响并不是一件大事。
答案 6 :(得分: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; }
}
由于我想在详细信息视图中显示有关公司的所有信息,因此构造函数将包含填充每个属性所需的所有代码。鉴于这是一个复杂类型,公司构造函数也将触发执行Address,Phones和Contact构造函数。
现在,如果我填充目录列表视图,我可能只需要CompanyName和主要电话号码,我可能在类上有第二个构造函数,它只检索该信息并将剩余信息留空,或我可能只创建一个仅保存该信息的单独对象。它实际上只取决于如何检索信息,以及从何处检索信息。
无论一个类的构造函数是多少,我个人的目标都是做任何必要的处理来准备对象,以便执行任何可能强加给它的任务。
答案 7 :(得分:3)
我同意长时间运行的构造函数本身并不坏。但我认为你几乎总是做错事。我的建议与Hugo,Rich和Litb的建议类似:
I / O问题示例:许多硬盘存在问题,即它们进入不提供读取或写入100或甚至数千毫秒的状态。第一代和第一代固态硬盘经常这样做。用户现在已经知道你的程序有点挂了 - 他们只是认为这是你的错误软件。
当然,长时间运行的构造函数的邪恶取决于两件事:
现在,如果'long'只是几百个额外的工作周期,那么它不会很长。但是一个构造函数正在进入100微秒的范围,我认为它很长。当然,如果你只是实例化其中一个,或者很少实例化它们(比如说每隔几秒钟一次),那么由于这个范围内的持续时间,你不太可能看到问题。
频率是一个重要的因素,如果你只是构建其中的一部分,那么500 us ctor不是问题:但是创建其中的一百个将会造成严重的性能问题。
让我们谈谈你的例子:在“类目录”对象中填充目录对象树。 (注意,我将假设这是一个带有图形用户界面的程序)。在这里,您的CTOR持续时间不依赖于您编写的代码 - 其被告在枚举任意大型目录树所花费的时间。这在本地硬盘上已经足够糟糕了。它在远程(网络)重新出现时甚至更成问题。
现在,想象一下在您的用户界面线程上执行此操作 - 您的UI将在其轨道中停留数秒,10秒或甚至数分钟。在Windows中,我们称之为UI挂起。他们坏坏坏(是的,我们有他们......是的,我们努力消除它们。)
UI挂起可能会让人们真的讨厌你的软件。
这里正确的做法是简单地初始化目录对象。在可以取消的循环中构建目录树,并使UI保持响应状态(取消按钮应始终有效)
答案 8 :(得分:2)
至于在构造函数中应该完成多少工作,我会说它应该考虑到事情的缓慢程度,你将如何使用这个课程,以及你个人对它的感觉。
在您的目录结构对象上:我最近为我的HTPC实现了一个samba(Windows共享)浏览器,因为它非常慢,我选择仅在触摸时实际初始化目录。例如首先,树只包含一个机器列表,然后每当您浏览目录时,系统将自动从该机器初始化树,并使目录列表更深一层,依此类推。
理想情况下,我认为你甚至可以把它写成一个扫描目录广度优先的工作线程,并优先考虑你当前正在浏览的目录,但一般来说这对简单的事情来说太过分了。 )
答案 9 :(得分:1)
尽可能多,不再需要。
构造函数必须将对象置于可用状态,因此至少应该启动类变量。启动意味着什么可以有广泛的解释。这是一个人为的例子。想象一下,你有一个有责任提供N的课程!到您的通话应用程序。
实现它的一种方法是让构造函数不做任何事情,使用带有循环的成员函数来计算所需的值并返回。
实现它的另一种方法是拥有一个类变量,它是一个数组。构造函数会将所有值设置为-1,以指示尚未计算该值。成员函数会进行惰性求值。它查看数组元素。如果它是-1,它会计算并存储它并返回值,否则它只返回数组中的值。
实现它的另一种方法就像最后一种方法一样,只有构造函数会预先计算值,并填充数组,因此方法可以将值拉出数组并返回它。
实现它的另一种方法是将值保存在文本文件中,并使用N作为文件偏移量的基础来从中提取值。在这种情况下,构造函数将打开文件,析构函数将关闭文件,而方法将执行某种fseek / fread并返回值。
实现它的另一种方法是预先计算值,并将它们存储为类可以引用的静态数组。构造函数没有工作,该方法将进入数组以获取值并返回它。多个实例将共享该数组。
所有人都说,要关注的是,通常你希望能够一次调用构造函数,然后经常使用其他方法。如果在构造函数中做更多的工作意味着你的方法需要做的工作少,而且运行速度更快,那么这是一个很好的权衡。如果你正在构建/破坏很多东西,比如在循环中,那么构造函数的成本可能不是一个好主意。
答案 10 :(得分:1)
确保ctor没有做任何可能引发异常的事情。
答案 11 :(得分:1)
如果可以在构造函数外部执行某些操作,请避免在内部执行此操作。之后,当你知道你的班级表现良好时,你可能会冒险在里面做这件事。
答案 12 :(得分:1)
RAII是C ++资源管理的支柱,因此在构造函数中获取所需的资源,在析构函数中释放它们。
这是您建立类不变量的时候。如果需要时间,则需要时间。你拥有的“如果X存在做Y”构造越少,类的其余部分就越容易设计。稍后,如果分析显示这是一个问题,请考虑优化,如延迟初始化(在您第一次需要时获取资源)。
答案 13 :(得分:0)
我投票支持瘦构造函数,并在这种情况下为您的对象添加额外的“未初始化”状态行为。
原因:如果你不这样做,你就会强迫所有用户拥有繁重的构造函数,或者动态分配你的类。在这两种情况下,它都可能被视为一种麻烦。
如果这些对象变为静态,可能很难捕获这些对象的错误,因为构造函数然后在main()之前运行,并且调试器更难以跟踪。
答案 14 :(得分:0)
很好的问题:您给出的“目录”对象引用其他“目录”对象的示例也是一个很好的例子。
对于这个特定的情况,我会移动代码来构建构造函数中的下级对象(或者可以做第一级[直接子项],这是另一篇文章推荐),并且有一个单独的'initialize'或'build'机构)。
另外还有一个潜在的问题 - 除了性能之外 - 就是内存占用:如果最终进行非常深的递归调用,你最终可能会遇到内存问题[因为堆栈会保留所有内容的副本局部变量,直到递归结束]。
答案 15 :(得分:0)
这实际上取决于上下文,即类必须解决的问题。例如,它应该始终能够显示当前的孩子吗?如果答案是肯定的,则不应在构造函数中加载子项。另一方面,如果类表示目录结构的快照,则可以在构造函数中加载它。
答案 16 :(得分:-2)
尝试拥有你认为必要的东西,不要考虑它是慢还是快。预优化是浪费时间,因此对其进行编码,对其进行分析并在需要时对其进行优化。
答案 17 :(得分:-2)
对象数组将始终使用默认(无参数)构造函数。没有办法解决这个问题。
有“特殊”构造函数:复制构造函数和operator =()。
你可以拥有很多构造函数!或者稍后与很多建设者结束。在la-la land中,Bill偶尔会想要一个带有浮点数的新构造函数而不是双精度来保存这4个糟糕的字节。 (买一些RAM账单!)
你不能像普通方法一样调用构造函数来重新调用那个初始化逻辑。
您无法使构造函数逻辑成为虚拟,并在子类中进行更改。 (虽然如果从构造函数而不是手动调用initialize()方法,虚拟方法将不起作用。)
当构造函数中存在重要逻辑时,所有这些都会产生很多悲伤。 (或至少重复代码。)
因此,作为设计选择,我更喜欢使用最小的构造函数(可选地,根据其参数和情况)调用initialize()方法。
根据具体情况,initialize()可能是私有的。或者它可以是公共的并支持多次调用(例如重新初始化)。
最终,这里的选择因情况而异。我们必须灵活,并考虑权衡。没有一个适合所有人的。
我们用来实现具有单个孤立实例的类的方法,该实例使用线程与一个专用硬件进行通信,并且必须在1/2小时内写入,这不一定是我们用来实现的在几个月内写出的变精度浮点数表示数学的类。