title author date CreateTime categories
C# 使用Emit深克隆
lindexi
2018-08-10 19:16:52 +0800
2018-2-13 17:23:3 +0800
C# dotnet Emit

有人问,复制一个类所有属性到另一个类有多少方法?这也就是问深克隆有多少个方法,容易想的有三个。直接复制,反射复制,序列化复制。但是性能比较快的有表达式树复制 IL复制两个,本文主要讲最后一个

关于表达式树复制,参见 Fast Deep Copy by Expression Trees (C#) - CodeProject

在开始读本文之前,我推荐两个博客 读懂IL代码就这么简单 (一) - Zery - 博客园 秒懂C#通过Emit动态生成代码 - 匠心十年 - 博客园

需要先知道一点IL的,后面才比较容易说,假设大家知道了 IL 是什么, 知道了简单的 IL 如何写,那么开始进行功能的开发。第一步是命名,因为需要把一个类的所有属性复制到另一个类,需要调用方法,而方法需要名字,所以第一步就是命名。

为了创建方法 public void Clone<T>(T source, T los) 我就使用了下面代码

            var dynamicMethod = new DynamicMethod("Clone", null, new[] { typeof(T), typeof(T) });

创建方法的第一个参数很容易看到,我就不解释了,第二个参数就是方法的返回值,因为返回是 void 所以不用写。第三个参数是函数的参数,只需要使用类型,如果有多个参数就是写数组,如果这里发现有看不懂的,请和我说。

但是定义方法后需要写方法内的代码,这时需要使用 ILGenerator ,使用他的 Emit 方法,这个方法的速度很快,使用的时候需要知道 IL 的,如果不知道,没关系,我接下来会仔细说。

            ILGenerator generator = dynamicMethod.GetILGenerator();

需要获得类型的所有属性,虽然这里用了反射,但是只是用一次,因为这里用反射获得方法是在写IL代码,写完可以很多次使用,可能第一次的速度不快,但是之后的速度和自己写代码编译的速度是差不多,所以建议使用这个方法。可以自己去使用 dot trace 去查看性能,我自己看到的是性能很好。

拿出所有属性可以读写的代码foreach (var temp in typeof(T).GetProperties().Where(temp=>temp.CanRead&&temp.CanWrite))

查看 IL 需要先把第一个参数放在左边,第二个参数放在右边,调用第二个参数的 get 设置第一个参数的set对应的属性看起来的正常代码就是

los.foo=source.foo;

这里的 foo 就是拿到一个属性,随意写的,写出来的 IL 请看下面。

Ldarg_1 //los
Ldarg_0 //s
callvirt instance string lindexi.Foo::get_Name()
callvirt instance void lindexi.Foo::set_Name(string)
ret

可以从上面的代码 callvirt 使用一个方法,对应压入参数,所以可以通过反射获得方法,然后调用这个方法,于是写成代码请看下面

                generator.Emit(OpCodes.Ldarg_1);// los
generator.Emit(OpCodes.Ldarg_0);// s
generator.Emit(OpCodes.Callvirt,temp.GetMethod);
generator.Emit(OpCodes.Callvirt, temp.SetMethod);

因为可以把这个拿出转化方法,于是所以的下面给所有代码

        private static void CloneObjectWithIL<T>(T source, T los)
{
var dynamicMethod = new DynamicMethod("Clone", null, new[] { typeof(T), typeof(T) });
ILGenerator generator = dynamicMethod.GetILGenerator(); foreach (var temp in typeof(T).GetProperties().Where(temp=>temp.CanRead&&temp.CanWrite))
{
generator.Emit(OpCodes.Ldarg_1);// los
generator.Emit(OpCodes.Ldarg_0);// s
generator.Emit(OpCodes.Callvirt,temp.GetMethod);
generator.Emit(OpCodes.Callvirt, temp.SetMethod);
}
generator.Emit(OpCodes.Ret);
var clone = (Action<T, T>) dynamicMethod.CreateDelegate(typeof(Action<T, T>));
clone(source, los);
}

如果测试了这个方法,那么会发现,这个方法对于这个方法不可以见的类就会出现MethodAccessException,所以传入的类需要这个方法可以直接用。

//A.dll
public class Foo
{ } CloneObjectWithIL(foo1,foo2); //B.dll
private static void CloneObjectWithIL<T>(T source, T los) 这时无法使用

之外,对于静态属性,使用上面代码也是会出错,因为静态的属性的访问没有权限,所以请看修改后的。

    /// <summary>
/// 提供快速的对象深复制
/// </summary>
public static class Clone
{
/// <summary>
/// 提供使用 IL 的方式快速对象深复制
/// 要求本方法具有T可访问
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source">源</param>
/// <param name="los">从源复制属性</param>
/// <exception cref="MethodAccessException">如果输入的T没有本方法可以访问,那么就会出现这个异常</exception>
// ReSharper disable once InconsistentNaming
public static void CloneObjectWithIL<T>(T source, T los)
{
//参见 http://lindexi.oschina.io/lindexi/post/C-%E4%BD%BF%E7%94%A8Emit%E6%B7%B1%E5%85%8B%E9%9A%86/
if (CachedIl.ContainsKey(typeof(T)))
{
((Action<T, T>) CachedIl[typeof(T)])(source, los);
return;
}
var dynamicMethod = new DynamicMethod("Clone", null, new[] { typeof(T), typeof(T) });
ILGenerator generator = dynamicMethod.GetILGenerator(); foreach (var temp in typeof(T).GetProperties().Where(temp => temp.CanRead && temp.CanWrite))
{
//不复制静态类属性
if (temp.GetAccessors(true)[0].IsStatic)
{
continue;
} generator.Emit(OpCodes.Ldarg_1);// los
generator.Emit(OpCodes.Ldarg_0);// s
generator.Emit(OpCodes.Callvirt, temp.GetMethod);
generator.Emit(OpCodes.Callvirt, temp.SetMethod);
}
generator.Emit(OpCodes.Ret);
var clone = (Action<T, T>) dynamicMethod.CreateDelegate(typeof(Action<T, T>));
CachedIl[typeof(T)] = clone;
clone(source, los);
} private static Dictionary<Type, Delegate> CachedIl { set; get; } = new Dictionary<Type, Delegate>();
}

需要注意,这里的复制只是复制类的属性,对类的属性内是没有进行复制。如果存在类型 TestA1 ,请看下面代码。

        public class TestA1
{
public string Name { get; set; }
}

那么在执行下面的代码之后,得到的 TestA1 是相同的。

        public class Foo
{
public string Name { get; set; } public TestA1 TestA1 { get; set; }
} var foo = new Foo()
{
Name = "123",
TestA1 = new TestA1()
{
Name = "123"
}
}; var foo1 = new Foo(); Clone.CloneObjectWithIL(foo, foo1);
foo1.TestA1.Name == foo.TestA1.Name foo.Name = "";
foo.TestA1.Name = "lindexi"; foo1.TestA1.Name == foo.TestA1.Name

那么上面的代码在什么时候可以使用?实际如果在一个创建的类需要复制基类的属性,那么使用这个方法是很好,例如在 Model 会创建一些类,而在 ViewModel 有时候需要让这些类添加一些属性,如 Checked ,那么需要重新复制 Model 的属性,如果一个个需要自己写属性复制,那么开发速度太慢。所以这时候可以使用这个方法。

例如基类是 Base ,继承类是Derived ,请看下面代码

public class Base
{
public string BaseField;
} public class Derived : Base
{
public string DerivedField;
} Base base = new Base();
//some alother code
Derived derived = new Derived();
CloneObjectWithIL(base, derived);

如果需要复制一个类到一个新类,可以使用这个代码

    private static T CloneObjectWithIL<T>(T myObject)
{
Delegate myExec = null;
if (!_cachedIL.TryGetValue(typeof(T), out myExec))
{
// Create ILGenerator
DynamicMethod dymMethod = new DynamicMethod("DoClone", typeof(T), new Type[] { typeof(T) }, true);
ConstructorInfo cInfo = myObject.GetType().GetConstructor(new Type[] { }); ILGenerator generator = dymMethod.GetILGenerator(); LocalBuilder lbf = generator.DeclareLocal(typeof(T));
//lbf.SetLocalSymInfo("_temp"); generator.Emit(OpCodes.Newobj, cInfo);
generator.Emit(OpCodes.Stloc_0);
foreach (FieldInfo field in myObject.GetType().GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic))
{
// Load the new object on the eval stack... (currently 1 item on eval stack)
generator.Emit(OpCodes.Ldloc_0);
// Load initial object (parameter) (currently 2 items on eval stack)
generator.Emit(OpCodes.Ldarg_0);
// Replace value by field value (still currently 2 items on eval stack)
generator.Emit(OpCodes.Ldfld, field);
// Store the value of the top on the eval stack into the object underneath that value on the value stack.
// (0 items on eval stack)
generator.Emit(OpCodes.Stfld, field);
} // Load new constructed obj on eval stack -> 1 item on stack
generator.Emit(OpCodes.Ldloc_0);
// Return constructed object. --> 0 items on stack
generator.Emit(OpCodes.Ret); myExec = dymMethod.CreateDelegate(typeof(Func<T, T>));
_cachedIL.Add(typeof(T), myExec);
}
return ((Func<T, T>)myExec)(myObject);
}

http://www.c-sharpcorner.com/uploadfile/puranindia/reflection-and-reflection-emit-in-C-Sharp/

https://stackoverflow.com/a/46580446/6116637

2018-8-10-C#-使用Emit深克隆的更多相关文章

  1. 申请Office 365一年免费的开发者账号攻略(2018年10月份版本)

    要进行Office 365开发,当然需要有完整的Office 365环境才可以.为了便于广大开发人员快速地启动这项工作,微软官方给所有开发人员提供了免费的一年开发者账号   那么如何申请Office ...

  2. IntelliJ IDEA 最新激活码(截止到2018年10月14日)

    IntelliJ IDEA 注册码: EB101IWSWD-eyJsaWNlbnNlSWQiOiJFQjEwMUlXU1dEIiwibGljZW5zZWVOYW1lIjoibGFuIHl1IiwiYX ...

  3. 新手C#SQL Server使用记录2018.08.10

    主键(PrimaryKey):主键就是每个数据行(记录)的唯一标识,不会有重复值的列(字段)才能当做主键.一个表可以没有主键,但是这样会很难处理表,因此一般情况表都要设置主键. 主键有两张选用策略,分 ...

  4. 01 mybatis框架整体概况(2018.7.10)-

    01 mybatis框架整体概况(2018.7.10)- F:\廖雪峰 JavaEE 企业级分布式高级架构师课程\廖雪峰JavaEE一期\第一课(2018.7.10) maven用的是3.39的版本 ...

  5. 北京化工大学2018年10月程序设计竞赛部分题解(A,C,E,H)

    目录 北京化工大学2018年10月程序设计竞赛部分题解(A,C,E,H) 竞赛事件相关 竞赛链接 竞赛题目 总结 北京化工大学2018年10月程序设计竞赛部分题解(A,C,E,H) 竞赛事件相关 竞赛 ...

  6. 富士康的盈利秒杀99%的A股公司:3星|《三联生活周刊》2018年10期

    三联生活周刊·最美的数学:天才为何成群到来(2018年10期) 本期专题是数学和成都,我都跳过去没看.其他内容也还有点意思. 总体评价3星. 以下是本期一些内容的摘抄,#号后面是kindle电子版中的 ...

  7. Burn Down Chart(2018.6.4~2018.6.10)

    Burn Down Chart (2018.6.4~2018.6.10) 娄雨禛[前端部分] 曾子轩[后端部分+燃尽图] 前端 1. 娄雨禛+李鑫 1)在总工程中完成跳转,实现图片显示,并发布到Git ...

  8. C# 使用Emit深克隆

    原文:C# 使用Emit深克隆 有人问,复制一个类所有属性到另一个类有多少方法?这也就是问深克隆有多少个方法,容易想的有三个.直接复制,反射复制,序列化复制.但是性能比较快的有表达式树复制 IL复制两 ...

  9. Java分布式互联网架构/微服务/高性能/springboot/springcloud 2018年10月17日直播内容

    2018年10月17日直播内容 大规模并发必备的消息中间件技术ActiveMq 网盘链接: https://pan.baidu.com/s/1GlxsZ2JnrvX- YN16-S7lQw 提取码: ...

随机推荐

  1. 解决Apache日志"internal dummy connection"方法

    最近查看服务器中apache日志,发现有大量的 OPTIONS * HTTP/1.0" 200 - "-" "Apache (internal dummy co ...

  2. Mybatis自查询递归查找子菜单

    之前写过 java从数据库读取菜单,递归生成菜单树 今天才发现mybatis也可以递归查询子菜单 先看一下数据库 主键id,名称name,父id,和url 设计菜单类 public class Men ...

  3. hdu4267 线段树

    开始敲了一发线段树,觉得可以暴力一点的过,tle了.后来进行修改,发现了问题. 后来一看大神的做法,由于1<=k<=10,所以对于不同的k,有55个余,找答案的时候只要找不同的k值满足条件 ...

  4. Jmeter VS LR参数取值方式和迭代方式

    Jmeter的参数化 Jmeter中的参数就是变量. 变量的来源:测试计划.UDV.CSV.函数.正则表达式.数据库. 以Jmeter的CSV文件参数化为例:

  5. Ceph 之Multisite 下的bucket reshard

    目录 一.背景和问题 二.bucket reshard 过程 主集群信息汇总 Multisite 下手动reshard References 一.背景和问题 默认情况下只有当单个bucket承载的ob ...

  6. LeetCode21 Merge Two Sorted Lists

    题意: Merge two sorted linked lists and return it as a new list. The new list should be made by splici ...

  7. Linux 下的mysql+centos7+主从复制

    mysql+centos7+主从复制   MYSQL(mariadb) MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用GPL授权许可.开发这个分支的原因之一是:甲骨文公 ...

  8. vagrant 安装 ubuntu

    安装版本: ubuntu  trusty64(14.04) step1: 安装vagrant,vbox step2: 下载box文件(官网http://www.vagrantbox.es/) http ...

  9. 2018-8-10-WPF-编译为-AnyCPU-和-x86-有什么区别

    title author date CreateTime categories WPF 编译为 AnyCPU 和 x86 有什么区别 lindexi 2018-08-10 19:16:53 +0800 ...

  10. 学生信息管理系统之【修改信息窗口】 标签: 数据库vb 2014-06-13 21:23 1167人阅读 评论(15)

    自从开始敲学生信息管理,就发现有几个窗口从来木有成功打开过,它们是(修改学籍信息)(修改成绩信息)和(修改课程信息)窗口,这几个窗口每次想打开的时候都会弹出"实时错误:3021"这 ...