Caching is a technique where objects or data are held in memory after they are first used in case they are needed again later. Caching is commonly used when reading data from slow sources, such as remote file systems or databases. It's also useful when you need to perform a complex calculation that will take a long time to run and may want to use the result again. Rather than recalculate each time you need the information, you can read the existing information directly from the cache. This can lead to large performance improvements in your software at the cost of the increased memory requirements of the cache.
A lazily loaded object is not fully instantiated at the time that it is defined. Instead, instructions for the instantiation are given, perhaps in the form of delegate or lambda expression. When the lazy object is accessed for the first time, the initialisation process is executed and the real object is populated. When accessed subsequently, the existing object is used; initialisation is not repeated. In some ways this is similar to caching.
Some lazily initialised objects will never be used. For example, you might create a program that shows a list of files in a folder and shows details about a file when selected. If each file uses a lazily initialised object, only the ones that the user views will need to be constructed. The others will not go through the initialisation process. This may save resources, as the extra data will not be in memory. It may also improve performance.
Normally, objects created in a .NET application use strong references. If any reachable, strong references to an object are present, the object lives within the managed heap. Once all strong references to an object are unreachable, the object becomes a candidate for garbage collection.
In some cases you may decide to create weak references. Such objects can be removed from memory from the garbage collector to free up space when required, as long as no strong references are present. If you need to reuse a weakly referenced object that has been garbage collected you need to recreate it first.
Generic Lazy-Loaded Weakly Referenced Cache
In this article we'll create a caching class that incorporates the three techniques described above. The cache will store a single object, which will only be instantiated when required. To determine the contents of the cache we'll provide a function that initialises the object using a Func delegate. This will be executed when the cached object is accessed for the first time. Until then, only the function will be stored.
Once created, the cached object will be held in a weak reference in the cache. If the garbage collector runs whilst no strong references are present, the object will be a candidate for removal. If an attempt to access the object is made after garbage collection, the function will be executed again and the cache repopulated. From the point of view of the consumer, the object will always be available but there may be a delay accessing it.
Overall, our new cache will provide the performance benefit of other caches but be willing to clear itself automatically should memory pressure be high. It will also rebuild itself transparently to the consumers of the cache.
Creating the Class
We'll start by creating a new class for the cache. Add a class named, "LazyCache". As the class should be able to store any object we'll modify the definition to create a generic class. We will be using a WeakReference<T> instance to hold the cached object after it is first initialised. As this only permits weak references to reference types, we need to modify our generic type parameter so that the LazyCache<T> type is constrained to reference types too.
Update the standard class definition to match the following:
public class LazyCache<T> where T : class
Adding the Fields
We need three private fields in the LazyCache type. One is populated when a new LazyCache object is created. This holds the function that will be used to initialise the cached object. The second field holds a weak reference to the object once it has been initialised. The third is a locking object that will be used to ensure thread-safety.
object _lock = new object();
Creating the Constructor
To set up the cache we'll use constructor injection to provide the function. The constructor's parameter receives a Func delegate that describes a function that obtains or creates the cached object. This allows any process to be defined, such as querying a database, reading a file or performing a complex calculation. We don't need to run this function in the constructor, as we want to avoid the performance penalty if it turns out that the object is never used.
public LazyCache(Func<T> generateFunction)
_generateFunction = generateFunction;
28 December 2012