Created
May 8, 2015 22:08
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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