Lazy<T> is a new class in .NET 4, but I imagine most developers have been running their own versions for years. It’s used to avoid the fetching of expensive resources. However, the fundamental assumption is that it’s the fetching of T that is expensive. Assume, instead, that the cost of fetching/calculating T isn’t your main concern, but the memory storage cost. For this, you want something with slightly different characteristics:
- T is computed when needed.
- T is cached
- T is thrown away if the memory is at a premium.
- T can be recreated if necessary
- The external API should be the same as Lazy<T>
Sadly, the last isn’t quite possible, since Microsoft didn’t create an ILazy<T> while they were there. (One day someone at Microsoft will read this and figure out where they’ve been going wrong.) So, this is as close as I can manage:
using System; /// <summary> /// Like Lazy, only can recreate the object on demand /// </summary> /// <typeparam name="T"></typeparam> public class LazyWeak<T> { private static readonly object __noObject = 3; private readonly Func<T> _factory; private readonly WeakReference _reference; public LazyWeak(Func<T> factory, T initial) { _factory = factory; _reference = new WeakReference(initial); } public LazyWeak(Func<T> factory) { _factory = factory; _reference = new WeakReference(__noObject); } public bool IsValueCreated { get { return _reference.IsAlive && !ReferenceEquals(_reference.Target, __noObject); } } public T Value { get { var result = _reference.Target; if (ReferenceEquals(result, __noObject) || !_reference.IsAlive) { _reference.Target = result = _factory(); } return (T)result; } } }
You can create the LazyWeak<T> with an initial value. This makes no sense in the case of Lazy<T>, but can be pretty useful here. I’ve also aimed to ensure that the code is thread-safe. If you spot a bug, let me know.
Finally, a quick test:
public class LazyTest { static void Main() { int calls = 0; Func<int> factory = () => { calls++; return 7; }; var lazy = new LazyWeak<int>(factory, 7); Console.WriteLine(string.Format("{0}:{1}", lazy.Value, calls)); Console.WriteLine(string.Format("{0}:{1}", lazy.Value, calls)); Console.WriteLine("GC"); System.GC.Collect(); Console.WriteLine(string.Format("{0}:{1}", lazy.Value, calls)); } }
There are lots of advantages of relying on the garbage collector, but you lose control over what’s going on.
So, what’s it good for? Well, it uses less memory than just storing T or Lazy<T> and it’s faster than repeatedly calling Func<T>. So, it falls between those two, which may be the performance sweet spot for some problem in your application. On the other hand, it’s fairly important that T is immutable. There’s nothing stopping you changing T, but it would revert back to its original state each time the garbage collector ran.
It’s worth mentioning that this is one of many partial solutions to the long queue problem: stick LazyWeak<T> instead of T on the queue. This can allow you to just keep keys in memory and leave values persisted elsewhere. If you want to do both, you want a proper external queue.