因为采用字典的方式来保存属性变动值的底层设计思想,致使了性能问题,虽然.NET的字典实现已经很高效了,但相对于直接读写字段的方式而言依然有巨大的性能差距,同时也会致使对属性的读写过程当中产生没必要要的装箱和拆箱。git
那么此次咱们就来完全解决这个问题,同时还要解决“哪些属性发生过变动”、“获取变动的属性集”这些功能特性,因此咱们先把接口定义出来,以便后续问题讲解。github
/* 源码位于 Zongsoft.CoreLibary 项目的 Zongsoft.Data 命名空间中 */ /// <summary> 表示数据实体的接口。</summary> public interface IEntity { /// <summary> /// 判断指定的属性或任意属性是否被变动过。 /// </summary> /// <param name="names">指定要判断的属性名数组,若是为空(null)或空数组则表示判断任意属性。</param> /// <returns> /// <para>若是指定的<paramref name="names"/>参数有值,当只有参数中指定的属性发生过更改则返回真(True),不然返回假(False);</para> /// <para>若是指定的<paramref name="names"/>参数为空(null)或空数组,当实体中任意属性发生过更改则返回真(True),不然返回假(False)。</para> /// </returns> bool HasChanges(params string[] names); /// <summary> /// 获取实体中发生过变动的属性集。 /// </summary> /// <returns>若是实体没有属性发生过变动,则返回空(null),不然返回被变动过的属性键值对。</returns> IDictionary<string, object> GetChanges(); /// <summary> /// 尝试获取指定名称的属性变动后的值。 /// </summary> /// <param name="name">指定要获取的属性名。</param> /// <param name="value">输出参数,指定属性名对应的变动后的值。</param> /// <returns>若是指定名称的属性是存在的而且发生过变动,则返回真(True),不然返回假(False)。</returns> /// <remarks>注意:即便指定名称的属性是存在的,但只要其值未被更改过,也会返回假(False)。</remarks> bool TryGetValue(string name, out object value); /// <summary> /// 尝试设置指定名称的属性值。 /// </summary> /// <param name="name">指定要设置的属性名。</param> /// <param name="value">指定要设置的属性值。</param> /// <returns>若是指定名称的属性是存在的而且可写入,则返回真(True),不然返回假(False)。</returns> bool TrySetValue(string name, object value); }
根本要点是取消用字典来保存属性值回归到字段方式,只有这样才能确保性能,关键问题是如何在写入字段值的时候,标记对应的属性发生过变动的呢?应用布隆过滤器(Bloom Filter)算法的思路来处理这个应用场景是一个完美的解决方案,由于布隆过滤器的空间效率和查询效率极高,而它的缺点在此刚好能够针对性的优化掉。算法
将每一个属性映射到一个整型数(byte/ushort/uint/ulong)的某个比特位(bit),若是发生过变动则将该位置一,只要确保属性与二进制位顺序是肯定的便可,算法复杂度是O(1),而且比特位操做的效率也是极高的。数组
有了算法,咱们写一个简单范例来感觉下:性能
public class Person : IEntity { #region 静态字段 private static readonly string[] __NAMES__ = new string[] { "Name", "Gender", "Birthdate" }; private static readonly Dictionary<string, PropertyToken<Person>> __TOKENS__ = new Dictionary<string, PropertyToken<Person>>() { { "Name", new PropertyToken<Person>(0, target => target._name, (target, value) => target.Name = (string) value) }, { "Gender", new PropertyToken<Person>(1, target => target._gender, (target, value) => target.Gender = (Gender?) value) }, { "Birthdate", new PropertyToken<Person>(2, target => target._birthdate, (target, value) => target.Birthdate = (DateTime) value) }, }; #endregion #region 标记变量 private byte _MASK_; #endregion #region 成员字段 private string _name; private bool? _gender; private DateTime _birthdate; #endregion #region 公共属性 public string Name { get => _name; set { _name = value; _MASK_ |= 1; } } public bool? Gender { get => _gender; set { _gender = value; _MASK_ |= 2; } } public DateTime Birthdate { get => _birthdate; set { _birthdate = value; _MASK_ |= 4; } } #endregion #region 接口实现 public bool HasChanges(string[] names) { PropertyToken<Person> property; if(names == null || names.Length == 0) return _MASK_ != 0; for(var i = 0; i < names.Length; i++) { if(__TOKENS__.TryGetValue(names[i], out property) && (_MASK_ >> property.Ordinal & 1) == 1) return true; } return false; } public IDictionary<string, object> GetChanges() { if(_MASK_ == 0) return null; var dictionary = new Dictionary<string, object>(__NAMES__.Length); for(int i = 0; i < __NAMES__.Length; i++) { if((_MASK_ >> i & 1) == 1) dictionary[__NAMES__[i]] = __TOKENS__[__NAMES__[i]].Getter(this); } return dictionary; } public bool TryGetValue(string name, out object value) { value = null; if(__TOKENS__.TryGetValue(name, out var property) && (_MASK_ >> property.Ordinal & 1) == 1) { value = property.Getter(this); return true; } return false; } public bool TrySetValue(string name, object value) { if(__TOKENS__.TryGetValue(name, out var property)) { property.Setter(this, value); return true; } return false; } #endregion } // 辅助结构 public struct PropertyToken<T> { public PropertyToken(int ordinal, Func<T, object> getter, Action<T, object> setter) { this.Ordinal = ordinal; this.Getter = getter; this.Setter = setter; } public readonly int Ordinal; public readonly Func<T, object> Getter; public readonly Action<T, object> Setter; }
上面实现代码,主要有如下几个要点:测试
_MASK_
,来标记对应更改属性序号;__NAMES__
和 __TOKENS__
两个静态只读变量,来保存实体类的元数据,以便更高效的实现 IEntity 接口方法。根据代码可分析出其理论执行性能与原生实现基本一致,内存消耗只多了一个字节(若是可写属性数量小于9),因为 __NAMES__
和 __TOKENS__
是静态变量,所以不占用实例空间,理论上该方案的总体效率很是高。优化
上面咱们从代码角度简单分析了下整个方案的性能和消耗,那么实际状况到底怎样呢?跑个分呗(性能对比测试代码地址:https://github.com/Zongsoft/Zongsoft.CoreLibrary/tree/feature-data/samples/Zongsoft.Samples.Entities),具体代码就不在这里占用版面了,下面给出某次在个人老旧台式机(CPU:Intel i5-3470@3.2GHz | RAM:8GB | Win10| .NET 4.6)上生成100万个实例的截图:ui
<div data-type="alignment" data-value="center" style="text-align:center">
<div data-type="p">this
<div id="l3wopz" data-type="image" data-display="block" data-align="" data-src="https://cdn.yuque.com/yuque/0/2018/png/86907/1531714549493-133f1ae1-2ef7-4109-8e85-28762ad3c803.png" data-width="356"> <img src="https://cdn.yuque.com/yuque/0/2018/png/86907/1531714549493-133f1ae1-2ef7-4109-8e85-28762ad3c803.png" width="356" /> </div>
</div>
</div>spa
TrySet(...)
方法的运行时长,因为 TrySet(...)
方法内部须要进行字典查询因此有性能损耗亦属正常,在百万量级跑到这个时长说明性能也是很不错的,若是切换到 .NET Core 2.1 的话,得益于基础类库的性能改善,还能再享受一波性能红利。综上所述,该方案付出极少的内存成本得到了与原生简单属性访问基本一致的性能,同时还提供了属性变动跟踪等新功能(即高效完成了 Zongsoft.Data.IEntity 接口中定义的那些重要功能特性),为后续业务开发提供了有力的基础支撑。
上面的实现范例代码并无实现 INotifyPropertyChanged
接口,下面补充完善下实现该接口后的属性定义:
public class Person : IEntity, INotifyPropertyChanged { // 事件声明 public event PropertyChangedEventHandler PropertyChanged; public string Name { get => _name; set { if(_name == value) return; _name = value; _MASK_ |= 1; this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); } } }
如上,属性的设置器中的作了一个新旧值的比对判断和对 PropertyChanged
事件激发,其余代码没有变化。
另外,咱们使用的是 byte 类型的 _MASK_
的标记变量来保存属性的更改状态,若是当实体的属性数量超过 8 个,就须要根据具体数量换成相应的 UInt16,UInt32,UInt64
类型,但若是超过 64 就须要采用 byte[]
了,固然必需要变更下相关代码,假设如下实体类有 100 个属性(注意仅例举了第一个 Property1
和最后一个 Property100
属性):
public class MyEntity : IEntity { #region 标记变量 private readonly byte[] _MASK_; #endregion public Person() { _MASK_ = new byte[13]; // 13 = Math.Ceiling(100 / 8) } public object Property1 { get => _property1; set { _property1 = value; _MASKS_[0] |= 1; // _MASK_[0 / 8] |= (byte)Math.Pow(2, 0 % 8); } } public object Property100 { get => _property100; set { _property100 = value; _MASKS_[12] |= 8; // _MASK_[99 / 8] |= (byte)Math.Pow(2, 99 % 8); } } }
变化内容为先根据当前属性的顺序号来肯定到对应的标记数组的下标,而后再肯定对应的掩码值。固然,也别忘了调整 Zongsoft.Data.IEntity
接口中各方法的实现。
public class MyEntity : IEntity { public bool HasChanges(params string[] names) { PropertyToken<UserEntity> property; if(names == null || names.Length == 0) { for(int i = 0; i < _MASK_.Length; i++) { if(_MASK_[i] != 0) return true; } return false; } for(var i = 0; i < names.Length; i++) { if(__TOKENS__.TryGetValue(names[i], out property) && (_MASK_[property.Ordinal / 8] >> (property.Ordinal % 8) & 1) == 1) return true; } return false; } public IDictionary<string, object> GetChanges() { var dictionary = new Dictionary<string, object>(__NAMES__.Length); for(int i = 0; i < __NAMES__.Length; i++) { if((_MASK_[i / 8] >> (i % 8) & 1) == 1) dictionary[__NAMES__[i]] = __TOKENS__[__NAMES__[i]].Getter(this); } return dictionary.Count == 0 ? null : dictionary; } public bool TryGet(string name, out object value) { value = null; if(__TOKENS__.TryGetValue(name, out var property) && (_MASK_[property.Ordinal / 8] >> (property.Ordinal % 8) & 1) == 1) { value = property.Getter(this); return true; } return false; } public bool TrySetValue(string name, object value) { /* 相对以前版本没有变化 */ /* No changes relative to previous versions */ } }
代码变化部分比较简单,只有掩码处理部分须要调整。
有了这些实现范式,定义个实体基类并在基类中完成主要功能便可推广应用了,可是,这里有个掩码类型和处理方式没法通用化实现的问题,若是要把这部分代码交由子类来实现的话,那么代码复用度会大打折扣甚至彻底失去复用的意义。
为展现这个问题的艰难,在 https://github.com/Zongsoft/Zongsoft.CoreLibrary/blob/feature-data/tests/Entities.cs 源文件中,写了属性数量不等的几个实体类(Person、Customer、Employee、SpecialEmployee),采用继承方式进行复用性验证,可清晰看到实现的很是冗长繁琐,对实现者的细节把控要求很高、实现上很是容易出错,更致命的是复用度还极差。而且当实体类须要进行属性增减,是很是麻烦的,须要仔细调整原有代码结构中掩码的映射位置,这对于代码维护无心是场恶梦。
解决办法其实很简单,正是本文的标题——“动态生成”,完全解放实现者并确保实现的正确性。业务方再也不定义具体的实体类,而是定义实体接口便可,实体类将由实体生成器来动态生成。咱们依然“从场景出发”,先来看看业务层的使用。
public interface IPerson : IEntity { string Name { get; set; } bool? Gender { get; set; } DateTime Birthdate { get; set; } } public interface IEmployee : IPerson { byte Status { get; set; } decimal Salary { get; set; } } var person = Entity.Build<IPerson>(); var employee = Entity.Build<IEmployee>();
至此,终于获得了一个兼顾性能与功能并易于使用且无需繁琐的手动实现的最终方案,虽然刚开始看起来是一个多么日常又简单的任务。那么接下来咱们该怎么实现这个动态生成器呢?最终它能性能无损的被实现出来吗? 请关注咱们的公众号(Zongsoft)留言讨论。
本文可能会更新,请阅读原文: https://zongsoft.github.io/blog/zh-cn/zongsoft/entity-dynamic-generation-2,以免因内容陈旧而致使的谬误,同时亦有更好的阅读体验。
本做品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、从新发布,但必须保留本文的署名 钟峰(包含连接:http://zongsoft.github.io),不得用于商业目的,基于本文修改后的做品务必以相同的许可发布。若有任何疑问或受权方面的协商,请致信给我 (zongsoft@qq.com)。