好或坏的做法? 在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的影响。

背景和解释:

具体实现:
让我们先看看你的具体样本,为什么我认为它的实现天真:

  1. 这违反了最小惊喜原则(POLS) 。 当一个值被分配给一个属性时,预期这个值被返回。 在你的实现中, null不是这种情况:

     foo.Bar = null; Assert.Null(foo.Bar); // This will fail 
  2. 它引入了相当多的线程问题:两个在不同线程上调用foo.Bar调用者可能会获得两个不同的Bar实例,其中一个将不会连接到Foo实例。 对Bar实例所做的任何更改都会丢失。
    这是违反POLS的另一个案例。 当只访问一个属性的存储值时,它被期望是线程安全的。 虽然你可能会认为这个类不是线程安全的,包括你的属性的getter,但是你必须正确地logging下来,因为这不是正常情况。 而且这个问题的介绍是不必要的,我们很快就会看到。

一般来说:
一般来说,现在是查看惰性初始化的时候了:
延迟初始化通常用于延迟构build需要很长时间的对象的构造,或者一旦完成构build就需要大量的内存
这是使用惰性初始化的一个非常有效的原因。

但是,这样的属性通常没有制定者,从而摆脱了上面提到的第一个问题。
此外,将使用线程安全的实现 – 像Lazy<T> – 来避免第二个问题。

即使在执行懒惰属性时考虑这两点,以下几点也是这种模式的一般问题:

  1. 对象的构造可能不成功,导致属性获取器的exception。 这是POLS的另一个违规行为,因此应该避免。 甚至在“开发类库的devise指南”中的属性部分也明确指出,属性获取者不应该抛出exception:

    避免从属性获取器中抛出exception。

    属性获取者应该是没有任何先决条件的简单操作。 如果一个getter可能抛出一个exception,可以考虑重新devise这个属性作为一个方法。

  2. 编译器自动优化会受到影响,即内联和分支预测。 有关详细解释,请参阅Bill K的答案 。

这些结论是:
对于每个懒惰实施的单一财产,您应该考虑这些要点。
这意味着,这是一个每个案件的决定,不能被视为一般的最佳做法。

这种模式有其自己的位置,但是在实现类时不是最佳实践。 由于上述原因, 不应无条件使用


在本节中,我想讨论一些其他人提出的无条件使用惰性初始化的参数:

  1. 连载:
    EricJ在一个评论中说:

    一个可能序列化的对象在反序列化时不会调用它的构造器(取决于序列化器,但许多常见的行为就像这样)。 将初始化代码放入构造器意味着您必须为反序列化提供额外的支持。 这种模式避免了特殊的编码。

    这个论点有几个问题:

    1. 大多数对象永远不会被序列化。 在不需要的时候添加某种支持就会违反YAGNI 。
    2. 当一个类需要支持序列化的时候,有一些方法可以在没有解决方法的情况下启用它,而乍一看没有任何与序列化有关的方法。
  2. 微观优化:你的主要观点是,只有当有人实际访问它们时才想要构​​build对象。 所以你实际上是在谈论优化内存使用。
    我不同意这个说法,原因如下:

    1. 在大多数情况下,记忆中的一些物体对任何事物都没有影响。 现代计算机有足够的内存。 如果没有一个由事件探查器证实的实际问题,这是过时的优化 ,有很好的理由反对它。
    2. 我承认有时这种优化是有道理的。 但即使在这些情况下,懒惰初始化似乎也不是正确的解决scheme。 有两个原因反对它:

      1. 延迟初始化可能会损害性能。 也许只是微乎其微,但正如比尔的答案所显示的那样,其影响比乍看起来要大。 所以这种方法基本上是交换性能与内存。
      2. 如果你有一个通用的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的实例,那么我懒得去做没有什么错误。