Skip to content

Instantly share code, notes, and snippets.

@mikernet
Last active March 22, 2021 00:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mikernet/d8884f3a9ca30e85c9a27fdeaa695a90 to your computer and use it in GitHub Desktop.
Save mikernet/d8884f3a9ca30e85c9a27fdeaa695a90 to your computer and use it in GitHub Desktop.
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;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment