C#编程之使用Emit深克隆的方法
小标 2018-05-18 来源 : 阅读 1464 评论 0

摘要:在C#编程学习中,会碰到:复制一个类所有属性到另一个类有多少方法?这也就是问深克隆有多少个方法,容易想的有三个。直接复制,反射复制,序列化复制。但是性能比较快的有表达式树复制 IL复制两个,本文主要讲最后一个,希望对大家的C#编程的学习有所帮助。

在C#编程学习中,会碰到:复制一个类所有属性到另一个类有多少方法?这也就是问深克隆有多少个方法,容易想的有三个。直接复制,反射复制,序列化复制。但是性能比较快的有表达式树复制 IL复制两个,本文主要讲最后一个,希望对大家的C#编程的学习有所帮助。

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

为了创建方法 public void Clone(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);
}</t,></t,></t>

   

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


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

   

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


    /// <summary>
    /// 提供快速的对象深复制
    /// </summary>
    public static class Clone
    {
        /// <summary>
        /// 提供使用 IL 的方式快速对象深复制
        /// 要求本方法具有T可访问
        /// </summary>
        /// <typeparam name="T"></typeparam>
        ///<param name="source">源
        ///<param name="los">从源复制属性
        /// <exception cref="MethodAccessException">如果输入的T没有本方法可以访问,那么就会出现这个异常</exception>
        // ReSharper disable once InconsistentNaming
        public static void CloneObjectWithIL<t>(T source, T los)
        {
            //参见 https://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="">();
    }
</type,></type,></t,></t,></t,></t>

   

需要注意,这里的复制只是复制类的属性,对类的属性内是没有进行复制。如果存在类型 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);
}</t,></t,></t>

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注职坐标编程语言C#.NET频道!

本文由 @小标 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式AI+学习就业服务平台 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved