DEV Community

Cover image for Lazy props for the lazy!
Davyd McColl
Davyd McColl

Posted on

Lazy props for the lazy!

What is lazy evaluation?

From Wikipedia:

In programming language theory, lazy evaluation, or call-by-need is an
evaluation strategy which delays the evaluation of an expression until its
value is needed and which also avoids repeated evaluations

Why be lazy?

As per the description above, lazy evaluation results in less up-front cost and is highly beneficial when writing code where not all paths will be immediately exercised and one would like to provide a friendlier API for consuming code.

For example, consider code which has to fetch values from the database:

public interface ICustomerRepository
{
    OrderAddress[] FetchCustomerOrderAddresses(int customerId);
}

public class CustomerModel
{
    public int CustomerId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public OrderAddress[] OrderAddresses
    {
        get
        {
            if (_orderAddresses == null)
            {
                _orderAddresses = FetchOrderAddresses();
            }

            return _orderAddresses;
        }
    }

    private OrderAddress[] _orderAddresses;

    private readonly ICustomerRepository _customerRepository;

    public CustomerModel(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository;
    }

    private OrderAddress[] FetchOrderAddresses()
    {
        return _customerRepository.FetchCustomerOrderAddresses(
            CustomerId
        );
    }
}

We might have such a convenience object which we would only sometimes interrogate for recent order addresses (as one of the steps on checkout, for example). The _customerRepository would do a database request, inspecting prior orders from the customer and returning the unique set of addresses we've delivered to before.

Since we only need those addresses sometimes, we'd perfer not to always incur the cost of fetching them. On the other hand, we also don't want to fetch from the database multiple times by accident. So we use a backing field and a lazy getter. Nothing too exciting here.

Enter the null-coalescing operator (C# 2)

If you aren't familiar with it, the null-coalescing operator (??) provides a simpler syntax for the OrderAddresses property above:

public OrderAddress[] OrderAddresses =>
    _orderAddresses ?? (_orderAddresses = FetchOrderAddresses());

When this property is read, the _orderAddresses field is checked for null. If not null, it is returned, otherwise the result of

(_orderAddresses = FetchOrderAddresses());

is returned. C#, like many other languages, returns the value of the assignment when making an assignment. So this statement above not only assignes the customers order addresses to the _orderAddresses field, it also returns that value.

Side-note: the ?? operator can also be used as shorthand for throwing an exception when the left-hand-side value is null, for example:

public OrderAddress[] FetchOrderAddresses(int customerId)
{
   return DatabaseRequestForAddressesByCustomerId(customerId)
          ?? throw new CustomerHasNoSavedAddressesException(customerId);
}

Now back to your regular programming!

Today I've been recommended by Rider to use the new null-coalescing assignment operatorhttps://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operator).

Null-what-now?

As someone who makes use of the lazy property pattern a lot, I was quite pleased to see this new syntax which Rider suggested and then fixed in-place for me:

public OrderAddress[] OrderAddresses =>
    _orderAddresses ??= FetchOrderAddresses();

This works exactly like the examples above, but is really quick to write and (I think) just as clear. This operator comes with C# 8.0

But I don't have C# 8.0? Do I have to get the latest Visual Studio or Rider?

Here's the really great news: Roslyn is really, really smart -- and pluggable. One of the questions I ask prospective hires is "how can I get the latest C# features in my IDE?". Most say that I need to update my IDE or system-wide tooling, but you really don't

Because of the pluggable nature of Roslyn, you just need to add the package:

Microsoft.Net.Compilers

to your project and add this to your .csproj file:

<PropertyGroup>
  <LangVersion>latest</LangVersion>
</PropertyGroup>

(if you already have a LangVersion node in one or another property group(s), you can modify there -- but having a single PropertyGroup for this setting means you don't have to specify it for each build configuration in your .csproj.

With those two requirements met, you may now write C# with all of the latest and greatest features -- and there are plenty more beyond ??=.

Top comments (2)

Collapse
 
peledzohar profile image
Zohar Peled • Edited

The null-coalescing assignment operator really does seem like a good choice, but I wonder: is it better than using the Lazy<T> class already available since .Net 4?

BTW, thanks a lot for the c#8 tip! I'll be sure to try it out soon.

Collapse
 
fluffynuts profile image
Davyd McColl

I've always been wary of Lazy -- and I have a good example of why:

Lazy will capture whatever is in the given func which resulted in this oddness during testing:

A colleague had a Lazy prop which pulled information out of application config (ConfigurationManager). The testing environment sets up temporary redis and mysql databases (actually spins up new instances of the processes) and rewrites the app config with the applicable ports to connect on. ConfigurationManager is quite aggressive with caching, so in order to get updated config in the rest of your application, you have to do something like:

var config = 
  ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
config.AppSettings.Settings["Redis.Port"].Value = 
  _tempRedis.Port.ToString();
config.Save(ConfigurationSaveMode.Modified);

And the consuming code elsewhere has to read the config after you've done this.

The problem we saw was that a lazy prop which was using this config never gets the updated value, because the app config was captured too early (and this test setup code runs far before any test code, so I'm not entirely sure exactly when it's done). We had to work around this capturing behavior.

In addition, there's no way to reset a Lazy -- and sometimes you really need to. Consider where you might have a lazy prop exposing the total value of a shopping card. When the customer adds a new item, the underlying field can just be set to null and the next time someone wants a total, they can just read from the property.

I don't see any huge advantage over a regular old lazy prop (it's not significantly less code). If Lazy is working ok for you, then cool (: I have just been bitten a few times by it not being as lazy as it says it is :D

Anyways, thanks for reading & thanks for commenting! Discussion is always good!