关于c#:好的还是坏的做法? 在getter中初始化对象

Good or bad practice? Initializing objects in getter

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[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
  • 延迟或避免加载"重"物体

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

最后更新:

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


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

简短回答:

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

背景和解释:

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

  • 它违反了最低惊喜原则(POLS)。将值分配给属性时,应返回此值。在您的实现中,null不是这种情况:

    1
    2
    foo.Bar = null;
    Assert.Null(foo.Bar); // This will fail
  • 它引入了一些线程问题:在不同线程上有两个foo.Bar的调用者可能会获得两个不同的Bar实例,其中一个实例没有连接到Foo实例。对该Bar实例所做的任何更改都会无声地丢失。
    这是违反POLS的另一个案例。当只访问属性的存储值时,它应该是线程安全的。虽然你可以说这个类本身不是线程安全的 - 包括你的属性的getter - 你必须正确记录这个,因为这不是正常的情况。此外,我们将很快看到这个问题的引入是不必要的。
  • 一般来说:
    现在是时候看一般的懒惰初始化了:
    延迟初始化通常用于延迟构建需要很长时间构建的对象,或者一旦完全构造就占用大量内存。
    这是使用延迟初始化的一个非常有效的原因。

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

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

  • 对象的构造可能不成功,导致属性getter的异常。这是对POLS的又一次违反,因此应该避免。甚至"开发类库的设计指南"中的属性部分也明确指出属性getter不应抛出异常:

    Avoid throwing exceptions from property getters.

    Ok.

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

    Ok.

  • 编译器的自动优化是有害的,即内联和分支预测。有关详细说明,请参阅Bill K的答案。

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

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

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

  • 连载:
    EricJ在一条评论中说:

    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.

    Ok.

    这个论点有几个问题:

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

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

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


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

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

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

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

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

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

    这条规则有例外。

    关于"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这样的简单类使用进行延迟初始化。

    在评论懒惰之前

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

    在评论微优化之前

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

    关于用户界面

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

    "二传手"

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

    集合

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

    复杂类

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

    最后

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


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

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

    • http://msdn.microsoft.com/en-us/library/dd642331.aspx

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


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

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


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


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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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方法如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
        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预测问题的例子(您反复发生,仅发生一次),请参阅这个令人敬畏的问题的第一个答案:为什么处理排序数组比处理未排序数组更快?


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


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

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

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


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

    对我来说,让Foo实例化任何东西似乎都很臭(尽管不一定是错误的)。除非Foo明确表示要成为工厂,否则它不应该实例化它自己的协作者,而是将它们注入构造函数中。

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