好坏练习?在getter中初始化对象

Joh*_*mse 167 c# getter coding-style

我似乎有一种奇怪的习惯......据我的同事说,至少.我们一直在一个小项目上工作.我编写类的方式是(简化示例):

[Serializable()]
public class Foo
{
    public Foo()
    { }

    private Bar _bar;

    public Bar Bar
    {
        get
        {
            if (_bar == null)
                _bar = new Bar();

            return _bar;
        }
        set { _bar = value; }
    }
}
Run Code Online (Sandbox Code Playgroud)

所以,基本上,我只在调用getter并且字段仍然为null时初始化任何字段.我认为这可以通过不初始化任何地方没有使用的任何属性来减少过载.

ETA:我这样做的原因是我的类有几个属性返回另一个类的实例,而这个属性又具有更多类的属性,依此类推.调用顶级类的构造函数随后将调用所有这些类的所有构造函数,而不是总是需要它们.

除个人偏​​好外,是否有任何反对这种做法的反对意见?

更新:我已经考虑了很多关于这个问题的不同意见,我将坚持我接受的答案.但是,我现在对这个概念有了更好的理解,我能够决定何时使用它,何时不能.

缺点:

  • 线程安全问题
  • 当传递的值为null时,不遵守"setter"请求
  • 微优化
  • 异常处理应该在构造函数中进行
  • 需要在类'代码中检查null

优点:

  • 微优化
  • 属性永远不会返回null
  • 延迟或避免加载"重"物体

大多数缺点不适用于我当前的库,但是我必须测试"微优化"是否实际上是在优化任何东西.

最后更新:

好的,我改变了答案.我最初的问题是这是否是一个好习惯.我现在确信它不是.也许我仍会在我当前代码的某些部分使用它,但不是无条件的,绝对不是所有的时间.因此,在使用它之前,我会失去习惯并思考它.感谢大家!

Dan*_*rth 170

你在这里有一个 - 天真 - "懒惰初始化"的实现.

简短回答:

无条件地使用延迟初始化不是一个好主意.它有它的位置,但必须考虑到这个解决方案的影响.

背景和解释:

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

  1. 它违反了最低惊喜原则(POLS).将值分配给属性时,应返回此值.在您的实现中,情况并非如此null:

    foo.Bar = null;
    Assert.Null(foo.Bar); // This will fail
    
    Run Code Online (Sandbox Code Playgroud)
  2. 它引入了一些线程问题:foo.Bar不同线程上的两个调用者可能会获得两个不同的实例,Bar其中一个将没有与Foo实例的连接.对该Bar实例所做的任何更改都会默默丢失.
    这是违反POLS的另一个案例.当只访问属性的存储值时,它应该是线程安全的.虽然你可以说这个类本身不是线程安全的 - 包括你的属性的getter - 你必须正确记录这个,因为这不是正常的情况.此外,我们将很快看到这个问题的引入是不必要的.

一般来说:
现在是时候看一般的
延迟初始化:延迟初始化通常用于延迟构建需要很长时间构建的对象,或者在完全构造后占用大量内存.
这是使用延迟初始化的一个非常有效的原因.

但是,这些属性通常没有setter,这摆脱了上面提到的第一个问题.
此外,将使用线程安全的实现 - 比如Lazy<T>- 以避免第二个问题.

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

  1. 对象的构造可能不成功,导致属性getter的异常.这是对POLS的又一次违反,因此应该避免.即使是在属性部分 "为类库开发设计指南"中明确规定,属性获取不应该抛出异常:

    避免从属性getter中抛出异常.

    属性getter应该是简单的操作,没有任何先决条件.如果getter可能抛出异常,请考虑将该属性重新设计为方法.

  2. 编译器的自动优化受到损害,即内联和分支预测.有关详细说明,请参阅Bill K的答案.

这些要点的结论如下:
对于懒惰实施的每个单一属性,您应该考虑这些要点.
这意味着,这是一个案例决定,不能作为一般的最佳实践.

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


在本节中,我想讨论其他人作为无条件使用延迟初始化的参数提出的一些观点:

  1. 序列化:
    EricJ在一条评论中说:

    可以序列化的对象在反序列化时不会调用它的构造函数(取决于序列化程序,但许多常见的行为都是这样的).将初始化代码放在构造函数中意味着您必须为反序列化提供额外的支持.这种模式避免了这种特殊的编码.

    这个论点有几个问题:

    1. 大多数对象永远不会被序列化.在不需要时为其添加某种支持会违反YAGNI.
    2. 当一个类需要支持序列化时,存在启用它的方法,而没有与第一眼看上去与序列化无关的解决方法.
  2. 微优化:您的主要论点是您只想在有人实际访问它们时构造对象.所以你实际上是在谈论优化内存使用情况.
    我不同意这个论点,原因如下:

    1. 在大多数情况下,内存中的一些对象对任何事物都没有任何影响.现代计算机有足够的内存.如果没有分析器确认的实际问题,这是预先成熟的优化,并且有充分的理由反对它.
    2. 我承认有时候这种优化是合理的.但即使在这些情况下,延迟初始化似乎也不是正确的解决方案.反对它的原因有两个:

      1. 延迟初始化可能会损害性能.也许只是轻微的,但正如比尔的回答所显示的那样,影响比乍看之下的影响要大.所以这种方法基本上交换了性能与内存.
      2. 如果你的设计只是部分类的常见用例,这暗示了设计本身的问题:有问题的类很可能有不止一个责任.解决方案是将类拆分为几个更集中的类.

  • @JohnWillemse或许可以认为这是一个过早优化的案例.除非你有_measured_性能/内存瓶颈,否则我建议不要这样做,因为它增加了复杂性并引入了线程问题. (26认同)
  • @JohnWillemse:这是你的架构问题.您应该以更小,更专注的方式重构您的课程.不要为5个不同的事物/任务创建一个类.改为创建5个类. (4认同)
  • +1,对于95%的课程来说这不是一个好的设计选择.延迟初始化有其优点,但不应针对所有属性进行推广.它增加了复杂性,难以阅读代码,线程安全问题......在99%的情况下都没有可察觉的优化.此外,正如SolutionYogi所说,OP的代码是错误的,这证明这种模式实现起来并不容易,除非实际需要延迟初始化,否则应该避免使用. (2认同)
  • @DanielHilgarth感谢您一路上写下(几乎)无条件使用此模式的所有错误.很好! (2认同)

AMi*_*ico 49

这是一个很好的设计选择.强烈推荐用于库代码或核心类.

它通过一些"延迟初始化"或"延迟初始化"来调用,并且通常认为它是一个很好的设计选择.

首先,如果在类级别变量或构造函数的声明中初始化,那么在构造对象时,您将有创建可能永远不会使用的资源的开销.

其次,只有在需要时才会创建资源.

第三,避免垃圾收集未使用的对象.

最后,更容易处理属性中可能出现的初始化异常,然后处理类级变量或构造函数初始化期间发生的异常.

这条规则有例外.

关于"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,则必须使用问题中说明的代码模式.这种代码模式已经变得非常普遍,以至于微软认为在最新的.NET库中包含一个类可以更容易地实现该模式.此外,如果您的实现需要线程安全,那么您必须添加它.

原始数据类型和简单类

显而易见,您不会对原始数据类型或简单类使用进行惰性初始化List<string>.

在评论懒惰之前

Lazy<T> 在.NET 4.0中引入,所以请不要添加关于此类的其他评论.

在评论微优化之前

在构建库时,必须考虑所有优化.例如,在.NET类中,您将看到整个代码中用于布尔类变量的位数组,以减少内存消耗和内存碎片,仅举两个"微优化".

关于用户界面

您不会对用户界面直接使用的类使用延迟初始化.上周,我花了大部分时间来删除在组合框的视图模型中使用的八个集合的延迟加载.我有一个LookupManager处理任何用户界面元素所需的集合的延迟加载和缓存.

"二传手"

我从未对任何延迟加载的属性使用set-property("setters").因此,你永远不会允许foo.Bar = null;.如果你需要设置Bar然后我会创建一个调用的方法,SetBar(Bar value)而不是使用延迟初始化

集合

声明时,类集合属性始终初始化,因为它们永远不应为null.

复杂类

让我重复一遍,你对复杂的类使用延迟初始化.通常是设计不良的课程.

最后

我从来没有说过要为所有课程或所有情况都这样做.这是一个坏习惯.

  • 我认为这是一个不好的经验法则,没有多少考虑因素.除非Bar是一个知道资源,否则这是一个不必要的微优化.在Bar是资源密集型的情况下,.net中内置了线程安全的Lazy <T>. (25认同)
  • 我真的很担心其他开发者会看到这个答案,并认为这确实是一个很好的做法(哦,小男孩).如果您无条件使用它,这是一种非常糟糕的做法.除了已经说过的话,你们每个人的生活都会变得更加困难(对于客户开发人员和维护开发人员来说)获得如此微不足道的收益(如果有任何收益的话).你应该从专业人士那里听到:Donald Knuth,在"计算机程序设计艺术"系列中,着名地说"过早优化是所有邪恶的根源." 你正在做的不仅是邪恶,而且是恶作剧! (20认同)
  • "更容易处理属性中可能出现的初始化异常,然后在类级别变量或构造函数初始化期间发生异常." - 好吧,这很傻.如果某个对象由于某种原因无法初始化,我想尽快知道.即它在构建时立即.对于使用延迟初始化有很好的论据,但我不认为使用它*普遍*是一个好主意. (10认同)
  • 如果你可以在不同的线程中多次调用foo.Bar而没有任何干预值设置但得到不同的值,那么你就有一个糟糕的穷人类. (6认同)
  • 有很多指标你选择了错误的答案(以及错误的编程决定).你的名单中的专业人士比专业人士更多.你有更多的人担保它而不是它.在这个问题上发布的这个网站(@BillK和@DanielHilgarth)经验丰富的成员反对它.你的同事已经告诉你这是错的.说真的,这是错的!如果我抓住我的团队的一名开发人员(我是团队领导)这样做,他将花费5分钟的超时时间,然后进行演讲,为什么他不应该这样做. (4认同)
  • Con列表中的另一个点,以及"库代码"推理的计数器参数是您的消费者不会意识到所有属性都是Lazy加载的.他们可能会合理地期望一个属性立即返回(例如在UI绑定中),在你的代码中,Bar的延迟初始化很容易变得非常重要.你应该考虑改变接受的答案和练习. (3认同)
  • @Peri我很困惑,人们在不考虑后果的情况下不断提高这个答案.用户在他的问题中明确指出"这是一种奇怪的习惯".我觉得新开发者的可怜的灵魂将会学到很难的原因,为什么这不仅是奇怪的,而且确实是一种非常糟糕的习惯**. (3认同)
  • -1,对于95%的课程来说,这不是一个好的设计选择.延迟初始化有其优点,但不应针对所有属性进行推广.它增加了复杂性,难以阅读代码,线程安全问题......在99%的情况下都没有可察觉的优化.此外,正如SolutionYogi所说,OP的代码是错误的,这证明这种模式实现起来并不容易,除非你真的需要延迟初始化,否则应该避免. (3认同)
  • 我将尝试在*框架设计指南*中找到推荐这种做法的参考. (2认同)
  • @ach不,在使用`Foo`时,知道`Bar`是否是资源占用增加了它们之间的心理耦合.最好保持简单并使用这种模式(无论是在问题中发布还是使用`Lazy <T>`;只是不要在构造函数中执行它(这是问题所在)). (2认同)
  • 从来没有见过这么多错误的回答有这么多的赞成并被接受作为答案! (2认同)

Mat*_*zer 17

你考虑使用这种模式Lazy<T>吗?

除了轻松创建延迟加载的对象之外,还可以在初始化对象时获得线程安全性:

正如其他人所说,如果对象非常耗费资源,或者在对象构建期间加载它们需要一些时间,那么就懒得加载对象.


Lui*_*lez 9

我可以看到的缺点是,如果你想询问Bars是否为null,它将永远不会,并且你将在那里创建列表.

  • @PeterPorfy:它违反了[POLS](http://en.wikipedia.org/wiki/Principle_of_least_surprise).你把'null`放入,但不要把它取回来.通常情况下,您假设您获得与放入房产相同的价值. (6认同)
  • @AMissico:它不是一个概念化的.与推动前门旁边的按钮预期敲响门铃的方式大致相同,看起来像属性的东西预计会表现得像属性一样.打开脚下的活板门是一种令人惊讶的行为,特别是如果按钮没有这样标记的话. (6认同)

Col*_*kay 9

我认为这取决于你的初始化.我可能不会为列表做这个,因为构建成本非常小,所以它可以进入构造函数.但如果它是一个预先填充的列表,那么我可能不会在第一次需要它之前.

基本上,如果构建成本超过对每个访问进行条件检查的成本,那么懒惰创建它.如果没有,请在构造函数中执行.


Tor*_*mod 8

惰性实例化/初始化是一种完全可行的模式.但请记住,作为一般规则,API的消费者不希望getter和setter从最终用户POV(或失败)中获取可辨别的时间.


Bil*_*l K 8

我只想对丹尼尔的回答发表评论,但老实说,我认为这远远不够.

虽然这是在某些情况下使用的非常好的模式(例如,当从数据库初始化对象时),但这是一个可怕的习惯.

关于对象的最好的事情之一是它提供了一个安全,可信赖的环境.最好的情况是如果你创建尽可能多的字段"Final",用构造函数填充它们.这使你的课程非常防弹.允许通过setter更改字段的情况稍微少一些,但并不可怕.例如:

class SafeClass
{
    String name="";
    Integer age=0;

    public void setName(String newName)
    {
        assert(newName != null)
        name=newName;
    }// follow this pattern for age
    ...
    public String toString() {
        String s="Safe Class has name:"+name+" and age:"+age
    }
}

使用您的模式,toString方法如下所示:

    if(name == null)
        throw new IllegalStateException("SafeClass got into an illegal state! name is null")
    if(age == null)
        throw new IllegalStateException("SafeClass got into an illegal state! age is null")

    public String toString() {
        String s="Safe Class has name:"+name+" and age:"+age
    }

不仅如此,你需要在你的类中使用该对象的任何地方进行空检查(由于getter中的null检查,你的类之外是安全的,但你应该主要在类中使用你的类成员)

此外,你的类永远处于不确定的状态 - 例如,如果你决定通过添加一些注释使该类成为一个hibernate类,你会怎么做?

如果你根据一些没有要求和测试的微观验证做出任何决定,那几乎肯定是错误的决定.实际上,即使在最理想的情况下,你的模式实际上很有可能实际上减慢了系统的速度,因为if语句会导致CPU上的分支预测失败,这将使事情减慢许多倍.只是在构造函数中指定一个值,除非您创建的对象相当复杂或来自远程数据源.

有关brance预测问题的例子(您反复发生,仅发生一次),请参阅这个令人敬畏的问题的第一个答案:为什么处理排序数组比处理未排序数组更快?


Bra*_*vic 5

让我在其他人提出的许多优点中再补充一点......

调试器将(默认情况下)在单步执行代码时评估属性,这可能Bar比仅执行代码通常更快地实例化属性。换句话说,调试的行为就是改变程序的执行。

这可能是也可能不是问题(取决于副作用),但需要注意。