Instantiating Immutable Objects With Reflection

2020-06-04 08:03发布

I created a base class to help me reduce boilerplate code of the initialization of the immutable Objects in C#,

I'm using lazy initialization in order to try not to impact performance a lot , I was wondering how much am I affecting the performance by doing this?

This is my base class:

public class ImmutableObject<T>
{
    private readonly Func<IEnumerable<KeyValuePair<string, object>>> initContainer;

    protected ImmutableObject() {}

    protected ImmutableObject(IEnumerable<KeyValuePair<string,object>> properties)
    {
        var fields = GetType().GetFields().Where(f=> f.IsPublic);

        var fieldsAndValues =
            from fieldInfo in fields
            join keyValuePair in properties on fieldInfo.Name.ToLower() equals keyValuePair.Key.ToLower()
            select new  {fieldInfo, keyValuePair.Value};

        fieldsAndValues.ToList().ForEach(fv=> fv.fieldInfo.SetValue(this,fv.Value));

    }

    protected ImmutableObject(Func<IEnumerable<KeyValuePair<string,object>>> init)
    {
        initContainer = init;
    }

    protected T setProperty(string propertyName, object propertyValue, bool lazy = true)
    {

        Func<IEnumerable<KeyValuePair<string, object>>> mergeFunc = delegate
                                                                        {
                                                                            var propertyDict = initContainer == null ? ObjectToDictonary () : initContainer();
                                                                            return propertyDict.Select(p => p.Key == propertyName? new KeyValuePair<string, object>(propertyName, propertyValue) : p).ToList();
                                                                        };

        var containerConstructor = typeof(T).GetConstructors()
            .First( ce => ce.GetParameters().Count() == 1 && ce.GetParameters()[0].ParameterType.Name == "Func`1");

        return (T) (lazy ?  containerConstructor.Invoke(new[] {mergeFunc}) :  DictonaryToObject<T>(mergeFunc()));
    }

    private IEnumerable<KeyValuePair<string,object>> ObjectToDictonary()
    {
        var fields = GetType().GetFields().Where(f=> f.IsPublic);
        return fields.Select(f=> new KeyValuePair<string,object>(f.Name, f.GetValue(this))).ToList();
    }

    private static object DictonaryToObject<T>(IEnumerable<KeyValuePair<string,object>> objectProperties)
    {
        var mainConstructor = typeof (T).GetConstructors()
            .First(c => c.GetParameters().Count()== 1 && c.GetParameters().Any(p => p.ParameterType.Name == "IEnumerable`1") );
        return mainConstructor.Invoke(new[]{objectProperties});
    }

    public T ToObject()
    {
        var properties = initContainer == null ? ObjectToDictonary() : initContainer();
        return (T) DictonaryToObject<T>(properties);
    }
}

Can be implemented like so:

public class State:ImmutableObject<State>
{
    public State(){}
    public State(IEnumerable<KeyValuePair<string,object>> properties):base(properties) {}
    public State(Func<IEnumerable<KeyValuePair<string, object>>> func):base(func) {}

    public readonly int SomeInt;
    public State someInt(int someInt)
    {
        return setProperty("SomeInt", someInt);
    }

    public readonly string SomeString;
    public State someString(string someString)
    {
        return setProperty("SomeString", someString);
    }
}

and can be used like this:

//creating new empty object
var state = new State();

// Set fields, will return an empty object with the "chained methods".
var s2 = state.someInt(3).someString("a string");
// Resolves all the "chained methods" and initialize the object setting all the fields by reflection.
var s3 = s2.ToObject();

3条回答
\"骚年 ilove
2楼-- · 2020-06-04 08:12

Well to answer your question about performance, reflection is very expensive (relatively speaking). I would not use your design if it's in performance critical code.

When it comes to generics and reflection the performance hit can often be surprisingly large. Consider even something as simple as this:

public class Builder<T> where T : new()
{
    public T Build()
    {
        return new T();
    }
}

What this is actually doing is calling Activator.CreateInstance which uses reflection and it's extremely expensive.

If I wanted to optimize code like the above case I would use dynamic methods. And the performance difference between the two would be drastic.

Of course, keep in mind we're entering the zone of advanced code that's more complex and harder to read for the sake of performance. You could consider this overly optimized and overkill in code that isn't performance critical.

But in code that I write I avoid reflection like the plague.

查看更多
做个烂人
3楼-- · 2020-06-04 08:21

My favourite way to things like that is to use expression trees. You can manually construct your expression tree to just create a new instance of your type and compile this expression tree into a delegate. The beauty of this approach is that you only need reflection and dynamic code generation for once and afterwards you work with the generated delegate. Also, the expression compiler does its best to work even on partial trusted environments, where dynamic methods are problematic. On the other hand, you have an abstraction layer much higher than writing pure IL code in an ILGenerator, which would be the way to go in a dynamic method.

查看更多
▲ chillily
4楼-- · 2020-06-04 08:27

As was already mentioned in the comments, it would make more sense, not to "conflate" the immutable instance implementation or interface with the behavior of what is essentially a builder for new instances.

You could make a much cleaner and quite type safe solution that way. So we could define some marker interfaces and type safe versions thereof:

public interface IImmutable : ICloneable { }
public interface IImmutableBuilder { }

public interface IImmutableOf<T> : IImmutable where T : class, IImmutable 
{
    IImmutableBuilderFor<T> Mutate();
}

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();
}

And provide all the required basic builder behavior in a class like below. Note that most error checking/compiled delegate creation is omitted for the sake of brevity/simplicity. A cleaner, performance optimized version with a reasonable level of error checking can be found in this gist.

public class DefaultBuilderFor<T> : IImmutableBuilderFor<T> where T : class, IImmutableOf<T>
{
    private static readonly IDictionary<string, Tuple<Type, Action<T, object>>> _setters;
    private List<Action<T>> _mutations = new List<Action<T>>();

    static DefaultBuilderFor()
    {
        _setters = GetFieldSetters();
    }

    public DefaultBuilderFor(T instance)
    {
        Source = instance;
    }

    public T Source { get; private set; }

    public IImmutableBuilderFor<T> Set<TFieldType>(string fieldName, TFieldType value)
    {
        // Notes: error checking omitted & add what to do if `TFieldType` is not "correct".
        _mutations.Add(inst => _setters[fieldName].Item2(inst, value));
        return this;
    }

    public IImmutableBuilderFor<T> Set<TFieldType>(string fieldName, Func<T, TFieldType> valueProvider)
    {
        // Notes: error checking omitted & add what to do if `TFieldType` is not "correct".
        _mutations.Add(inst => _setters[fieldName].Item2(inst, valueProvider(inst)));
        return this;
    }

    public IImmutableBuilderFor<T> Set<TFieldType>(Expression<Func<T, TFieldType>> fieldExpression, TFieldType value)
    {
        // Error checking omitted.
        var memberExpression = fieldExpression.Body as MemberExpression;
        return Set<TFieldType>(memberExpression.Member.Name, value);
    }

    public IImmutableBuilderFor<T> Set<TFieldType>(Expression<Func<T, TFieldType>> fieldExpression, Func<TFieldType, TFieldType> valueProvider)
    {
        // Error checking omitted.
        var memberExpression = fieldExpression.Body as MemberExpression;
        var getter = fieldExpression.Compile();
        return Set<TFieldType>(memberExpression.Member.Name, inst => valueProvider(getter(inst)));
    }

    public T Build()
    {
        var result = (T)Source.Clone();
        _mutations.ForEach(x => x(result));
        return result;
    }

    private static IDictionary<string, Tuple<Type, Action<T, object>>> GetFieldSetters()
    {
        // Note: can be optimized using delegate setter creation (IL). 
        return typeof(T).GetFields(BindingFlags.Public | BindingFlags.Instance)
            .Where(x => !x.IsLiteral)
            .ToDictionary(
                x => x.Name,
                x => SetterEntry(x.FieldType, (inst, val) => x.SetValue(inst, val)));
    }

    private static Tuple<Type, Action<T, object>> SetterEntry(Type type, Action<T, object> setter)
    {
        return Tuple.Create(type, setter);
    }
}

Example usage

This could then be used like this, using your example class of State:

public static class Example
{
    public class State : IImmutableOf<State>
    {
        public State(int someInt, string someString)
        {
            SomeInt = someInt;
            SomeString = someString;
        }

        public readonly int SomeInt;
        public readonly string SomeString;

        public IImmutableBuilderFor<State> Mutate()
        {
            return new DefaultBuilderFor<State>(this);
        }

        public object Clone()
        {
            return base.MemberwiseClone();
        }

        public override string ToString()
        {
            return string.Format("{0}, {1}", SomeInt, SomeString);
        }
    }

    public static void Run()
    {
        var original = new State(10, "initial");

        var mutatedInstance = original.Mutate()
            .Set("SomeInt", 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);
    }
}

With the following output:

45, Hello SO
20, initial
查看更多
登录 后发表回答