Skip to content

Instantly share code, notes, and snippets.

Created May 8, 2015 22:08
Show Gist options
  • Save anonymous/ddf8b3541e3896b748a9 to your computer and use it in GitHub Desktop.
Save anonymous/ddf8b3541e3896b748a9 to your computer and use it in GitHub Desktop.
Performance optimized version for http://stackoverflow.com/questions/24395241/instantiating-immutable-objects-with-reflection. With additional fallback for value type conversions, to support e.g. Guid, TimeSpan, nullable properties.
namespace TryOuts.Immutable.Sample
{
using System;
using Tryouts.Immutable;
public static class Example
{
public class State : IImmutableOf<State>
{
public State() : this(0, null) { }
public State(int someInt, string someString)
{
SomeInt = someInt;
SomeString = someString;
MyGuid = Guid.NewGuid();
}
public readonly int SomeInt;
public readonly string SomeString;
public readonly Guid MyGuid;
public readonly double? MyNullableDouble;
public IImmutableBuilderFor<State> Mutate()
{
return ImmutableBuilder.DefaultFor(this);
}
public object Clone()
{
return base.MemberwiseClone();
}
public override string ToString()
{
return string.Format(
"{0}, {1}, {2}, {3}",
SomeInt, SomeString, MyGuid,
MyNullableDouble.HasValue ? MyNullableDouble.Value.ToString() : "<null>");
}
}
public static void Run()
{
var original = new State(10, "initial");
var mutatedInstance = original.Mutate()
.Set("SomeInt", (UInt16)45)
.Set(x => x.SomeString, "Hello SO")
.Build();
Console.WriteLine(mutatedInstance);
mutatedInstance = original.Mutate()
.Set(x => x.SomeInt, val => val + 10)
.Build();
Console.WriteLine(mutatedInstance);
var newInstance = ImmutableBuilder.CreateNew<State>()
.Set(x => x.SomeInt, 12)
.Set(x => x.SomeString, "Newly initialized")
.Build();
Console.WriteLine(newInstance);
newInstance = ImmutableBuilder.CreateNew(
() => new State(100, "From factory"),
builder => builder.Set(x => x.SomeString, s => string.Concat(s, " now modified")));
Console.WriteLine(newInstance);
newInstance = original.Mutate(b => b
.Set(x => x.SomeInt, b.Source.SomeInt + 20)
.Set(x => x.SomeString, string.Concat(b.Source.SomeString, " more"))
.Set(x => x.MyGuid, Guid.NewGuid())
.Set(x => x.MyNullableDouble, y => y.HasValue ? y.Value + 10 : 22.0d));
Console.WriteLine(newInstance);
}
}
}
namespace TryOuts.Immutable
{
using System;
using System.Linq;
using System.Linq.Expressions;
using Tryouts.Immutable.Internal;
// Marker interface for a class that is immutable
public interface IImmutable : ICloneable { }
// Marker interface for a builder of immutable instances.
public interface IImmutableBuilder { }
// Type safe interface for an immutable class of type T.
public interface IImmutableOf<T> : IImmutable where T : class, IImmutable
{
IImmutableBuilderFor<T> Mutate();
}
// Type safe interface for the builder of an immutable class of type T.
public interface IImmutableBuilderFor<T> : IImmutableBuilder where T : class, IImmutable
{
T Source { get; }
IImmutableBuilderFor<T> Set<TFieldType>(string fieldName, TFieldType value);
IImmutableBuilderFor<T> Set<TFieldType>(string fieldName, Func<T, TFieldType> valueProvider);
IImmutableBuilderFor<T> Set<TFieldType>(Expression<Func<T, TFieldType>> fieldExpression, TFieldType value);
IImmutableBuilderFor<T> Set<TFieldType>(Expression<Func<T, TFieldType>> fieldExpression, Func<TFieldType, TFieldType> valueProvider);
T Build();
}
// Public builder interface with convenience methods.
public static class ImmutableBuilder
{
public static IImmutableBuilderFor<T> CreateNew<T>()
where T : class, IImmutableOf<T>, new()
{
return new DefaultBuilderFor<T>(new T(), true);
}
public static IImmutableBuilderFor<T> CreateNew<T>(Func<T> factory)
where T : class, IImmutableOf<T>
{
return new DefaultBuilderFor<T>(factory(), true);
}
public static IImmutableBuilderFor<T> DefaultFor<T>(T source)
where T : class, IImmutableOf<T>
{
return new DefaultBuilderFor<T>(source);
}
public static T CreateNew<T>(params Action<IImmutableBuilderFor<T>>[] actions)
where T : class, IImmutableOf<T>, new()
{
return Apply(CreateNew<T>(), actions);
}
public static T CreateNew<T>(
Func<T> factory,
params Action<IImmutableBuilderFor<T>>[] actions)
where T : class, IImmutableOf<T>
{
return Apply(CreateNew<T>(factory), actions);
}
public static T Mutate<T>(
this T source,
params Action<IImmutableBuilderFor<T>>[] actions)
where T : class, IImmutableOf<T>
{
return source.Mutate().Apply(actions);
}
private static T Apply<T>(
this IImmutableBuilderFor<T> builder,
params Action<IImmutableBuilderFor<T>>[] actions)
where T : class, IImmutableOf<T>
{
foreach (var action in actions)
action(builder);
return builder.Build();
}
}
}
namespace Tryouts.Immutable.Internal
{
using System;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Linq.Expressions;
using System.Collections.Generic;
using System.Collections.Concurrent;
using TryOuts.Immutable;
//
// Internal implementation.
//
internal static class EmitUtils
{
private static readonly Type _cvtType = typeof(Convert);
private const BindingFlags Flags = BindingFlags.Static | BindingFlags.Public;
private static readonly ConcurrentDictionary<Type, MethodInfo> _Cvt =
new ConcurrentDictionary<Type, MethodInfo>();
public static Action<T, object> BuildSetter<T>(FieldInfo fieldInfo)
{
var methodName = typeof(T).FullName + "._dynSet" + fieldInfo.Name;
var dynamicMethod = new DynamicMethod(methodName, null, new[] { typeof(T), typeof(object) }, fieldInfo.DeclaringType, true);
var generator = dynamicMethod.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Castclass, fieldInfo.DeclaringType);
generator.Emit(OpCodes.Ldarg_1);
if (fieldInfo.FieldType.IsValueType)
{
var cvtMethod = GetConvertMethod(fieldInfo.FieldType);
if (cvtMethod != null)
generator.Emit(OpCodes.Call, cvtMethod);
else // this is the best we can do, it may fail at run time.
generator.Emit(OpCodes.Unbox_Any, fieldInfo.FieldType);
}
else
{
generator.Emit(OpCodes.Castclass, fieldInfo.FieldType);
}
generator.Emit(OpCodes.Stfld, fieldInfo);
generator.Emit(OpCodes.Ret);
return (Action<T, object>)dynamicMethod.CreateDelegate(typeof(Action<T, object>));
}
private static MethodInfo GetConvertMethod(Type dest)
{
return _Cvt.GetOrAdd(dest, k => RetrieveConvertMethod(dest));
}
private static MethodInfo RetrieveConvertMethod(Type dest)
{
var from = new Type[] { typeof(object) };
if (dest == typeof(bool))
return _cvtType.GetMethod("ToBoolean", Flags, null, from, null);
if (dest == typeof(byte))
return _cvtType.GetMethod("ToByte", Flags, null, from, null);
if (dest == typeof(SByte))
return _cvtType.GetMethod("ToSByte", Flags, null, from, null);
if (dest == typeof(char))
return _cvtType.GetMethod("ToChar", Flags, null, from, null);
if (dest == typeof(UInt16))
return _cvtType.GetMethod("ToUInt16", Flags, null, from, null);
if (dest == typeof(Int16))
return _cvtType.GetMethod("ToInt16", Flags, null, from, null);
if (dest == typeof(UInt32))
return _cvtType.GetMethod("ToUInt32", Flags, null, from, null);
if (dest == typeof(Int32))
return _cvtType.GetMethod("ToInt32", Flags, null, from, null);
if (dest == typeof(UInt64))
return _cvtType.GetMethod("ToUInt64", Flags, null, from, null);
if (dest == typeof(Int64))
return _cvtType.GetMethod("ToInt64", Flags, null, from, null);
if (dest == typeof(Single))
return _cvtType.GetMethod("ToSingle", Flags, null, from, null);
if (dest == typeof(Double))
return _cvtType.GetMethod("ToDouble", Flags, null, from, null);
if (dest == typeof(Decimal))
return _cvtType.GetMethod("ToDecimal", Flags, null, from, null);
if (dest == typeof(DateTime))
return _cvtType.GetMethod("ToDateTime", Flags, null, from, null);
return null;
//throw new ArgumentException(string.Format("No conversion possible to type '{0}'", dest));
}
}
internal class DefaultBuilderFor<T> : IImmutableBuilderFor<T> where T : class, IImmutableOf<T>
{
private static readonly IDictionary<string, Tuple<Type, Action<T, object>>> _setters;
private static readonly ConcurrentDictionary<string, object> _getters;
private readonly List<Action<T>> _mutations = new List<Action<T>>();
private readonly T _source;
private readonly bool _initializingSource;
static DefaultBuilderFor()
{
_setters = GetFieldSetters();
_getters = new ConcurrentDictionary<string, object>();
}
public DefaultBuilderFor(T instance, bool initializeSource = false)
{
_source = instance;
_initializingSource = initializeSource;
}
public T Source
{
get { return _initializingSource ? null : _source; }
}
public IImmutableBuilderFor<T> Set<TFieldType>(string fieldName, TFieldType value)
{
var setter = GetSetter<TFieldType>(fieldName);
return AddMutation(inst => setter(inst, value));
}
public IImmutableBuilderFor<T> Set<TFieldType>(string fieldName, Func<T, TFieldType> valueProvider)
{
var setter = GetSetter<TFieldType>(fieldName);
return AddMutation(inst => setter(inst, valueProvider(inst)));
}
public IImmutableBuilderFor<T> Set<TFieldType>(Expression<Func<T, TFieldType>> fieldExpression, TFieldType value)
{
return Set<TFieldType>(GetFieldExpression(fieldExpression).Member.Name, value);
}
public IImmutableBuilderFor<T> Set<TFieldType>(Expression<Func<T, TFieldType>> fieldExpression, Func<TFieldType, TFieldType> valueProvider)
{
var memberExpression = GetFieldExpression(fieldExpression);
var getter = GetOrAddGetter(memberExpression.Member.Name, fieldExpression);
return Set<TFieldType>(memberExpression.Member.Name, inst => valueProvider(getter(inst)));
}
public T Build()
{
var result = _initializingSource ? _source : (T)Source.Clone();
_mutations.ForEach(x => x(result));
return result;
}
private IImmutableBuilderFor<T> AddMutation(Action<T> action)
{
_mutations.Add(action);
return this;
}
private static MemberExpression GetFieldExpression<TFieldType>(Expression<Func<T, TFieldType>> fieldExpression)
{
var memberExpression = fieldExpression.Body as MemberExpression;
if (memberExpression == null || memberExpression.Member.MemberType != MemberTypes.Field)
throw new ArgumentException("A field expression is required", "fieldExpression");
return memberExpression;
}
private static Action<T, object> GetSetter<TFieldType>(string fieldName)
{
// Validity checks: for not only on field name
// Run-time errors will occur on non-castable type assignement attempts.
var entry = default(Tuple<Type, Action<T, object>>);
if (!_setters.TryGetValue(fieldName, out entry))
throw new ArgumentException(string.Format("No public field named '{0}' available for type '{1}'.", fieldName, typeof(T)));
return entry.Item2;
}
private static Func<T, TFieldType> GetOrAddGetter<TFieldType>(string fieldName, Expression<Func<T, TFieldType>> fieldExpression)
{
var getter = _getters.GetOrAdd(fieldName, n => fieldExpression.Compile());
return getter as Func<T, TFieldType>;
}
private static IDictionary<string, Tuple<Type, Action<T, object>>> GetFieldSetters()
{
return typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance)
.Where(x => !x.IsLiteral)
.ToDictionary(
x => x.Name,
x => Tuple.Create(x.FieldType, BuildSetter(x)));
}
private static Action<T, object> BuildSetter(FieldInfo fieldInfo)
{
try
{
return EmitUtils.BuildSetter<T>(fieldInfo);
}
catch // use fallback if we cannot generate using IL.
{
// This is a potential source for run-time errors.
return (inst, val) => fieldInfo.SetValue(inst, val);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment