首页 文章

好的还是坏的做法?在getter中初始化对象

提问于
浏览
165

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

[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并且字段仍为null时初始化任何字段 . 我认为这可以通过不初始化任何地方没有使用的任何属性来减少过载 .

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

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

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

缺点:

  • 线程安全问题

  • 当传递的值为null时,不服从"setter"请求

  • 微优化

  • 异常处理应该在构造函数中进行

  • 需要在类'代码中检查null

优点:

  • 微优化

  • 属性永远不会返回null

  • 延迟或避免加载"heavy"个对象

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

最后更新:

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

9 回答

  • 169

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

    简答:

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

    背景和解释:

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

    foo.Bar = null;
    Assert.Null(foo.Bar); // This will fail
    
    • 它引入了一些线程问题:在不同线程上的两个 foo.Bar 调用者可能会获得 Bar 的两个不同实例,其中一个实例将没有连接到 Foo 实例 . 对 Bar 实例所做的任何更改都会无声地丢失 .
      这是违反POLS的另一个案例 . 当只访问属性的存储值时,它应该是线程安全的 . 虽然你可以说这个课程并不是正常情况 . 此外,我们将很快看到这个问题的引入是不必要的 .

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

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

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

    • 对象的构造可能不成功,导致属性getter的异常 . 这是对POLS的又一次违反,因此应该避免 . 甚至"Design Guidelines for Developing Class Libraries"中的section on properties也明确指出属性getter不应该抛出异常:

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

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

    The conclusion of these points is the following:
    对于懒惰实施的每个单一属性,您应该考虑这些要点 .
    这意味着,这是一个案例决定,不能作为一般的最佳实践 .

    This pattern has its place, but it is not a general best practice when implementing classes. It should not be used unconditionally ,由于上述原因 .


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

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

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

    这个论点有几个问题:

    • 大多数对象永远不会被序列化 . 在不需要时为其添加某种支持会违反YAGNI .

    • 当类需要支持序列化时,存在启用它的方法,而没有与第一眼看上去没有任何关系的变通方法 .

    • 微优化:您的主要论点是您只想在有人实际访问它们时构造对象 . 所以你实际上是在谈论优化内存使用情况 .
      我不同意这个论点,理由如下:

    • 在大多数情况下,内存中的更多对象对任何事物都没有任何影响 . 现代计算机有足够的内存 . 如果没有分析器确认的实际问题,这是pre-mature optimization并且有充分的理由反对它 .

    • 我承认有时候这种优化是合理的 . 但即使在这些情况下,延迟初始化似乎也不是正确的解决方案 . 反对它的原因有两个:

    • 延迟初始化可能会损害性能 . 也许只是轻微的,但正如比尔的回答所显示的那样,影响比乍看之下的影响要大 . 因此,这种方法基本上交换了性能与内存 .

    • 如果你的设计只是使用了类的一部分是一个常见的用例,这就暗示了设计本身的问题:有问题的类很可能有多个责任 . 解决方案是将类拆分为几个更集中的类 .

  • 8

    It is a good design choice. Strongly recommended for library code or core classes.

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

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

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

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

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

    这条规则有例外 .

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

    基元数据类型和简单类

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

    在评论懒惰之前

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

    在评论微优化之前

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

    关于用户界面

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

    “塞特犬”

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

    收藏

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

    复杂类

    让我以不同的方式重复这一点,对复杂类使用延迟初始化 . 通常是设计不良的课程 .

    最后

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

  • 4

    您是否考虑使用 Lazy<T> 实现此类模式?

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

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

  • 49

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

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

  • 17

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

  • 9

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

  • 2

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

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

    关于对象的最好的事情之一是它提供了一个安全,可信赖的环境 . 最好的情况是如果你创建尽可能多的字段“最终”,用构造函数填充它们 . 这使你的课程非常防弹 . 允许通过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预测问题的例子(您反复发生,仅发生一次),请参阅这个令人敬畏的问题的第一个答案:Why is it faster to process a sorted array than an unsorted array?

  • 8

    让我再向其他人提出的许多好点再补充一点......

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

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

  • 9

    你确定Foo应该实例化任何东西吗?

    对我来说,让Foo实例化任何东西似乎都很臭(尽管不一定是错误的) . 除非是Foo 's express purpose to be a factory, it should not instantiate it'自己的合作者,但instead get them injected in its constructor .

    但是,如果Foo的目的是创建Bar类型的实例,那么我没有看到懒惰地做这件事有什么不妥 .

相关问题