For performance efficiency, you may wish to cache an object within another object. That sounds easy enough to do. But what if the object cannot be found? You have to take special care to ensure the object is not fetched repeatedly.
An Example
As always, the best way to demonstrate this is with an example. Consider a “Company” object that has a GetEmployee method which retrieves an Employee object from a remote database, an operation that could take a few seconds. There is also a CEO property that returns the Employee object representing the company’s CEO:
public class Company { public Employee CEO { get { // ... } } public Employee GetEmployee( Roles role ) { // ... } }
Cached Object
To improve performance, let’s say we want to cache the Employee object returned by the CEO property so that it can be accessed repeatedly with no delay after the first access. The easiest way to do this is simply include a private member that holds the CEO object once it has been loaded:
private Employee m_CEO; public Employee CEO { get { if (this.m_CEO == null) this.m_CEO = this.GetEmployee( Roles.CEO ); return this.m_CEO; } }
So far so good. But what if the CEO object cannot be found? As written above, the CEO property will call the GetEmployee method every time, resulting in a significant performance penalty.
Improved Cache
The solution is to create a blank Employee object and save it in a static member. As shown below, the Employee object stored in the s_EmployeeNotFound static member will serve as a “flag” to indicate that we’ve already attempted to get the CEO object but failed, hence do not attempt to get it again. If the CEO object could not be retrieved, instead of leaving the cached m_CEO reference as null, we set it equal to the s_EmployeeNotFound reference to indicate that we’ve already tried to load the CEO, so don’t bother doing it again.
public class Company { static private Employee s_EmployeeNotFound = new Employee(); private Employee m_CEO; public Employee CEO { get { // get the cached CEO Employee ceo = this.m_CEO; // if no CEO is cached if (ceo == null) { // try to load the CEO ceo = this.GetEmployee( Roles.CEO ); // if could not load the CEO if (ceo == null) { // indicate the CEO was not found ceo = s_EmployeeNotFound; } // cache the new CEO this.m_CEO = ceo; } // if CEO was not found if (Object.ReferenceEquals( ceo, s_EmployeeNotFound ) ) { // indicate there is no CEO to load ceo = null; } return ceo; } }
Note the use of Object.ReferenceEquals to compare objects. This ensures that we are truly checking for our special s_EmployeeNotFound object and prevents any accidental equality from an overridden Equals method in the Employee object.
Console Test Program
Using this example, here is a console test program that demonstrates how an object cache can really save time on multiple accesses. For simplicity, the GetEmployee method simply sleeps for 2 seconds to simulate a delay loading the object from a remote database.
using System; namespace CachedObject { class Program { static void Main( string[] args ) { Company company = new Company(); DateTime time1 = DateTime.Now; Console.WriteLine( "Accessing CEO for the first time..." ); Employee ceo = company.CEO; DateTime time2 = DateTime.Now; Console.WriteLine( "Accessing cached CEO...n" ); ceo = company.CEO; DateTime time3 = DateTime.Now; Console.WriteLine( "Time accessing CEO object:nNotCached...{0}nCached......{1}", time2.Subtract( time1 ), time3.Subtract( time2 ) ); Console.ReadLine(); } public class Company { static private Employee s_EmployeeNotFound = new Employee(); private Employee m_CEO; public Employee CEO { get { // get the cached CEO Employee ceo = this.m_CEO; // if no CEO is cached if (ceo == null) { // try to load the CEO ceo = this.GetEmployee( Roles.CEO ); // if could not load the CEO if (ceo == null) { // indicate the CEO was not found ceo = s_EmployeeNotFound; } // cache the new CEO this.m_CEO = ceo; } // if CEO was not found if (ceo == s_EmployeeNotFound) { // indicate there is no CEO to load ceo = null; } return ceo; } } public Employee GetEmployee( Roles role ) { // simulates a delay due to remote database access, for example System.Threading.Thread.Sleep( 2000 ); // would return the employee if found; for this test, assume fail return null; } } public class Employee { public string Name; public int ID; public Roles Role; public DateTime BirthDate; public DateTime HireDate; } public enum Roles { CEO, Management, Sales, Marketing, Development, Staff, } } }
Test Program Output
And here is the output to the test program. Notice how the first access of the CEO property took two seconds, but the second and all subsequent accesses will take no time at all, even if the CEO object could not be retrieved.
Accessing CEO for the first time…
Accessing cached CEO…
Time accessing CEO object:
NotCached…00:00:02.0156250
Cached……00:00:00