在 .NET 平台使用 ReflectionDynamicObject 优化反射调用的代码详解

2022-04-17 11:44:32

基于封装的原则,API 的设计者会将部分成员(属性、字段、方法等)隐藏以保证健壮性。但总有需要直接访问这些私有成员的情况。

为了访问一个类型的私有成员,除了更改 API 设计还有就是使用反射技术:

public class MyApi{public MyApi(){_createdAt = DateTime.Now;}private DateTime _createdAt;public int ShowTimes { get; private set; }public void ShowCreateTime(){Console.WriteLine(_createdAt);ShowTimes++;}}void Main(){var api = new MyApi();var field = api.GetType().GetField("_createdAt", BindingFlags.NonPublic | BindingFlags.Instance);var value = field.GetValue(api);Console.WriteLine(value);}

这种写法并不优雅:

代码冗长,编写麻烦。实现比较绕,不太直观。

笔者基于“动态类型技术”探索出了一种相对来说比较优雅的方案用于美化上述代码,并为其命名为 ReflectionDynamicObject :

void Main(){    var api = new MyApi();    dynamic wrapper = ReflectionDynamicObject.Wrap(api);    Console.WriteLine(wrapper._createdAt);}

除了支持获取值,ReflectionDynamicObject 还支持赋值:

void Main(){    var api = new MyApi();    dynamic wrapper = ReflectionDynamicObject.Wrap(api);    wrapper._createdAt = new DateTime(2022, 2, 2, 22, 22, 22);    api.ShowCreateTime();}

除了字段,当然也支持对属性的操作:

void Main(){    var api = new MyApi();    dynamic wrapper = ReflectionDynamicObject.Wrap(api);    wrapper.ShowTimes = 100;    Console.WriteLine(wraper.ShowTimes);}

在对属性的支持上,ReflectionDynamicObject 使用了“快速反射”技术,将取值和复制操作生成了委托以优化性能。

ReflectionDynamicObject 的实现原理

ReflectionDynamicObject 派生自 DynamicObject ,其内部通过反射技术获取到所有的属性和字段并对其 getter 和 setter 方法进行存储并通过 TryGetMember 和 TrySetMember 方法经运行时调用。

ReflectionDynamicObject 的源代码

public sealed class ReflectionDynamicObject : DynamicObject{    private readonly object _instance;    private readonly Accessor _accessor;    private ReflectionDynamicObject(object instance)    {        _instance = instance ?? throw new ArgumentNullException(nameof(instance));        _accessor = GetAccessor(instance.GetType());    }    public static ReflectionDynamicObject Wrap(Object value)        if (value == null) throw new ArgumentNullException(nameof(value));        return new ReflectionDynamicObject(value);    public override bool TryGetMember(GetMemberBinder binder, out object result)        if (_accessor.TryFindGetter(binder.Name, out var getter))        {            result = getter.Get(_instance);            return true;        }        return base.TryGetMember(binder, out result);    public override bool TrySetMember(SetMemberBinder binder, object value)        if (_accessor.TryFindSetter(binder.Name, out var setter))            setter.Set(_instance, value);        return base.TrySetMember(binder, value);    #region 快速反射    private interface IGetter        object Get(object instance);    private interface ISetter        void Set(object instance, object value);    private class Getter : IGetter        private FieldInfo _field;        public Getter(FieldInfo field)            _field = field ?? throw new ArgumentNullException(nameof(field));        public object Get(object instance)            return _field.GetValue(instance);    private class Setter : ISetter        public Setter(FieldInfo field)        public void Set(object instance, object value)            _field.SetValue(instance, value);    private class Getter<T1, T2> : IGetter        private readonly Func<T1, T2> _getter;        public Getter(Func<T1, T2> getter)            _getter = getter ?? throw new ArgumentNullException(nameof(getter));            return _getter((T1)instance);    private class Setter<T1, T2> : ISetter        private readonly Action<T1, T2> _setter;        public Setter(Action<T1, T2> setter)            this._setter = setter ?? throw new ArgumentNullException(nameof(setter));            this._setter.Invoke((T1)instance, (T2)value);    private class Accessor        public Accessor(Type type)            this._type = type ?? throw new ArgumentNullException(nameof(_type));            var getter = new SortedDictionary<string, IGetter>();            var setter = new SortedDictionary<string, ISetter>();            var fields = _type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);            foreach (var field in fields)            {                getter[field.Name] = new Getter(field);                setter[field.Name] = new Setter(field);            }            var props = _type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);            foreach (var item in props)                if (item.CanRead)                {                    var method = item.GetMethod;                    var funcType = typeof(Func<,>).MakeGenericType(item.DeclaringType, item.PropertyType);                    var func = method.CreateDelegate(funcType);                    var getterType = typeof(Getter<,>).MakeGenericType(item.DeclaringType, item.PropertyType);                    var get = (IGetter)Activator.CreateInstance(getterType, func);                    getter[item.Name] = get;                }                if (item.CanWrite)                    var method = item.SetMethod;                    var actType = typeof(Action<,>).MakeGenericType(item.DeclaringType, item.PropertyType);                    var act = method.CreateDelegate(actType);                    var setterType = typeof(Setter<,>).MakeGenericType(item.DeclaringType, item.PropertyType);                    var set = (ISetter)Activator.CreateInstance(setterType, act);                    setter[item.Name] = set;            _getters = getter;            _setters = setter;        private readonly Type _type;        private readonly IRegFCJuoASbDadOnlyDictionary<string, IGetter> _getters;        private readonly IReadOnlyDictionary<string, ISetter> _setters;        public bool TryFindGetter(string name, out IGetter getter) => _getters.TryGetValue(name, out getter);        public bool TryFindSetter(string name, out ISetter setter) => _setters.TryGetValue(name, out setter);    private static Dictionary<Type, Accessor> _accessors = new Dictionary<Type, Accessor>();    private static object _accessorsLock = new object();    private static Accessor GetAccessor(Type type)        if (_accessors.TryGetValue(type, out var accessor)) return accessor;        lock (_accessorsLock)            if (_accessors.TryGetValue(type, out accessor)) return accessor;            accessor = new Accessor(type);            var temp = new Dictionary<Type, Accessor>(_accessors);            temp[type] = new Accessor(type);            _accessors = temp;            return accessor;    #endregion}

ReflectionDynamicObject 的局限性

基于复杂度的考虑,ReflectionDynamicObject 并未添加对“方法”的支持。这也就意味着对方法的调用是缺失的。虽然动态行为让程序摆脱了对字符串的依赖,但是该实现对“重构”的支持仍然不友好。

哪里用到了 ReflectionDynamicObject ?

Liquid 主题引擎 是笔者根据 Liquid 语言和 Shopify 主题机制并采用 Fluid 模板引擎实现的一套 HTML 主题引擎。该引擎允许最终用户自由的修改自己的主题模板而不会对宿主造成影响。最终目标是做到多语言、多主题、高扩展性以及所见即所得。

在编写 Liquid 主题引擎 时,笔者需要重写 Fluid 模板引擎的 render 标签让子视图从 snippets 文件夹加载。在实现该标签时,需要访问 TemplateContext 的 LocalScope 和 RootScope 字段,不幸的是上述字段被标记为了 internal ,无法在外部程序集中访问到。于是便有了 ReflectionDynamicObject ,帮助笔者完成对 LocalScope 和 RootScope 的访问。

参考链接

Liquid 模板语言: https://www.coderbusy.com/liquid

Fluid 模板引擎:https://github.com/sebastienros/fluid

Liquid 主题引擎:https://gitee.com/zyingnet_kf/liquid-theme-engine