C#4 Lazy Loading&amp;懒惰<T> </T>

时间:2011-10-27 14:26:02

标签: c# .net c#-4.0 lazy-loading

我有一个购物车模型类,它具有如下所示的List属性:

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 :(
});

编译时错误是

  

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

服务是与CartItems在同一类中的公共属性,但我无法弄清楚是否可以在Func<List<CartItem>>委托中访问它。我不想为每个属性创建工厂类 - 我在很多地方都有这样的东西,我想......好......懒惰。

3 个答案:

答案 0 :(得分:22)

在评论中回答您的问题:

  

我现在好奇它为什么在构造函数中工作而不是在我的例子中。

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

一旦所有字段初始值设定项都已运行,则构造函数按照从最少派生到大多数派生的顺序运行。也就是说,首先运行B的构造函数体,然后运行D的构造函数体。

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

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

  • 其次,很明显,在构造函数体运行之前,初始化字段的值为。当有一个字段初始化器时,构造函数观察未初始化的字段会非常奇怪。

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

好的,现在我们已经确定了,当字段初始化程序明确或隐含地引用“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

答案 1 :(得分:7)

您可以在构造函数中创建字段。 将服务调用移动到它自己的方法也可能需要付费。 即

private readonly Lazy<List<CartItem>> _cartItems;

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

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

答案 2 :(得分:3)

Section 10.4.5.2 of the C# language specification明确禁止在字段初始值设定项中使用this

  

实例字段的变量初始值设定项无法引用正在创建的实例。因此,在变量初始化程序中引用this是编译时错误,因为变量初始化程序通过简单名称引用任何实例成员是编译时错误

documentation for error CS0236提供了其他人推荐的构造函数解决方法。