好的做法还是坏的做法?在 getter 中初始化对象
Posted
技术标签:
【中文标题】好的做法还是坏的做法?在 getter 中初始化对象【英文标题】:Good or bad practice? Initializing objects in getter 【发布时间】:2013-01-24 07:17:29 【问题描述】:我似乎有一个奇怪的习惯……至少我的同事是这样说的。我们一直在一起做一个小项目。我编写类的方式是(简化示例):
[Serializable()]
public class Foo
public Foo()
private Bar _bar;
public Bar Bar
get
if (_bar == null)
_bar = new Bar();
return _bar;
set _bar = value;
所以,基本上,我只在调用 getter 并且该字段仍然为空时初始化任何字段。我认为这将通过不初始化任何未在任何地方使用的属性来减少过载。
ETA:我这样做的原因是我的类有几个属性,它们返回另一个类的实例,而另一个类的实例又具有更多类的属性,依此类推。调用***类的构造函数随后会调用所有这些类的所有构造函数,而并非总是都需要它们。
除了个人喜好之外,对这种做法有什么反对意见吗?
更新:关于这个问题,我考虑了许多不同的意见,我会坚持我接受的答案。不过,我现在对这个概念有了更好的理解,我能够决定何时使用它,何时不使用它。
缺点:
线程安全问题 当传递的值为 null 时不服从“setter”请求 微优化 异常处理应在构造函数中进行 需要检查类代码中的空值优点:
微优化 属性永远不会返回 null 延迟或避免加载“重”对象大多数缺点不适用于我当前的库,但是我必须测试一下“微优化”是否真的在优化任何东西。
最后更新:
好的,我改变了答案。我最初的问题是这是否是一个好习惯。我现在确信它不是。也许我仍然会在我当前代码的某些部分使用它,但不是无条件的,也绝对不是一直使用它。所以我会失去我的习惯,在使用它之前考虑一下。谢谢大家!
【问题讨论】:
这是延迟加载模式,在这里它并没有给你带来什么好处,但它仍然是一件好事。 如果您对性能有可衡量的影响,或者如果这些成员很少使用并且消耗过多的内存,或者如果需要很长时间来实例化它们并且只想这样做,则延迟实例化是有意义的按需提供。无论如何,请务必考虑线程安全问题(您当前的代码不是)并考虑使用提供的Lazy<T> 类。 我认为这个问题更适合codereview.stackexchange.com @PLB 它不是单例模式。 我很惊讶没有人提到这段代码的严重错误。你有一个公共财产,我可以从外面设置它。如果我将其设置为 NULL,您将始终创建一个新对象并忽略我的 setter 访问。这可能是一个非常严重的错误。对于私有财产,这可能是好的。就个人而言,我不喜欢做这种过早的优化。增加了复杂性而没有额外的好处。 【参考方案1】:让我在其他人提出的许多优点中再补充一点......
调试器将 (by default) 在单步执行代码时评估属性,这可能会比仅执行代码通常更快地实例化 Bar
。换句话说,仅仅是调试的行为就改变了程序的执行。
这可能是也可能不是问题(取决于副作用),但需要注意。
【讨论】:
【参考方案2】:这是一个不错的设计选择。强烈推荐用于库代码或核心类。
它被一些“延迟初始化”或“延迟初始化”调用,通常被所有人认为是一个很好的设计选择。
首先,如果你在类级变量或构造函数的声明中进行初始化,那么当你的对象被构造时,你就有了创建一个可能永远不会被使用的资源的开销。
其次,只有在需要时才创建资源。
第三,避免垃圾收集未使用的对象。
最后,处理属性中可能发生的初始化异常比处理类级别变量或构造函数初始化期间发生的异常更容易。
这条规则有例外。
关于“get”属性中对初始化的附加检查的性能参数,它是无关紧要的。初始化和释放对象比使用跳转进行简单的空指针检查对性能的影响更大。
类库开发设计指南http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx
关于Lazy<T>
通用的Lazy<T>
类正是根据发布者的需要而创建的,请参阅http://msdn.microsoft.com/en-us/library/dd997286(v=vs.100).aspx 上的延迟初始化。如果您有旧版本的 .NET,则必须使用问题中说明的代码模式。这种 Code Pattern 变得如此普遍,以至于 Microsoft 认为可以在最新的 .NET 库中包含一个类,以便更容易实现该模式。此外,如果您的实现需要线程安全,那么您必须添加它。
原始数据类型和简单类
显然,您不会将惰性初始化用于原始数据类型或像List<string>
这样的简单类使用。
在评论 Lazy 之前
Lazy<T>
是在 .NET 4.0 中引入的,所以请不要再添加关于这个类的评论。
在评论微优化之前
在构建库时,必须考虑所有优化。例如,在 .NET 类中,您会在整个代码中看到用于布尔类变量的位数组,以减少内存消耗和内存碎片,仅举两个“微优化”。
关于用户界面
您不会对用户界面直接使用的类使用延迟初始化。上周我花了一天的大部分时间来删除组合框视图模型中使用的八个集合的延迟加载。我有一个 LookupManager
来处理任何用户界面元素所需的集合的延迟加载和缓存。
“二传手”
我从来没有为任何延迟加载的属性使用过 set-property(“setters”)。因此,您永远不会允许foo.Bar = null;
。如果您需要设置Bar
,那么我将创建一个名为SetBar(Bar value)
的方法并且不使用惰性初始化
收藏
类集合属性在声明时总是被初始化,因为它们不应该为空。
复杂类
让我以不同的方式重复这一点,您对复杂的类使用延迟初始化。通常是设计不佳的类。
最后
我从未说过要对所有课程或所有情况都这样做。这是一个坏习惯。
【讨论】:
如果你可以在不同的线程中多次调用 foo.Bar 而没有任何干预值设置但得到不同的值,那么你有一个糟糕的可怜的类。 我认为这是一个糟糕的经验法则,没有太多考虑。除非 Bar 是一个已知的资源消耗者,否则这是一个不必要的微优化。在 Bar 是资源密集型的情况下,.net 中内置了线程安全的 Lazy您在这里拥有的是“延迟初始化”的 - 天真 - 实现。
简答:
无条件地使用延迟初始化不是一个好主意。它有它的位置,但必须考虑到这个解决方案的影响。
背景及说明:
具体实施: 让我们先看看你的具体示例,以及为什么我认为它的实现很幼稚:
它违反了Principle of Least Surprise (POLS)。将值分配给属性时,预计会返回该值。在您的实现中,null
并非如此:
foo.Bar = null;
Assert.Null(foo.Bar); // This will fail
它引入了相当多的线程问题:在不同线程上的两个foo.Bar
调用者可能会获得Bar
的两个不同实例,其中一个将没有与Foo
实例的连接。对该 Bar
实例所做的任何更改都会自动丢失。
这是违反 POLS 的又一案例。当仅访问属性的存储值时,它应该是线程安全的。虽然您可能会争辩说该类根本不是线程安全的 - 包括您的属性的吸气剂 - 您必须正确记录这一点,因为这不是正常情况。此外,我们很快就会看到,没有必要引入这个问题。
一般: 现在是时候看一下一般的延迟初始化了: 延迟初始化通常用于延迟对象的构造需要很长时间才能构造或需要大量内存一旦完全构造。 这是使用延迟初始化的一个非常正当的理由。
但是,此类属性通常没有设置器,这消除了上面指出的第一个问题。
此外,将使用线程安全的实现——比如Lazy<T>
——来避免第二个问题。
即使在惰性属性的实现中考虑这两点,以下几点也是这种模式的普遍问题:
对象的构造可能不成功,从而导致属性 getter 异常。这是又一次违反 POLS,因此应该避免。甚至“开发类库的设计指南”中的 section on properties 也明确指出属性 getter 不应该抛出异常:
避免从属性 getter 中抛出异常。
属性获取器应该是没有任何先决条件的简单操作。如果 getter 可能引发异常,请考虑将属性重新设计为方法。
编译器的自动优化受到损害,即内联和分支预测。详细解释请见Bill K's answer。
这些点的结论如下: 对于每个延迟实现的单个属性,您应该考虑过这些点。 这意味着,这是一个个案决定,不能作为一般的最佳实践。
这种模式有它的位置,但它不是实现类时的一般最佳实践。由于上述原因,不应无条件使用它。。
在本节中,我想讨论其他人提出的一些观点,作为无条件使用延迟初始化的论据:
序列化: EricJ 在一条评论中指出:
一个可能被序列化的对象在反序列化时不会调用它的构造函数(取决于序列化程序,但许多常见的行为都是这样的)。将初始化代码放在构造函数中意味着您必须为反序列化提供额外的支持。这种模式避免了这种特殊的编码。
这个论点有几个问题:
-
大多数对象永远不会被序列化。在不需要时为其添加某种支持违反了YAGNI。
当一个类需要支持序列化时,有一些方法可以启用它,而无需使用乍一看与序列化无关的解决方法。
微优化: 您的主要论点是,您只想在有人实际访问它们时才构造对象。所以你实际上是在谈论优化内存使用。 我不同意这个论点,原因如下:
-
在大多数情况下,内存中的更多对象对任何事物都没有任何影响。现代计算机有足够的内存。如果没有分析器确认的实际问题,这是pre-mature optimization 并且有充分的理由反对它。
我承认有时这种优化是合理的。但即使在这些情况下,延迟初始化似乎也不是正确的解决方案。反对它的原因有两个:
-
延迟初始化可能会损害性能。也许只是微乎其微,但正如比尔的回答所表明的那样,影响比乍一看可能更大。因此,这种方法基本上是在性能与内存之间进行权衡。
如果您的设计通常只使用类的一部分,这暗示了设计本身存在问题:所讨论的类很可能有多个职责。解决方案是将课程拆分为几个更有针对性的课程。
【讨论】:
@JohnWillemse:这是您架构的问题。你应该重构你的类,使它们更小、更专注。不要为 5 个不同的事物/任务创建一个类。改为创建 5 个类。 @JohnWillemse 也许考虑这是一个过早优化的情况。除非您有 已测量 性能/内存瓶颈,否则我建议您不要这样做,因为它会增加复杂性并引入线程问题。 +1,对于 95% 的类来说,这不是一个好的设计选择。延迟初始化有其优点,但不应推广到所有属性。它增加了复杂性、阅读代码的难度、线程安全问题……在 99% 的情况下没有明显的优化。此外,正如 SolutionYogi 作为评论所说,OP 的代码有问题,这证明这种模式实现起来并不简单,除非实际需要延迟初始化,否则应避免使用。 @DanielHilgarth 感谢您一路写下(几乎)无条件使用此模式的所有错误。干得好! @DanielHilgarth 好吧,是的,不是的。违规是这里的问题,所以是的。但也“不”,因为 POLS 是严格的原则,即您可能不会对代码感到惊讶。如果 Foo 没有暴露在您的程序之外,那么您可以承担或不承担风险。在这种情况下,我几乎可以保证您最终会感到惊讶,因为您无法控制访问属性的方式。风险只是变成了一个错误,你关于null
案例的论点变得更加强大。 :-)【参考方案4】:
您是否考虑使用Lazy<T>
实现这种模式?
除了轻松创建延迟加载的对象外,您还可以在初始化对象时获得线程安全:
http://msdn.microsoft.com/en-us/library/dd642331.aspx正如其他人所说,如果对象真的很耗费资源,或者在对象构建期间加载它们需要一些时间,那么您将延迟加载它们。
【讨论】:
谢谢,我现在明白了,我现在一定会去看看Lazy<T>
,并且不要使用我一直以来的方式。
你没有得到 magic 线程安全......你仍然需要考虑它。来自 MSDN:Making the Lazy<T> object thread safe does not protect the lazily initialized object. If multiple threads can access the lazily initialized object, you must make its properties and methods safe for multithreaded access.
@EricJ。当然,当然。只有在初始化对象时才能获得线程安全,但稍后您需要像处理任何其他对象一样处理同步。【参考方案5】:
你确定 Foo 应该实例化任何东西吗?
对我来说,让 Foo 实例化任何东西似乎很臭(尽管不一定错误)。除非 Foo 的明确目的是成为一家工厂,否则它不应该实例化它自己的合作者,而是 instead get them injected in its constructor。
如果 Foo 存在的目的是创建 Bar 类型的实例,那么我认为懒惰地做它没有任何问题。
【讨论】:
@BenjaminGruenbaum 不,不是真的。恭敬地,即使是这样,你想表达什么观点?【参考方案6】:我只是想对丹尼尔的回答发表评论,但老实说,我认为这还不够。
虽然这是在某些情况下使用的非常好的模式(例如,当从数据库初始化对象时),但这是一个可怕的习惯。
对象的最大优点之一是它提供了一个安全、可信的环境。最好的情况是,如果您将尽可能多的字段设置为“Final”,并使用构造函数将它们全部填充。这使您的课程非常防弹。允许通过设置器更改字段的情况要少一些,但并不可怕。例如:
安全类 字符串名称=""; 整数年龄=0; 公共无效集合名称(字符串新名称) 断言(新名称!= null) 名称=新名称; // 遵循这个模式 ... 公共字符串 toString() String s="安全类有名字:"+name+" 和年龄:"+age使用您的模式,toString 方法将如下所示:
如果(名称 == 空) throw new IllegalStateException("SafeClass 进入了非法状态!name 为 null") 如果(年龄 == 空) throw new IllegalStateException("SafeClass 进入非法状态!年龄为空") 公共字符串 toString() String s="安全类有名字:"+name+" 和年龄:"+age不仅如此,你还需要在你可能在类中使用该对象的任何地方进行空值检查(在你的类之外是安全的,因为 getter 中的空值检查,但你应该主要在类中使用你的类成员)
此外,您的类永远处于不确定状态——例如,如果您决定通过添加一些注释使该类成为休眠类,您会怎么做?
如果您在没有要求和测试的情况下基于一些微优化做出任何决定,那几乎肯定是错误的决定。实际上,即使在最理想的情况下,您的模式实际上也很有可能会减慢系统速度,因为 if 语句可能会导致 CPU 上的分支预测失败,这会使事情变慢很多很多倍只需在构造函数中分配一个值,除非您创建的对象相当复杂或来自远程数据源。
有关 brance 预测问题的示例(您会反复出现,而不仅仅是一次),请参阅这个真棒问题的第一个答案:Why is it faster to process a sorted array than an unsorted array?
【讨论】:
感谢您的意见。就我而言,没有一个类有任何可能需要检查 null 的方法,所以这不是问题。我会考虑你的其他反对意见。 我不太明白。这意味着您没有在存储它们的类中使用您的成员——您只是将这些类用作数据结构。如果是这种情况,您可能想阅读javaworld.com/javaworld/jw-01-2004/jw-0102-toolbox.html,它很好地描述了如何通过避免外部操作对象状态来改进您的代码。如果您在内部操作它们,如何在不重复检查所有内容的情况下进行操作? 这个答案的部分内容很好,但部分内容似乎做作。通常使用这种模式时,toString()
会调用getName()
,而不是直接使用name
。
@BillK 是的,类是一个巨大的数据结构。所有工作都在静态类中完成。我将在链接中查看文章。谢谢!
@izkata 实际上,在课堂上,是否使用吸气剂似乎是一个折腾,我工作过的大多数地方都直接使用该成员。除此之外,如果您始终使用 getter,则 if() 方法的危害更大,因为分支预测失败会更频繁地发生,并且由于分支,运行时可能会在插入 getter 时遇到更多麻烦。然而,这一切都没有实际意义,因为 john 发现它们是数据结构和静态类,这是我最关心的事情。【参考方案7】:
我可以看到的缺点是,如果您想询问 Bars 是否为空,它永远不会,您将在那里创建列表。
【讨论】:
我不认为这是一个缺点。 为什么会有这样的缺点?只需检查任何而不是空。 if(!Foo.Bars.Any()) @PeterPorfy:它违反了POLS。你把null
放进去,但不要把它找回来。通常,您假设您得到的值与您放入属性中的值相同。
@DanielHilgarth 再次感谢。这是一个我以前没有考虑过的非常有效的论点。
@AMissico:这不是一个虚构的概念。就像按下前门旁边的按钮会敲响门铃一样,看起来像房产的东西也应该表现得像房产。在你的脚下打开活板门是一种令人惊讶的行为,尤其是在按钮没有这样标记的情况下。【参考方案8】:
延迟实例化/初始化是一种完全可行的模式。但请记住,作为一般规则,您的 API 的使用者不希望 getter 和 setter 从最终用户 POV 那里花费可辨别的时间(或失败)。
【讨论】:
我同意,我已经稍微编辑了我的问题。我希望完整的底层构造函数链比仅在需要时实例化类花费更多时间。【参考方案9】:我认为这取决于您正在初始化什么。我可能不会为清单做这件事,因为建设成本很小,所以它可以放在构造函数中。但如果它是一个预先填充的列表,那么在第一次需要它之前我可能不会这样做。
基本上,如果构建成本超过对每个访问进行条件检查的成本,那么就懒惰地创建它。如果没有,请在构造函数中执行。
【讨论】:
谢谢!这是有道理的。以上是关于好的做法还是坏的做法?在 getter 中初始化对象的主要内容,如果未能解决你的问题,请参考以下文章