C# 4中的惰性加载和Lazy<T>

4
我是一位有用的助手,可以为您翻译此文本。
我有一个购物车模型类,其中有一个列表属性,就像这样:

public List<CartItem> CartItems
{
    get
    {
        if (_cartItems == null)
            _cartItems = Services.CartItemService.GetCartItems();

        return _cartItems;
    }
}
private List<CartItem> _cartItems;

这是很好的,除非用于从SQL Server查询数据的服务返回null,在这种情况下,因为引用了CartItems,数据库可能会被不必要地多次访问。我注意到Lazy<T>对我可用,因此我试图稍微修改我的代码(由于Lazy<T>考虑到null并且可以防止多次访问数据库)

public List<CartItem> CartItems
{
    get
    {
        return _cartItems.Value;
    }
}

private Lazy<List<CartItem>> _cartItems = new Lazy<List<CartItem>>(() =>
{
    // return Services.CartItemService.GetCartItems(); cannot be called here :(
});

编译时错误是:

"字段初始值设定项不能引用非静态字段、方法或属性"

在同一类中,Services 是公共属性,但我无法确定在 Func<List<CartItem>> 委托中是否有可能访问它。我不想为每个属性创建工厂类——在许多地方都要使用这样的代码,所以我想要偷懒。

1
Lazy<T> 本身并不完美。从 GetCartItems 返回 null 是一个好的设计选择吗? - Jon
我得到了这个错误:"字段初始化器不能引用非静态字段、方法或属性"。 - Chris Klepeis
1
@Jon - 目前我们实际上不会返回 null,但我开始考虑一些情况,比如它不是一个 List,而是一个 Address 实例。如果客户没有地址信息,数据访问类会返回一个空的 Address 吗?在我看来,答案是否定的,但我总是乐于接受设计想法。 - Chris Klepeis
@ChrisKlepeis:不确定Address与购物车商品清单有何关系 :) 换句话说:如何可能不知道购物车中有哪些商品(“显然”,null表示“不知道”,因为空购物车“显然”是一个空列表)?如果不知道,你是否会对此毫不在意,而不抛出异常? - Jon
1
同意Jon的观点。当我处理这种情况时,我总是尝试返回一个空列表而不是null,以避免抛出异常。 - Dirk Dastardly
3个回答

22

回答您在评论中的问题:

我现在很好奇为什么它在构造函数中有效,而在我的示例中无效。

C#对象的构造顺序如下。首先,按从最多派生类最少派生类的顺序执行所有字段初始化程序。因此,如果您有一个类B和一个派生类D,并创建一个新的D,则D的所有字段初始化程序都会在B的任何字段初始化程序之前运行。

一旦所有字段初始化程序都运行完毕,那么构造函数将按最少派生到最多派生的顺序运行。也就是说,首先运行B的构造函数体,然后运行D的构造函数体。

这可能看起来很奇怪,但推理很简单:

  • 首先,显然基类ctor body必须在派生类ctor body之前运行。派生ctor可能依赖于由基类ctor初始化的状态,但反之不太可能成立; 基类通常不知道派生类。

  • 其次,显然期望在构造函数体运行之前已经初始化了字段的值。当存在字段初始化程序时,构造函数观察未初始化的字段非常奇怪。

  • 因此,我们编写代码生成ctor的方式是每个ctor遵循以下模式:“初始化我的字段,然后调用我的基类ctor,然后执行我的ctor”。由于每个人都遵循该模式,所有字段按从派生到基础的顺序进行初始化,并且所有ctor body都从基础运行到派生。

好的,既然我们已经确定了这一点,那么当字段初始化器显式或隐式地引用"this"时会发生什么?为什么要这样做?"this"可能被用来访问一个对象上的方法、字段、属性或事件,而该对象的字段初始化器尚未全部运行,并且没有任何构造函数体运行!显然这是极其危险的。类正确操作所需的大部分状态仍然缺失。

因此,在任何字段初始化器中,我们都不允许引用"this"。

当您到达特定的构造函数体时,您知道所有的字段初始化器都已运行,并且所有基类的构造函数都已运行。您有更多的证据表明对象很可能处于良好状态,因此在构造函数体中允许访问"this"。(您仍然可以做一些愚蠢危险的事情;例如,在基类构造函数内部调用在派生类中重写的虚拟方法;派生的构造函数尚未运行,因此派生方法可能失败。请小心!)

现在您可能会合理地说,以下两种情况之间存在很大的差异:

class D : B
{
    int x = this.Whatever(); // call a method on the base class, whose ctor has not run!

并且

class D : B
{
    Func<int> f = this.Whatever;

或类似地:

class D : B
{
    Func<int> f = ()=>this.Whatever();

这并没有调用任何东西。它也没有读取可能未初始化的任何状态。显然这是完全安全的。我们可以制定一条规则,即“当访问在方法组转换或 lambda 中时,在字段初始化程序中允许此访问”,对吗?
不行。这个规则会绕过安全系统:
class D : B
{
    int x = ((Func<int>)this.Whatever)();

我们又回到了同样的困境。所以我们可以制定一条规则,即“当访问在方法组转换为委托或 Lambda 时,并且流分析器可以证明在构造函数运行之前未调用委托时,在字段初始化器中允许此访问”,嘿,现在语言设计团队和编译器实现、开发、测试和用户教育团队正在花费大量时间、金钱和精力来解决我们首先不想要解决的问题。
相比于允许复杂情况的复杂规则,更好的做法是使语言具有简单易懂的规则,促进安全并能够被正确实现、轻松测试和清晰记录。简单而安全的规则是:实例字段初始化器不能有任何显式或隐式引用“this”。
进一步阅读:

http://blogs.msdn.com/b/ericlippert/archive/2008/02/15/why-do-initializers-run-in-the-opposite-order-as-constructors-part-one.aspx

http://blogs.msdn.com/b/ericlippert/archive/2008/02/18/why-do-initializers-run-in-the-opposite-order-as-constructors-part-two.aspx


8

您可以在构造函数中创建该字段。 同时,将服务调用移至其自己的方法可能也会更好。 例如:

private readonly Lazy<List<CartItem>> _cartItems;

public MyClass()
{
    _cartItems = new Lazy<List<CartItem>>(GetCartItems);
}

public List<CartItem> GetCartItems()
{
    return Services.CartItemService.GetCartItems();
}

嗯,这里捕获的是什么,this 还是 Services 实例?我猜是前者。 - leppie
太棒了。这似乎已经解决了编译时错误。但我现在很好奇,为什么它在构造函数中起作用而不在我的示例中。 - Chris Klepeis
@leppie 我也是,所以小心如何填充服务。 - Ray
如果 @ChrisKlepeis 所说的是真的,那么我的怀疑是正确的 :) - leppie
1
我刚刚验证了它可以在字段和属性上捕获this - leppie
显示剩余2条评论

3

C#语言规范的10.4.5.2节明确禁止在字段初始化器中使用this

实例字段的变量初始化器不能引用正在创建的实例。因此,在变量初始化器中引用this是编译时错误,因为变量初始化器通过简单名称引用任何实例成员也是编译时错误。

CS0236错误的文档提供了其他人推荐的构造函数解决方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接