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

我有个奇怪的习惯,至少我同事是这么说的。我们一起做了一个小项目。我写这些类的方式是(简化的例子) :

[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 时初始化任何字段,而该字段仍然为空。我认为这样可以通过不初始化任何不在任何地方使用的属性来减少重载。

埃塔: 我这样做的原因是,我的类有几个属性,这些属性返回另一个类的实例,而这些实例又有属性和更多的类,等等。调用 top 类的构造函数将随后调用所有这些类的所有构造函数,当它们不是所有需要的 一直都是时。

除了个人偏好外,是否还有其他反对意见?

更新: 关于这个问题,我已经考虑了许多不同的意见,我将坚持我接受的答案。然而,我现在对这个概念有了更好的理解,我能够决定什么时候使用它,什么时候不使用它。

缺点:

  • 线程安全问题
  • 当传递的值为空时不服从“ setter”请求
  • 微观优化
  • 异常处理应在构造函数中进行
  • 需要检查类代码中的 null

优点:

  • 微观优化
  • 属性永远不会返回 null
  • 延迟或避免加载“重型”对象

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

最后更新:

好吧,我改了答案。我最初的问题是这是不是一个好习惯。现在我确信不是这样的。也许我仍然会在我当前代码的某些部分使用它,但不是无条件的,而且肯定不会一直使用它。所以我要改掉我的习惯,在使用它之前好好考虑一下。谢谢大家!

26085 次浏览

你们现在看到的是一个天真的“惰性初始模式”实现。

简短的回答:

使用惰性初始模式 无条件的不是一个好主意。它有自己的位置,但人们必须考虑到这种解决方案的影响。

背景及说明:

具体执行情况:
让我们首先看看您的具体示例,以及为什么我认为它的实现很天真:

  1. 它违反了 最小惊喜原则。当将值分配给属性时,预计将返回该值。在您的实现中,null的情况并非如此:

    foo.Bar = null;
    Assert.Null(foo.Bar); // This will fail
    
  2. It introduces quite some threading issues: Two callers of foo.Bar on different threads can potentially get two different instances of Bar and one of them will be without a connection to the Foo instance. Any changes made to that Bar instance are silently lost.
    This is another case of a violation of POLS. When only the stored value of a property is accessed it is expected to be thread-safe. While you could argue that the class simply isn't thread-safe - including the getter of your property - you would have to document this properly as that's not the normal case. Furthermore the introduction of this issue is unnecessary as we will see shortly.

In general:
It's now time to look at lazy initialization in general:
Lazy initialization is usually used to delay the construction of objects that take a long time to be constructed or that take a lot of memory once fully constructed.
That is a very valid reason for using lazy initialization.

However, such properties normally don't have setters, which gets rid of the first issue pointed out above.
Furthermore, a thread-safe implementation would be used - like Lazy<T> - to avoid the second issue.

Even when considering these two points in the implementation of a lazy property, the following points are general problems of this pattern:

  1. Construction of the object could be unsuccessful, resulting in an exception from a property getter. This is yet another violation of POLS and therefore should be avoided. Even the section on properties in the "Design Guidelines for Developing Class Libraries" explicitly states that property getters shouldn't throw exceptions:

    Avoid throwing exceptions from property getters.

    Property getters should be simple operations without any preconditions. If a getter might throw an exception, consider redesigning the property to be a method.

  2. Automatic optimizations by the compiler are hurt, namely inlining and branch prediction. Please see Bill K's answer for a detailed explanation.

The conclusion of these points is the following:
For each single property that is implemented lazily, you should have considered these points.
That means, that it is a per-case decision and can't be taken as a general best practice.

This pattern has its place, but it is not a general best practice when implementing classes. It should not be used unconditionally, because of the reasons stated above.


In this section I want to discuss some of the points others have brought forward as arguments for using lazy initialization unconditionally:

  1. Serialization:
    EricJ states in one comment:

    An object that may be serialized will not have it's contructor invoked when it is deserialized (depends on the serializer, but many common ones behave like this). Putting initialization code in the constructor means that you have to provide additional support for deserialization. This pattern avoids that special coding.

    There are several problems with this argument:

    1. Most objects never will be serialized. Adding some sort of support for it when it is not needed violates YAGNI.
    2. When a class needs to support serialization there exist ways to enable it without a workaround that doesn't have anything to do with serialization at first glance.
  2. Micro-optimization: Your main argument is that you want to construct the objects only when someone actually accesses them. So you are actually talking about optimizing the memory usage.
    I don't agree with this argument for the following reasons:

    1. In most cases, a few more objects in memory have no impact whatsoever on anything. Modern computers have way enough memory. Without a case of actual problems confirmed by a profiler, this is pre-mature optimization and there are good reasons against it.
    2. I acknowledge the fact that sometimes this kind of optimization is justified. But even in these cases lazy initialization doesn't seem to be the correct solution. There are two reasons speaking against it:

      1. Lazy initialization potentially hurts performance. Maybe only marginally, but as Bill's answer showed, the impact is greater than one might think at first glance. So this approach basically trades performance versus memory.
      2. If you have a design where it is a common use case to use only parts of the class, this hints at a problem with the design itself: The class in question most likely has more than one responsibility. The solution would be to split the class into several more focused classes.

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

我认为这取决于你初始化什么。我可能不会这样做的列表,因为建设成本相当小,所以它可以进入构造函数。但是,如果它是一个预填充列表,那么我可能不会,直到它是第一次需要。

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

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

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

正如其他人所说,如果对象真的资源很多,或者在对象构造期间需要花费一些时间来加载它们,那么可以延迟加载它们。

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

它被一些“惰性初始模式”或“延迟初始化”称为,并且通常被所有人认为是一个好的设计选择。

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

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

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

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

这条规则也有例外。

关于“ get”属性中初始化的附加检查的性能参数,它是无关紧要的。初始化和释放对象比使用跳转进行简单的空指针检查对性能的影响更大。

开发类库的设计指南 < a href = “ http://msdn.microsoft.com/en-US/library/vStudio/ms229042.aspx”rel = “ noReferrer”> http://msdn.microsoft.com/en-us/library/vstudio/ms229042.aspx

关于 Lazy<T>

通用 Lazy<T>类正是为了海报想要的东西而创建的,参见 惰性初始模式 at http://msdn.microsoft.com/en-us/library/dd997286(v=vs.100).aspx。如果你有旧版本的。NET,您必须使用问题中说明的代码模式。这种代码模式已经变得如此普遍,以至于 Microsoft 认为最好在最新版本中包含一个类。NET 库,使模式的实现更加容易。另外,如果您的实现需要线程安全性,那么您必须添加它。

基本数据类型和简单类

显然,您不会对基本数据类型或简单类使用(如 List<string>)使用延迟初始化。

在评论懒惰之前

Lazy<T>是在.NET 4.0中引入的,所以请不要再添加关于这个类的评论。

在评论微观优化之前

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

关于用户界面

你不会对用户界面直接使用的类使用惰性初始模式。上周,我花了一天的大部分时间来删除组合框视图模型中使用的8个集合的延迟加载。我有一个 LookupManager,它可以处理任何用户界面元素所需的集合的延迟加载和缓存。

“塞特斯”

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

收款

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

复杂课程

让我用不同的方式重复一下,您对复杂类使用延迟初始化,这些类通常设计得很糟糕。

最后

我从来没有说过要在所有的课堂或者所有的情况下都这样做,这是一个坏习惯。

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

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

尽管在某些情况下(例如,当从数据库初始化对象时)使用这种模式非常好,但是这是一个很糟糕的习惯。

对象最好的一点是它提供了一个安全、可信的环境。最好的情况是,如果您创建尽可能多的字段“ 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 中使用了空检查,但是应该主要使用类内部的类成员)

此外,您的类永远处于一种不确定的状态——例如,如果您决定通过添加一些注释来使该类成为一个休眠类,您将如何做呢?

如果您在没有需求和测试的情况下基于某种微观优化做出任何决定,那么几乎可以肯定这是错误的决定。事实上,即使在最理想的情况下,你的模式也很有可能会减慢系统的运行速度,因为 if 语句会导致 CPU 上的分支预测失败,这会比仅仅在构造函数中赋值要慢很多很多倍,除非你创建的对象相当复杂或来自远程数据源。

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

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

对我来说,让 Foo 实例化任何东西似乎都有问题(尽管不一定是 错了)。除非 Foo 的明确目标是成为一个工厂,否则它不应该实例化自己的合作者,而应该实例化 而是把它们注入到它的构造函数中

然而,如果 Foo 的目的是创建类型 Bar 的实例,那么我认为懒惰地创建它没有什么错。

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

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

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