C# 使用Emit深克隆

原文: C# 使用Emit深克隆

有人問,複製一個類全部屬性到另外一個類有多少方法?這也就是問深克隆有多少個方法,容易想的有三個。直接複製,反射覆制,序列化複製。可是性能比較快的有表達式樹複製 IL複製兩個,本文主要講最後一個html

關於表達式樹複製,參見 Fast Deep Copy by Expression Trees (C#) - CodeProjectgit

在開始讀本文以前,我推薦兩個博客 讀懂IL代碼就這麼簡單 (一) - Zery - 博客園 秒懂C#經過Emit動態生成代碼 - 匠心十年 - 博客園數組

須要先知道一點IL的,後面才比較容易說,假設你們知道了 IL 是什麼, 知道了簡單的 IL 如何寫,那麼開始進行功能的開發。第一步是命名,由於須要把一個類的全部屬性複製到另外一個類,須要調用方法,而方法須要名字,因此第一步就是命名。函數

爲了建立方法 public void Clone<T>(T source, T los) 我就使用了下面代碼post

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

建立方法的第一個參數很容易看到,我就不解釋了,第二個參數就是方法的返回值,由於返回是 void 因此不用寫。第三個參數是函數的參數,只須要使用類型,若是有多個參數就是寫數組,若是這裏發現有看不懂的,請和我說。性能

可是定義方法後須要寫方法內的代碼,這時須要使用 ILGenerator ,使用他的 Emit 方法,這個方法的速度很快,使用的時候須要知道 IL 的,若是不知道,不要緊,我接下來會仔細說。測試

ILGenerator generator = dynamicMethod.GetILGenerator();

須要得到類型的全部屬性,雖然這裏用了反射,可是隻是用一次,由於這裏用反射得到方法是在寫IL代碼,寫完能夠不少次使用,可能第一次的速度不快,可是以後的速度和本身寫代碼編譯的速度是差很少,因此建議使用這個方法。能夠本身去使用 dot trace 去查看性能,我本身看到的是性能很好。ui

拿出全部屬性能夠讀寫的代碼foreach (var temp in typeof(T).GetProperties().Where(temp=>temp.CanRead&&temp.CanWrite))url

查看 IL 須要先把第一個參數放在左邊,第二個參數放在右邊,調用第二個參數的 get 設置第一個參數的set對應的屬性看起來的正常代碼就是spa

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/](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


本文會常常更新,請閱讀原文: https://lindexi.gitee.io/lindexi/post/C-%E4%BD%BF%E7%94%A8Emit%E6%B7%B1%E5%85%8B%E9%9A%86.html ,以免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。

知識共享許可協議 本做品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、從新發布,但務必保留文章署名林德熙(包含連接: https://lindexi.gitee.io ),不得用於商業目的,基於本文修改後的做品務必以相同的許可發佈。若有任何疑問,請 與我聯繫

相關文章
相關標籤/搜索