实体类的动态生成(二)

前言

实体类的动态生成(一)由于采用字典的方式来保存属性变更值的底层设计思想,导致了性能问题,虽然.NET的字典实现已经很高效了,但相对于直接读写字段的方式而言依然有巨大的性能差距,同时也会导致对属性的读写过程中产生不必要的装箱和拆箱。

那么这次我们就来彻底解决这个问题,同时还要解决“哪些属性发生过变”、“获取变更的属性集”这些功能特性,所以我们先把接口定义出来,以便后续问题讲解。

/* 源码位于 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),如果发生过变更则将该 bit 置为1,只要确保属性与二进制位顺序是确定的即可,算法复杂度是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;
}

上面实现代码,主要有以下几个要点:

  1. 属性设置器中除了对字段赋值外,多了一个位或赋值操作(这是一句非常低成本的代码);

  2. 需要一个额外的整型数的实例字段 _MASK_ ,来标记对应更改属性序号;

  3. 分别增加 __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万个实例的截图:

640?wx_fmt=png

  • “Native Object: 295”表示原生实现版(即简单的读写字段)的运行时长(单位:毫秒,下同);

  • “Data Entity: 295”为本案的运行时长,通常本方案比原生方案要慢10毫秒左右,偶尔能跑平(属于运行环境抖动,可忽略);

  • “Data Entity(TrySet): 835”为本方案中 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_;#endregionpublic 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)留言讨论。

相关文章:

  • 实体类的动态生成(一)

原文地址:http://zongsoft.github.io/blog/zh-cn/zongsoft/entity-dynamic-generation-2/

.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

640?wx_fmt=jpeg

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/320727.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CF442C-Artem and Array【贪心】

正题 题目链接:https://www.luogu.com.cn/problem/CF442C 题目大意 nnn个数&#xff0c;删除一个数可以获得左右两边最小值的价值&#xff0c;求删除所有数的最大价值。 解题思路 对于一个位置如果它左右两边都比它高那么这个位置一定删除&#xff0c;然后序列会呈一个单峰状…

Codeforces 1005D Polycarp and Div 3

Codeforces 1005D Polycarp and Div 3 dp[i]表示前i个数最多能分成多少块%3为0&#xff0c;nxt[x]表示x这个上一次出现的位置。 首先想到 $ dp[i] max(dp[j]) 1, (sum[i]-sum[j]) mod 3 0$&#xff0c;然后注意到他一定是从最近的那个满足条件的位置&#xff0c;也就是nxt[i…

维修栅栏【DP】

维修栅栏 题目大意&#xff1a; 有一串数&#xff0c;要把这一串数改成全部非0的&#xff0c;每一次可以更改某一段的全部数字&#xff0c;但有代价sqrt(m)sqrt(m)sqrt(m)&#xff08;m为当前子串的长度&#xff09; 原题&#xff1a; 题目描述 农场的栅栏年久失修&#x…

hdu6356-Glad You Came【RMQ】

正题 题目链接:http://acm.hdu.edu.cn/showproblem.php?pid6356 题目大意 nnn个数的一个序列aaa开始都是000。mmm个操作[li,ri,vi][l_i,r_i,v_i][li​,ri​,vi​]表示axmax{ax,v}(li≤x≤ri)a_xmax\{a_x,v\}(l_i\leq x\leq r_i)ax​max{ax​,v}(li​≤x≤ri​)&#xff0c;求…

2015 German Collegiate Programming Contest (GCPC 15)

2015 German Collegiate Programming Contest (GCPC 15) B. Bounty Hunter II 给定一张DAG&#xff0c;求一种方案&#xff1a;用最少的路径将所有点覆盖。写了按长度贪心&#xff0c;按出度的贪心。。。果断挂了。下来搜了下题解&#xff0c;看到了二分图。就懂了。。。把点拆…

ASP.NET Core URL Rewrite中间件

URL重写是基于一个或多个预置规则修改请求URL的行为。URL重写在资源位置和访问地址之间创建了一种抽象&#xff0c;这样二者之间就减少了紧密的联系。URL重写有多种适用的场景&#xff1a;临时或永久移动或替换服务器资源&#xff0c;同时为这些资源保持稳定的访问为不同应用程…

打击犯罪【并查集】

打击犯罪 题目大意&#xff1a; 有n个人&#xff0c;相互之间有一些关系&#xff0c;从而形成一个图&#xff0c;现在要从1……n1……n1……n按顺序去掉k个人&#xff08;即去掉1……k1……k1……k&#xff09;&#xff0c;使最大的子图的点数 <n/2<n/2<n/2&#xf…

StackExchange.Redis性能调优

编者&#xff1a;.net core redis 驱动推荐&#xff0c;为什么不使用 StackExchange.Redis 引起了很大的反响&#xff0c;大家反应过度&#xff0c;其实StackExchange.Redis 2.0已经从重构了异步队列&#xff0c;使用管道方式解决异步慢的问题。这篇文章也可以帮助大家正确认识…

糊涂的教授【拓扑排序】

糊涂的教授 题目大意&#xff1a; 有n个矩阵&#xff08;有些部分重叠在一起&#xff09;&#xff0c;现在有一些位置写着一些数字&#xff0c;表示它原来的序号&#xff0c;问每一个矩阵原来的序号 原题&#xff1a; 题目描述 陈教授是一个国际知名的教授&#xff0c;很多…

P6860-象棋与马【欧拉函数,杜教筛】

出题人来报个到 正题 题目链接:https://www.luogu.com.cn/problem/P6860 题目大意 p(a,b)1p(a,b)1p(a,b)1当且经当一只走a∗ba*ba∗b矩形的马可以走到棋盘上任何一个点 求∑a1n∑b1np(a,b)\sum_{a1}^n\sum_{b1}^np(a,b)a1∑n​b1∑n​p(a,b) 解题思路 这个马能走到全图的充要…

Codeforces Round #498 (Div. 3)

D. Two Strings Swaps 容易发现&#xff0c;a[i], a[n-i1], b[i], b[n-i1] 可以互相交换&#xff0c;且不会受其他地方影响&#xff0c;关键在于对于这4个字符怎们计算最小的操作数&#xff0c;讨论到死。。。看了别人的代码&#xff0c;用不同的字符对数表示字符的组成&#x…

用HttpClientFactory来实现简单的熔断降级

前言在2.1之后&#xff0c;有不少新东西&#xff0c;其中HttpClientFactory算是一个。HttpClientFactory涉及的东西也不算少&#xff0c;三四种clients , 请求中间件&#xff0c;与Polly的结合&#xff0c;生命周期等。Steeltoe的组件升级到2.1后&#xff0c;不少示例代码已经使…

【前缀和】【DP】登机(jzoj 5535)

登机 jzoj 5535 题目大意&#xff1a; 有一架飞机&#xff0c;有n个人要登机&#xff0c;每个人的不满值为登机时当前机舱在他所在行前方的人数总和&#xff0c;现在可以把飞机分为k个机舱&#xff0c;使不满值总和最小 原题&#xff1a; 题目描述 小H是机场登机的执行经…

后缀数组学习笔记

后缀数组学习笔记 说在前边 学习了《后缀数组——处理字符串的有力工具》终于感觉入门了&#xff0c;就总结一下&#xff0c;主要是应用原理讲解学习了 大佬Blog一些性质 height数组&#xff1a;定义height[i]suffix(sa[i-1])和suffix(sa[i])的最长公共前缀&#xff0c;也就是排…

CF140C-New Year Snowmen【优先队列】

正题 题目链接:https://www.luogu.com.cn/problem/CF140C 题目大意 nnn个雪球&#xff0c;一个雪人需要用333个不同大小的雪球堆起&#xff0c;求最多雪人。 解题思路 我们每次拿相同雪球中最多的三个来堆即可&#xff0c;用优先队列维护。 时间复杂度O(nlog⁡n)O(n\log n)O…

基于.net standard 的动态编译实现

在前文[基于.net core 微服务的另类实现]结尾处&#xff0c;提到了如何方便自动的生成微服务的客户端代理&#xff0c;使对于调用方透明&#xff0c;同时将枯燥的东西使用框架集成&#xff0c;以提高使用便捷性。在尝试了基于 Emit 中间语言后&#xff0c;最终决定使用生成代码…

线段树动态开点区间加区间求和

线段树动态开点区间加区间求和 题目来源&#xff1a; 陕西师范大学第七届程序设计竞赛网络同步赛 H. 万恶的柯怡 思想&#xff1a; 保证叶子节点被完整的覆盖&#xff0c;需要开节点&#xff0c;就把左右儿子都开出来&#xff0c;其余和普通线段树一样。 tips&#xff1a; 用结…

初一模拟赛总结(6.6 my brother高考前一天,加油!(。・`ω´・。))

成绩&#xff1a; 注&#xff1a;rankrankrank是有算其他$dalao的 T1T1T1好像因为精度问题被卡了 rankrankranknamenamenamescorescorescoreT1T1T1T2T2T2T3T3T3T4T4T4111hkyhkyhky180180180100100100000808080000222whdwhdwhd130130130100100100000202020101010222lyflyflyf13…

P1337-[JSOI2004]平衡点/吊打XXX【模拟退火】

正题 题目链接:https://www.luogu.com.cn/problem/P1337 题目大意 nnn个点有重量wiw_iwi​&#xff0c;求重心。 解题思路 模拟退火随机找一个重心然后不断接近即可。 codecodecode #include<cstdio> #include<cstring> #include<algorithm> #include<…

基于阿里云 DNS API 实现的 DDNS 工具

0.简要介绍0.1 思路说明AliDDNSNet 是基于 .NET Core 开发的动态 DNS 解析工具&#xff0c;借助于阿里云的 DNS API 来实现域名与动态 IP 的绑定功能。工具核心就是调用了阿里云 DNS 的两个 API &#xff0c;一个 API 获取指定域名的所有解析记录&#xff0c;然后通过比对与当前…