Practical .NET

It's OK To Be Lazy And Defer Creating Objects Until You Need Them

You can write some complicated code to ensure that you don't create any object until you absolutely need it. Or you can use the Lazy object...at least some of the time.

Sometimes your application needs an object or a collection of objects. And sometimes it doesn't. For example, imagine that your application uses a group of country code objects as part of its processing. That's potentially a list of several hundred objects -- one for each country in the world. However, the odds are that you'll only use a few of those country objects. What makes sense here? You could, of course, preload the list with all of the objects and be done with it -- that's simple code, but it's going to generate an awful lot of objects you'll never use. Alternatively, you could add each country to the list as it's asked for -- that's a pretty good solution but requires more complicated code than the first solution.

The problem doesn't have to involve a lot of objects -- you might just have one object that takes a long time to create and you'd prefer to avoid doing that if it turns out you don't need the object. This might even be an object stored in some class property -- if no one reads the property, you'd prefer not to create the object.

Typically, to handle these problems you wrap the code up in a test that checks to see if: (a) you need the object and (b) you haven't already created it. That code looks something like this:

Dim doneAlready As Boolean
Dim ccis As ICollection(of CustomerCreditInfo)

If cust.IsInDefault AndAlso
  Not doneAlready Then
  ccis = GetCreditInfo(cust.CustId)
  doneAlready = True
End If

The more logic you have in your program, the harder it is to test, explain and document. For instance, in this code you'd better make sure to reset that doneAlready flag to False whenever you move to another customer or you'll process the next customer with the last customer's credit information. After looking at this code, you might just say, "The heck with it," and always retrieve all the objects because it makes your program so much simpler. It's the usual situation in data processing: Do you want your arm cut off or ripped off?

Using Lazy<T> can give you the best of both worlds: Objects retrieved only when you need them with the simplicity of the code that always retrieves the objects. Unfortunately, there are sufficient restrictions around what kind of objects you can use with Lazy<T> that you may not be able to take as much advantage of it as you would like. It's probably not possible, for example, to use Lazy<T> to generate that list of countries that are only created when you need them. On the other hand, Lazy<T> might be just the solution you want for some of the properties on your classes.

Using Lazy<T>
Lazy<T> is a placeholder for the class with which you actually want to work. You can use the Lazy<T> object (pass it to methods, for example) but the class that Lazy<T> holds for you won't be instantiated until the application asks for it by reading the Lazy<T> object's Value property. The following example creates a Lazy<T> object for a CustomerCreditInfo object. After checking to see if the credit information needs to be validated, the code then passes the Lazy<T> object to a method called ValidateCreditInfo. That method returns the object updated with validation information:

lcci = New Lazy(Of CustomerCreditInfo)
If '...test to determine if ValidateCreditInfo needs to be called...
  lcci = ValidateCreditInfo(lcci)
End if

There's nothing in this code to suggest that a CustomerCreditInfo object is ever created. It's only inside the ValidateCreditInfo, where the code finally reads the Lazy<T> object's Value method where you find the point that the CustomerCreditInfo object is instantiated:

Public Function ValidateCreditInfo(lcci As Lazy(Of CustomerCreditInfo)) As Lazy(Of CustomerCreditInfo)
  Dim cci As CustomerCreditInfo
  cci = lcci.Value
  '...update the CustomerCreditInfo object with validation information
  Return lcci
End Function

In this example, my ValidateCreditInfo object returns the Lazy<T> object to the application that called the method. Later, the application reads the Lazy<T> object's Value property:

Dim cci As CustomerCreditInfo
cci = lcci.Value

If the lcci object has been through the ValidateCreditInfo method, then the CustomerCreditInfo object created (and updated) in ValidateCreditInfo will be returned to the application. If the object hasn't been through the ValidateCreditInfo (and, therefore, the CustomerCreditInfo object hasn't been created yet) then the CustomerCreditInfo object will be created at this time. If it matters to you whether the class has or hasn't been created yet, you can check the Lazy<T> object's IsValueCreated property, which will return True if the class has been created, False if it hasn't.

The Lazy<T> object is clever about handling exceptions with creating its object. If, inside ValidateCreditInfo, reading the Lazy<T> object's Value property raised an exception (presumably because there was a problem instantiating the class), the Lazy<T> object will hold onto that exception. If, later in the application, the Value property is read again, the Lazy<T> object will not attempt to recreate the object -- instead, it will just raise the exception again.

This is all very wonderful but there's a significant restriction imposed on the design of the CustomerCreditInfo object that reduces the usefulness of Lazy<T>: For this code to work the CustomerCreditInfo object must either not have a constructor or have a constructor that accepts no parameters. Because most objects have constructors that must be passed values, this is a significant restriction.

This requirement isn't checked at compile time, by the way -- it isn't until the Lazy<T> object is created that the Microsoft .NET Framework checks to make sure that CustomerCreditInfo has an appropriate constructor.

Creating Factories
Because the more typical case is that you'll want to pass some parameters to the object inside your Lazy<T> object, rather than pass a class to Lazy<T>, you can pass a method that creates the object. Passing a method also allows you to create a collection of objects inside your Lazy<T> object instead of a single object.

This method, for example, creates a list of countries (in real life, I assume that I'd retrieve these country codes from a database):

Public Function GetCountries() As ICollection(Of Countries)
  Dim lstCtrys As New List(Of Country) From {
                                              New Country("USA"),
                                              New Country("CDN"),
                                              New Country("UK"),
                                             ... more countries ...
                                            }
   Return lstCtrys
End Function

To avoid calling this factory method until I need it (while still being able to pass it to methods that could use it), I can pass my factory method to a Lazy<T> object, like this:

Dim lctrys As New Lazy(Of ICollection(Of Country))(AddressOf GetCountries)

As with my CustomerCreditInfo example, my factory method won't be called until someone reads the Lazy<T> object's Value property. Code like this would trigger the factory method:

For Each ctry As Country In lctrys.Value
   '... code to work with countries...
Next

You don't have to set up a factory method. Instead, you can also pass a lambda expression to a Lazy<T> object, as this code does:

Dim lctrys As New Lazy(Of ICollection(Of Country))(Function()
                                        Return New List(Of Country) From {
                                          New Country("USA"),
                                          New Country("CDN"),
                                          New Country("UK"),
                                          ... more countries ... }
                                        End Function)

Unfortunately, while you can use a factory method to create objects that require values to be passed to their constructors you can't pass parameters to the factory method. Instead, any parameters passed to the objects you're instantiating will have to be generated in the factory method (as in my example) or retrieved from variables declared external to the factory method.

If you do use externally declared variables, you need to be aware that the factory method will use the values of the variables at the time the factory method is called, not the values at the time the Lazy<T> object was created. In the following code, for example, the Country object will be created with the parameter "CDN" because that's the value in the CountryInitializer variable when the Value property is read (it's irrelevant that CountryInitializer was set to "USA" when the Lazy<T> object was created):

Dim CountryInitializer As String
CountryInitializer = "USA"
Dim lctry As New Lazy(Of Country)(Function()
                                  New Country(CountryIniitalizer)    
                                  End Function)
CountryInitializer = "CDN"
Dim ctry As Country
ctry = lcty.Value

Lazy Properties
For me, these restrictions mean that Lazy<T> is most useful to me when returning an object that's expensive to create from a property. For an example, take a look at the Customer object in Listing 1 that has its CreditInfo property implemented using a Lazy<T> object.

Listing 1: Using a Lazy<T> Object to Implement CreditInfo Property in Customer Object

Public Class Customer
  Private id As String
  Public Sub New(id As String)
    Me.id = id
  End Sub
  Private lcci As New Lazy(Of CustomerCreditInfo)(AddressOf GetCreditInfo)
  Public ReadOnly Property CreditInfo() As CustomerCreditInfo
    Get
      Return lcci.Value
    End Get
  End Property
  Private Function GetCreditInfo() As CustomerCreditInfo
     Return New CustomerCreditInfo(Me.Id)
  End Function
End Class

With this class, I defer creating my CustomerCreditInfo class until the application using my Customer class reads the CreditInfo property. At that point, my GetCreditInfo factory method will be triggered, passing the id field to the CustomerCreditInfo object. Because that id field is declared as readonly, I'm guaranteed that its value won't change during the life of the Customer class.

But I can improve on this: Instead of returning the CustomerCreditInfo object by returning the Value property of the Lazy<T> object, I can just return that Lazy<T> object. The enhanced property looks like this:

Public ReadOnly Property CreditInfo() As Lazy(Of CustomerCreditInfo)
  Get
    Return lcci
  End Get
End Property

Now, even when the application reads the CreditInfo property, my CustomerCreditInfo object will not be created. It's only when the application finally reads the lcci variable's Value property that my factory method inside the class will finally execute and use the class id field.

If I got your hopes up at the start of this article, I hope that I've let you down easy: The restrictions around constructors for Lazy<T> classes and parameters passed to factory methods do limit the usefulness of Lazy<T>. But, even with those restrictions, if you want to defer creating a class or collection until you really need it, you may yet find a way to be lazy about it.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube