实体类的动态生成(一)

前言

在应用开发中,通常都会涉及各种 POJO/POCO 实体类(DO, DTO, BO, VO)的编写,有时这些实体类还需要实现 INotifyPropertyChanged 接口以支持属性变更通知,一般我们都会手写这些代码或者通过工具根据数据库表定义抑或别的什么模板、映射文件之类的来静态生成它们。

但是,在业务实现中往往伴随着诸如“如何简单且高效的获取某个实体实例有哪些属性发生过变更?”、“变更后的值是什么?”这样的问题,而大致的解决方法有:

  1. 由实体容器来跟踪实例的属性变更;

  2. 改造实体类(譬如继承特定实体基类,在基类中实现这些基础构造)。

方法(1)需要配合一整套架构设计来提供支撑,也不是专为解决上述实体类的问题而设,并且实现和使用也都不够简单高效,故此略过不表。接下来我将通过几篇文章来详细阐述这些问题的来由以及解决方案,并给出完整的代码实现以及性能比对测试。

关于源码

下面将要介绍的所有代码均位于我们的开源系列项目(地址:https://github.com/Zongsoft),项目主要采用 LGPL 2.1授权协议,欢迎大家参与并使用(请遵照授权协议)。

本文相关的源码位于其中 Zongsoft.CoreLibrary 项目的 feature-data 分支(https://github.com/Zongsoft/Zongsoft.CoreLibrary/tree/feature-data)及其中的 /samples/Zongsoft.Samples.Entities 范例项目,由于目前我正在忙着造 Zongsoft.Data 数据引擎这个轮子,不排除后面介绍到的代码会有一些调整,待该项目完成后这些代码亦会合并到 master 分支中,敬请留意。

基础版本

万里长城也是从第一块砖头开始磊起来的,就让我们来搬第一块砖吧:

public class User
{private uint _userId;private string _name;// 传统写法public uint UserId
   {get {return _userId;}set {_userId = value;}}// C# 7.0 语法public string Name
   {get => _name;set => _name = value;}// 懒汉写法:仅限不需要操作成员字段的场景public string Namespace
   {get;set;}
}

以上代码特地用了三种编码方式,它们被C#编译器生成的IL没有模式上的不同,故而性能没有任何区别,大家根据自己的口味采用某种即可,因为我们的源码由于历史原因可能会有一些混写,在此一并做个展示而已。

由于业务需要,我们希望实体类能支持属性变更通知,即让它支持 INotifyPropertyChanged 接口,这么简单的需求当然不在话下:

public class User : INotifyPropertyChanged
{public event PropertyChangedEventHandler PropertyChanged;private uint _userId;private string _name;public uint UserId
    {get => _userId;set {if(_userId == value)return;_userId = value;this.OnPropertyChanged("UserId"); // 传统写法}}public string Name
   {get => _name;set {if(_name == value)return;_name = value;this.OnPropertyChanged(nameof(Name)); // nameof 为 C# 7.0 新增操作符}}protected virtual void OnPropertyChanged(string propertyName){// 注意 ?. 为 C# 7.0 新增操作符this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));}
}

一切看起来是那么完美,但是,当我们写了几个这样的实体类,尤其是有些实体类的属性还不少时,体验就有点糟糕了。自然我们会想到写个实体基类来实现属性变更通知的基础构造,当然,在某些特定场景也可以通过工具来生成类似上面这样的C#实体类文件,但工具生成的方式有一定局限性并且不易维护(譬如需要在生成的代码基础上进行特定改造),在此不再赘述。

实体基类

在进行基础类库或API设计的时候,我有个建议:从应用场景开始。具体的作法是,先尝试编写使用这些API的应用代码,待各种应用场景的使用代码基本都完成后,API接口也就自然而然的确定了。譬如,在我们这个需求中我希望这么去使用实体基类:

public class User : ModelBase
{private uint _userId;private string _name;public uint UserId
   {get => _userId;set => this.SetPropertyValue(nameof(UserId), ref _userId, value);}public string Name
   {get => _name;set => this.SetPropertyValue(nameof(Name), ref _name, value);}
}

有了这样的实体基类后,增强了功能后代码依然如第一块砖的“基础版本”一样简洁,真是高兴啊!但这就够了么,能不能把具体实体类里面的成员字段也省了,交给基类来处理呢?嗯,有点意思,试着写下应用场景代码:

public class User : ModelBase
{public uint UserId
   {get => (uint)this.GetPropertyValue(nameof(UserId));set => this.SetPropertyValue(nameof(UserId), value);}
}

看起来棒极了,代码变得更简洁了,真是天才啊!淡定,丧心病狂的 C# 设计者似乎看到了这种普遍的需求,于是在 C# 5 中增加了 System.Runtime.CompilerServices.CallerMemberNameAttribute 自定义标记,C# 编译器将自动把调用者名字生成出来传递给加注了该标记的参数,因此这样的代码还可以继续简化:

public class User : ModelBase
{public uint UserId
   {get => (uint)this.GetPropertyValue();set => this.SetPropertyValue(value);}
}

但是,属性的 getter 里面的那个类型强制转换,怎么看都像是一朵“乌云”啊,能不能把它也去掉呢?嗯,利用C#的泛型类型推断可以完美解决它,继续强势进化:

public class User : ModelBase
{public uint UserId
   {get => this.GetPropertyValue(() => this.UserId);set => this.SetPropertyValue(() => this.UserId, value);}
}

哇喔,有点小崇拜自己了,这代码漂亮的一批!至此,实体基类的API接口基本确定,已经迫不及待想要去实现它了。

提示:由于采用 CallerMemberNameAttribute 自定义标记的参数会导致 C# 编译器要求该参数必需有默认值,因此有些 SetPropertyValue(...) 方法重载版本中 propertyName 参数需要位于参数集的最后,为了与上面的范例代码对应就省略了这些参数的标记,并保持与原有范例相同的签名设计。

using System;
using System.Linq.Expressions;

public class ModelBase : INotifyPropertyChanged
{public event PropertyChangedEventHandler PropertyChanged;protected object GetPropertyValue([CallerMemberName]string propertyName = null);protected T GetPropertyValue<T>(Expression<Func<T>> property);protected void SetPropertyValue<T>(string propertyName, ref T field, T value);protected void SetPropertyValue<T>(string propertyName, T value);protected void SetPropertyValue<T>(Expression<Func<T>> property, T value);
}

实体基类的实现主要思路就是采用字典来记录各属性的变更值,有了这个基础,要继续增加诸如“获取哪些属性发生过变更”之类的需求自然就很容易了:

public class ModelBase : INotifyPropertyChanged
{// other memberspublic bool HasChanges(params string[] propertyNames);public IDictionary<string, object> GetChangedPropertys();
}

具体的代码就不在这里贴出了,有兴趣的可以参考:https://github.com/Zongsoft/Zongsoft.CoreLibrary/blob/master/src/Common/ModelBase.cs,从功能角度上看,目前的设计还是不错的。但是,某些方法的设计有严重性能缺陷的,主要有以下几点:

  1. 每次读写属性都会解析 Lambda 表达式的操作会产生巨大的性能损耗;

  2. 采用字典来保存实体属性值的设计机制,会导致值类型的属性读写反复被装箱(Boxing)、拆箱(Unboxing);

  3. 字典的读写效率也远低于直接操作成员字段的语言原语方式。

综上所述,虽然目前方案有性能缺陷,但应对一般场景其实是没有问题的,而且功能和易用性方面都是很好的;但是,性能对于后台程序猿而言犹如悬在头顶的达摩克利斯之剑,这正是这个系列文章要最终解决的问题。在此之前,如果大家有关于这个问题的性能优化方案,欢迎关注我们的公众号(Zongsoft)留言讨论

原文地址:https://mp.weixin.qq.com/s/MmBU53hxTDHzhRGoj3DjRg

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

640?wx_fmt=jpeg

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

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

相关文章

CF525D-Arthur and Walls【贪心】

正题 题目链接:https://www.luogu.com.cn/problem/CF525D 题目大意 n∗mn*mn∗m的网格&#xff0c;有∗*∗和...&#xff0c;每次可以将∗*∗改成...。求最少操作使得每个...所在的联通块都是一个矩形。 解题思路 因为一个大矩形中每一个小块也是一个矩形&#xff0c;反之同理…

SoundHound Inc. Programming Contest 2018[C. Ordinary Beauty]

SoundHound Inc. Programming Contest 2018 -Masters Tournament-[C. Ordinary Beauty] 打表找规律的。 \(n 1\) 时&#xff0c; \(ans m\)\(n 2\) 时&#xff0c; \(ans 2*(m-1)*2^{m-2}\)\(n 3\) 时&#xff0c; 1) \(d 0, ~~ ans 3*(m-1)*3^{m-2}\) 2) \(d 1, ~~ an…

初一模拟赛总结(2019.6.1)

成绩&#xff1a; 注&#xff1a;rankrankrank是有算其他dalaodalaodalao的 rankrankranknamenamenamescorescorescoreT1T1T1T2T2T2T3T3T3T4T4T4333hkyhkyhky360360360100100100606060100100100100100100444lyflyflyf320320320100100100202020100100100100100100555lthlthlth1…

实体类的动态生成(二)

前言实体类的动态生成&#xff08;一&#xff09;由于采用字典的方式来保存属性变更值的底层设计思想&#xff0c;导致了性能问题&#xff0c;虽然.NET的字典实现已经很高效了&#xff0c;但相对于直接读写字段的方式而言依然有巨大的性能差距&#xff0c;同时也会导致对属性的…

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;最终决定使用生成代码…