好或坏的做法? 在getter中初始化对象
我似乎有一种奇怪的习惯,至less据我的同事说。 我们一起在一个小项目上工作。 我写这些类的方法是(简单的例子):
[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被调用,并且该字段仍然为空。 我想这会减less过载,不要初始化任何地方没有使用的属性。
ETA:我这样做的原因是我的类有几个属性返回另一个类的实例,反过来也有更多类的属性,等等。 调用顶级类的构造函数随后会调用所有这些类的所有构造函数,但并不总是需要。
除个人喜好外,是否有人反对这种做法?
更新:我已经考虑了许多关于这个问题的不同意见,我会支持我接受的答案。 但是,现在我对这个概念有了更好的理解,我可以决定何时何时使用它。
缺点:
- 线程安全问题
- 传递的值为空时不服从“setter”请求
- 微优化
- exception处理应该在构造函数中进行
- 需要在类的代码中检查null
优点:
- 微优化
- 属性永远不会返回null
- 延迟或避免加载“重”的对象
大多数缺点不适用于我目前的图书馆,但是我将不得不testing一下“微观优化”是否实际上是在优化任何东西。
最后更新:
好吧,我改变了我的答案。 我最初的问题是这是否是一个好习惯。 而我现在确信它不是。 也许我仍然会在当前代码的某些部分使用它,但不是无条件,绝对不是全部。 所以在使用之前,我会丢掉我的习惯并思考它。 感谢大家!
你在这里是一个天真的 – 执行“懒惰初始化”。
简短的回答:
无条件地使用懒惰的初始化不是一个好主意。 它有它的地方,但必须考虑到这个解决scheme的影响。
背景和解释:
具体实现:
让我们先看看你的具体样本,为什么我认为它的实现天真:
-
这违反了最小惊喜原则(POLS) 。 当一个值被分配给一个属性时,预期这个值被返回。 在你的实现中,
null
不是这种情况:foo.Bar = null; Assert.Null(foo.Bar); // This will fail
- 它引入了相当多的线程问题:两个在不同线程上调用
foo.Bar
调用者可能会获得两个不同的Bar
实例,其中一个将不会连接到Foo
实例。 对Bar
实例所做的任何更改都会丢失。
这是违反POLS的另一个案例。 当只访问一个属性的存储值时,它被期望是线程安全的。 虽然你可能会认为这个类不是线程安全的,包括你的属性的getter,但是你必须正确地logging下来,因为这不是正常情况。 而且这个问题的介绍是不必要的,我们很快就会看到。
一般来说:
一般来说,现在是查看惰性初始化的时候了:
延迟初始化通常用于延迟构build需要很长时间的对象的构造,或者一旦完成构build就需要大量的内存 。
这是使用惰性初始化的一个非常有效的原因。
但是,这样的属性通常没有制定者,从而摆脱了上面提到的第一个问题。
此外,将使用线程安全的实现 – 像Lazy<T>
– 来避免第二个问题。
即使在执行懒惰属性时考虑这两点,以下几点也是这种模式的一般问题:
-
对象的构造可能不成功,导致属性获取器的exception。 这是POLS的另一个违规行为,因此应该避免。 甚至在“开发类库的devise指南”中的属性部分也明确指出,属性获取者不应该抛出exception:
避免从属性获取器中抛出exception。
属性获取者应该是没有任何先决条件的简单操作。 如果一个getter可能抛出一个exception,可以考虑重新devise这个属性作为一个方法。
-
编译器自动优化会受到影响,即内联和分支预测。 有关详细解释,请参阅Bill K的答案 。
这些结论是:
对于每个懒惰实施的单一财产,您应该考虑这些要点。
这意味着,这是一个每个案件的决定,不能被视为一般的最佳做法。
这种模式有其自己的位置,但是在实现类时不是最佳实践。 由于上述原因, 不应无条件使用 。
在本节中,我想讨论一些其他人提出的无条件使用惰性初始化的参数:
-
连载:
EricJ在一个评论中说:一个可能序列化的对象在反序列化时不会调用它的构造器(取决于序列化器,但许多常见的行为就像这样)。 将初始化代码放入构造器意味着您必须为反序列化提供额外的支持。 这种模式避免了特殊的编码。
这个论点有几个问题:
- 大多数对象永远不会被序列化。 在不需要的时候添加某种支持就会违反YAGNI 。
- 当一个类需要支持序列化的时候,有一些方法可以在没有解决方法的情况下启用它,而乍一看没有任何与序列化有关的方法。
-
微观优化:你的主要观点是,只有当有人实际访问它们时才想要构build对象。 所以你实际上是在谈论优化内存使用。
我不同意这个说法,原因如下:- 在大多数情况下,记忆中的一些物体对任何事物都没有影响。 现代计算机有足够的内存。 如果没有一个由事件探查器证实的实际问题,这是过时的优化 ,有很好的理由反对它。
-
我承认有时这种优化是有道理的。 但即使在这些情况下,懒惰初始化似乎也不是正确的解决scheme。 有两个原因反对它:
- 延迟初始化可能会损害性能。 也许只是微乎其微,但正如比尔的答案所显示的那样,其影响比乍看起来要大。 所以这种方法基本上是交换性能与内存。
- 如果你有一个通用的devise来使用这个类的一部分,这就提示了devise本身存在一个问题:这个类很可能有多个责任。 解决办法是把课堂分成几个更集中的课程。
这是一个很好的deviseselect。 强烈build议使用库代码或核心类。
它被一些“延迟初始化”或“延迟初始化”调用,所有人普遍认为这是一个很好的deviseselect。
首先,如果在类级别variables或构造函数的声明中进行初始化,那么当构build对象时,就会产生一个可能永远不会使用的资源的开销。
其次,资源只在需要的时候被创build。
第三,避免垃圾收集未使用的对象。
最后,处理属性中可能发生的初始化exception,以及在类级别variables或构造函数的初始化期间发生的exception,会更容易。
这个规则也有例外。
关于在“get”属性中额外检查初始化的性能参数,它是不重要的。 初始化和处理对象比使用跳转的简单空指针检查更有效。
有关开发类库的devise指南,请访问http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx
关于Lazy<T>
通用的Lazy<T>
类是为海报的需要而创build的,请参阅http://msdn.microsoft.com/zh-cn/library/dd997286(v=vs.100).aspx上的 Lazy Initialization 。 如果您的.NET版本较旧,则必须使用问题中说明的代码模式。 这种代码模式已经变得如此普遍,以至于微软认为在最新的.NET库中包含了一个类,以便于实现这个模式。 另外,如果你的实现需要线程安全,那么你必须添加它。
原始数据types和简单类
Obvioulsy,你不会使用惰性初始化的基本数据types或简单的类使用像List<string>
。
在评论懒惰之前
Lazy<T>
是在.NET 4.0中引入的,所以请不要再添加关于这个类的其他评论。
在评论微观优化之前
当你build立图书馆时,你必须考虑所有的优化。 例如,在.NET类中,您将看到在整个代码中用于布尔类variables的位数组,以减less内存消耗和内存碎片,仅举两个“微优化”。
关于用户界面
您不会使用用户界面直接使用的类的延迟初始化。 上周我花了一天中的更多时间去除combobox的视图模型中使用的八个集合的延迟加载。 我有一个LookupManager
处理任何用户界面元素所需集合的延迟加载和caching。
“二传手”
我从来没有使用set-property(“setters”)的任何懒加载属性。 因此,你永远不会允许foo.Bar = null;
。 如果你需要设置Bar
那么我会创build一个名为SetBar(Bar value)
而不是使用延迟初始化
集合
类集合属性始终在声明时进行初始化,因为它们不应该为null。
复杂的类
让我重复一遍,对复杂类使用延迟初始化。 通常,devise不佳的class级。
最后
我从来没有说过要为所有class级或所有情况做这件事。 这是一个坏习惯。
你是否考虑使用Lazy<T>
来实现这样的模式?
除了轻松创build延迟加载的对象之外,还可以在对象初始化时获得线程安全性:
正如其他人所说的那样,如果对象的构build时间过长或者需要花费一些时间来加载对象,那么就会延迟加载对象。
我认为这取决于你正在初始化。 我可能不会列表,因为build设成本很小,所以它可以在构造函数中。 但是,如果这是一个人口稠密的清单,那么我可能不会在第一次需要之前。
基本上,如果build设成本大于对每个访问进行有条件检查的成本,那就懒得创build它。 如果没有,在构造函数中执行。
我能看到的缺点是,如果你想问一下Bars是否为null,那么它永远不会,你将在那里创build列表。
懒惰的实例化/初始化是一个完全可行的模式。 但请记住,作为一般规则,API的消费者不希望getter和setter从最终用户POV(或失败)中获取可辨别的时间。
我只是要对丹尼尔的回答发表评论,但我真的不觉得这样做还不够。
虽然在某些情况下这是一个非常好的模式(例如,当数据库初始化对象时),但这是一个不好的习惯。
关于一个对象的最好的事情之一就是它会产生一个安全可信的环境。 最好的情况是,如果您尽可能多地填写“最终”字段,将它们全部填入构造函数中。 这使得你的课程相当防弹。 允许字段通过setter更改是less一点,但不是可怕的。 例如:
类SafeClass { String name =“”; 整数年龄= 0; public void setName(String newName) { 断言(newName!= null) 命名=了newName; } //遵循这个年龄的模式 ... public String toString(){ strings =“安全类名称:”+名+“和年龄:”+年龄 } }
使用你的模式,toString方法看起来像这样:
如果(name == null) 抛出新的IllegalStateException(“SafeClass进入非法状态!名称为空”) 如果(年龄== null) 抛出新的IllegalStateException(“SafeClass进入非法状态!年龄为空”) public String toString(){ strings =“安全类名称:”+名+“和年龄:”+年龄 }
不仅如此,而且在你的类中可能会使用这个对象的时候,你需要空的检查(在你的类之外是安全的,因为getter中有空的检查,但是你应该主要在类中使用你的类成员)
此外,你的课堂永远处于一个不确定的状态 – 例如,如果你决定通过添加一些注释让这个课程成为一个冬眠课程,你将如何做?
如果您在没有要求和testing的基础上做出任何基于微观化的决定,那几乎肯定是错误的决定。 事实上,即使在最理想的情况下,你的模式实际上也会减慢系统的速度,因为if语句可能会导致CPU上的分支预测失败,这会使得事情减慢很多很多倍只需在构造函数中分配一个值,除非您创build的对象相当复杂或来自远程数据源。
对于一个brance预测问题的例子(你一再发生,只是一次),看到这个真棒问题的第一个答案: 为什么处理sorting的数组比一个未sorting的数组更快?
让我再补充一点,指出别人提出的许多好的观点。
debugging器将( 在默认情况下 )逐步执行代码时评估属性,这可能会比通常只执行代码更快地实例化Bar
。 换句话说,单纯的debugging行为就是改变程序的执行。
这可能是也可能不是问题(取决于副作用),但是需要注意的是。
你确定Foo应该实例化吗?
对我来说,让Foo实例化任何东西似乎都很臭(虽然不一定是错的 )。 除非Foo的明确目的是成为一个工厂,否则它不应该实例化它自己的合作者, 而是把它们注入到它的构造函数中 。
但是,如果Foo的存在目的是创buildBartypes的实例,那么我懒得去做没有什么错误。