For caching of (deterministic) results of expensive calls, use Lazy<T>
- this has an optional LazyThreadSafetyMode
parameter allowing you to specify how to resolve concurrency issues.
Update - Assuming CalculateStr
is not static
public class A
{
private readonly Lazy<string> _lazyStr;
public A()
{
// Provide a factory method
_lazyStr = new Lazy<string>(() => CalculateStr());
}
public string GetStr()
{
// Lazy retrieval of the value, invokes factory if needed.
return _lazyStr.Value;
}
public string CalculateStr()
{
// Expensive method goes here. Track to ensure method only called once.
Console.WriteLine("Called");
return "Foo";
}
}
The behaviour is as follows, viz:
- If nothing ever calls
GetStr
, then the (presumed expensive) call to CalculateStr
is avoided altogether
- If
GetStr
is called more than once, then the value is cached and reused.
- If two or more threads concurrently invoke
GetStr
the first time it is needed, then the LazyThreadSafetyMode
will allow you to decide how you want concurrency to be handled. You can either serialize the calls (with ExecutionAndPublication
, the default), i.e. block until one of the threads creates a single instance, OR you can concurrently call the factory on all the threads, and one of the invocation results will be cached (PublicationOnly
). For expensive calls, you won't want to be using PublicationOnly
.
Update - "Retry" if CalculateStr returns null or empty
Note that OP's updated requirement doesn't quite fit the classic 'lazy instantiation' mold - seemingly the CalculateStr
method call is unreliable and sometimes returns null. OP's requirement is thus to cache the first non-null response from the method, but not to retry if the initial response is null. Instead of using Lazy
, we'll need to do this ourselves. Here's a double-checked lock implementation.
public class A
{
private string _cachedString = null;
private object _syncLock = new object();
public string GetStr()
{
if (_cachedString == null)
{
lock(_syncLock)
{
if (_cachedString == null)
{
var test = CalculateStr();
if (!string.IsNullOrEmpty(test))
{
_cachedString = test;
}
return test;
}
}
}
return _cachedString;
}
public string CalculateStr()
{
// Unreliable, expensive method here.
// Will be called more than once if it returns null / empty.
Console.WriteLine("Called");
return "Foo";
}
}
Note that neither of the above requires a singleton instance - as many A
instances can be invoked as needed, and each A
instance will (eventually) cache a single non-null value returned from CalculateStr
. If a singleton is required, then share the A
instance, or use an IoC container to control a single instance of A.