Skip to content

Instantly share code, notes, and snippets.

@mikernet mikernet/BoxCache.cs
Last active Sep 5, 2019

Embed
What would you like to do?
Value Type Box Cache
// Box Cache implementation by Mike Marynowski
// View the article here: http://www.singulink.com/CodeIndex/post/value-type-box-cache
// Licensed under the Code Project Open License: http://www.codeproject.com/info/cpol10.aspx
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Diagnostics;
using System.Reflection;
namespace Singulink
{
public static class BoxCache
{
static int _intialized;
internal static void IntializeDefaults()
{
if (Interlocked.CompareExchange(ref _intialized, 1, 0) == 0)
{
// If you are only caching a default value then don't bother doing it here,
// it gets automatically added in the generic BoxCache<T> initializer.
AddValues(false, true);
AddValues(Enumerable.Range(0, 255).Select(v => (byte)v));
AddValues(Enumerable.Range(-128, 127).Select(v => (sbyte)v));
AddValues(Enumerable.Range(-10, 256).Select(v => (short)v));
AddValues(Enumerable.Range(0, 256).Select(v => (ushort)v));
AddValues(Enumerable.Range(-10, 256).Select(v => (int)v));
AddValues(Enumerable.Range(0, 256).Select(v => (uint)v));
AddValues(Enumerable.Range(-10, 256).Select(v => (long)v));
AddValues(Enumerable.Range(0, 256).Select(v => (ulong)v));
AddValues<float>(-1, 0, 1);
AddValues<double>(-1, 0, 1);
AddValues<decimal>(-1, 0, 1);
}
}
#region Public Methods
/// <summary>
/// Gets a cached box for the specified value if one is cached, otherwise it returns a new box for the value.
/// </summary>
public static object GetBox<T>(T value) => BoxCache<T>.GetBox(value);
/// <summary>
/// Gets a cached box for the specified value. A box is created and added to the cache if it doesn't already exist.
/// </summary>
public static object GetOrAddBox<T>(T value) => BoxCache<T>.GetOrAddBox(value);
/// <summary>
/// Adds a box for the specified value into the cache. Note that the cache uses a copy-on-write mechanism to ensure thread safety and
/// ensure fastest possible lookup time so use this sparingly and use 'AddValues()' for multiple values instead.
/// </summary>
public static void AddValue<T>(T value) where T : struct => BoxCache<T>.AddValue(value);
/// <summary>
/// Adds boxes for the specified values into the cache. Note that the cache uses a copy-on-write mechanism to ensure thread safety and
/// ensure fastest possible lookup time so use this sparingly after application startup.
/// </summary>
public static void AddValues<T>(IEnumerable<T> values) where T : struct, IComparable<T> => BoxCache<T>.AddValues(values);
#endregion
#region Helpers
static void AddValues<T>(params T[] values) where T : struct => BoxCache<T>.AddValues(values);
#endregion
}
static class BoxCache<T>
{
static Dictionary<T, object> _boxLookup;
static Func<T, object> _getNullableBox;
static Func<T, object> _getOrAddNullableBox;
static readonly object _syncRoot = new object();
static readonly bool _init = Initialize(); // Avoid static constructor performance penalty
#region Initializer
static bool Initialize()
{
if (!typeof(T).IsValueType)
throw new InvalidOperationException("Only value types can be boxed.");
if (typeof(T).IsNullable()) {
// This is nullable so force static initializer for the underlying type cache to run so default boxes get populated there
// and the delegates for this class are assigned
var valueType = Nullable.GetUnderlyingType(typeof(T));
Type boxCacheEnumType = typeof(BoxCache<>).MakeGenericType(valueType);
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(boxCacheEnumType.TypeHandle);
// Assign delegates so box requests are forwarded to the non-nullable cache
var underlyingType = Nullable.GetUnderlyingType(typeof(T));
var underlyingCacheType = typeof(BoxCache<>).MakeGenericType(underlyingType);
var getMethod = underlyingCacheType.GetMethod(nameof(GetNullableBox), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(underlyingType);
var getOrAddMethod = underlyingCacheType.GetMethod(nameof(GetOrAddNullableBox), BindingFlags.Static | BindingFlags.NonPublic).MakeGenericMethod(underlyingType);
_getNullableBox = (Func<T, object>)Delegate.CreateDelegate(typeof(Func<T, object>), getMethod);
_getOrAddNullableBox = (Func<T, object>)Delegate.CreateDelegate(typeof(Func<T, object>), getOrAddMethod);
}
else {
// Ensure default boxes are initialized
BoxCache.IntializeDefaults();
if (typeof(T).IsEnum) {
var values = (T[])Enum.GetValues(typeof(T));
AddValues(values);
}
if (_boxLookup == null)
_boxLookup = new Dictionary<T, object>();
if (!_boxLookup.ContainsKey(default))
_boxLookup.Add(default, default(T));
}
return true;
}
#endregion
internal static object GetBox(T value)
{
if (_boxLookup == null) {
Debug.Assert(typeof(T).IsNullable(), "box lookup should only be null for nullable types");
if (value == null)
return null;
return _getNullableBox(value);
}
Debug.Assert(!typeof(T).IsNullable(), "box lookup should not be created for nullable types");
if (_boxLookup.TryGetValue(value, out object obj))
return obj;
return value;
}
internal static object GetOrAddBox(T value)
{
if (_boxLookup == null) {
Debug.Assert(typeof(T).IsNullable(), "box lookup should only be null for nullable types");
if (value == null)
return null;
return _getOrAddNullableBox(value);
}
Debug.Assert(!typeof(T).IsNullable(), "box lookup should not be created for nullable types");
if (_boxLookup.TryGetValue(value, out object obj))
return obj;
object box = AddValue(value);
return box;
}
internal static void AddValues(IEnumerable<T> values)
{
Debug.Assert(!typeof(T).IsNullable(), "nullable types should always be forwarded to non-nullable cache");
if (values == null)
throw new ArgumentNullException(nameof(values));
lock (_syncRoot) {
// Avoid copying the dictionary if there are no new values.
Dictionary<T, object> newBoxLookup = _boxLookup == null ? new Dictionary<T, object>() : null;
foreach (var value in values) {
if (newBoxLookup != null) {
if (!newBoxLookup.ContainsKey(value))
newBoxLookup.Add(value, value);
}
else if (!_boxLookup.ContainsKey(value)) {
newBoxLookup = new Dictionary<T, object>(_boxLookup) { { value , value } };
}
}
_boxLookup = newBoxLookup;
}
}
internal static object AddValue(T value)
{
Debug.Assert(!typeof(T).IsNullable(), "nullable types should always be forwarded to non-nullable cache");
lock (_syncRoot) {
if (_boxLookup.TryGetValue(value, out object obj))
return obj;
object box = value;
var newBoxLookup = new Dictionary<T, object>(_boxLookup) { { value, box } };
_boxLookup = newBoxLookup;
return box;
}
}
// Forwarding methods for nullable types
private static object GetNullableBox<TNullable>(TNullable? value) where TNullable : struct
{
Debug.Assert(value.HasValue, "value required");
return GetBox((T)(object)value.Value);
}
private static object GetOrAddNullableBox<TNullable>(TNullable? value) where TNullable : struct
{
Debug.Assert(value.HasValue, "value required");
return GetOrAddBox((T)(object)value.Value);
}
}
public static class Helpers
{
public static bool IsNullable(this Type type) => Nullable.GetUnderlyingType(type) != null;
}
}
@mikernet

This comment has been minimized.

Copy link
Owner Author

mikernet commented Feb 1, 2018

Please leave comments on the blog article instead of here, for god knows what reason GitHub has not implemented gist comment notifications.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.