简体   繁体   中英

C# 4 Lazy Loading & Lazy<T>

I have a cart model class that has a List property like so:

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

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

This is fine, unless the service that's used to query the data from SQL Server returns null, in which case, the database can be unnecessarily hit multiple times as CartItems is referenced. I then noticed that Lazy<T> was available to me, so I attempted to modify my code slightly (since Lazy<T> accounts for null and would prevent multiple hits to the database)

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

The compile time error is

"A field initializer cannot reference the non-static field, method, or property"

Services is a public property in the same class as CartItems but I can't figure out if it's even possible to access that in a Func<List<CartItem>> delegate. I do not want to have to create factory classes for each property - I have to us something like this in many places, and I want to be ... well.... lazy.

To answer your question in a comment:

I'm curious now as to why it works in the constructor and not in my example.

The order of construction of a C# object goes like this. First all the field initializers are executed, in order from most to least derived class . So if you have a class B, and a derived class D, and you create a new D, then the field initializers of D all run before any field initializer of B.

Once all the field initializers have run then the constructors run in order from least derived to most derived . That is, first the constructor body of B runs, then the constructor body of D runs.

This might seem odd, but the reasoning is straightforward:

  • First off, clearly the base class ctor bodies must run before the derived class ctor bodies. The derived ctors might depend on state initialized by the base class ctor, but the opposite is unlikely to be true; the base class typically does not know about the derived class.

  • Second, clearly it is expected that initialized fields have their values before the constructor bodies run. It would be very strange for a constructor to observe an uninitialized field when there is a field initializer right there.

  • Therefore, the way we code generate ctors is that every ctor follows the pattern: "Initialize my fields, then call my base class ctor, then execute my ctor". Since everyone follows that pattern, all the fields are initialized in order from derived to base, and all the ctor bodies are run from base to derived.

OK, so now that we've established that, what happens when a field initializer references "this" either explicitly or implicitly ? Why would you do that? "this" is probably going to be used to access a method, field, property or event on an object whose field initializers have not all run, and none of the constructor bodies have run! Clearly that is incredibly dangerous . Most of the state required for correct operation of the class is still missing.

Therefore we disallow any reference to "this" in any field initializer.

By the time you get to a particular constructor body, you know that all the field initializers have run and that all the constructors for all your base classes have run. You have much more evidence that the object is likely to be in a good state, so "this" access is allowed in a constructor body. (You can still do foolishly dangerous things; for example, you could call a virtual method overridden in a derived class when inside a base class constructor; the derived constructor has not run yet and the derived method might fail as a result. Be careful!)

Now you might quite reasonably say that there is a big difference between:

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

and

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

or similarly:

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

That doesn't call anything. It doesn't read any state that might be uninitialized. Clearly this is perfectly safe. We could make a rule that says "Allow this access in a field initializer when the access is in a method-group-to-delegate conversion or a lambda", right?

Nope. That rule allows for an end-run around the safety system:

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

And we're back in the same boat again. So we could make a rule that says "allow this access in a field intializer when the access is in a method-group-to-delegate-conversion or a lambda and a flow analyzer can prove that the delegate is not invoked before the constructor runs" and hey, now the language design team and the compiler implementation, development, testing and user education teams are spending an enormous amount of time, money and effort on solving a problem that we don't want to solve in the first place.

It is better for the language to have simple, understandable rules that promote safety and can be correctly implemented, easily tested and clearly documented than to have complex rules that allow for obscure scenarios to work. The simple, safe rule is an instance field initializer cannot have any explicit or implicit reference to 'this', period.

Further reading:

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

You can create the field in a constructor. It might also pay to move the service call to it's own method. Ie

private readonly Lazy<List<CartItem>> _cartItems;

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

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

Section 10.4.5.2 of the C# language specification explicitly forbids using this in a field initializer:

A variable initializer for an instance field cannot reference the instance being created. Thus, it is a compile-time error to reference this in a variable initializer, as it is a compile-time error for a variable initializer to reference any instance member through a simple-name .

The documentation for error CS0236 provides the constructor workaround others have recommended.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM