【型別轉換】使用c#實現簡易的型別轉換(Emit,Expression,反射)

四處觀察發表於2023-12-28

引言

    哈嘍。大家好,好久不見,最近遇到了一個場景,就是在FrameWork的asp.net mvc中,有個系統裡面使用的是EntityFramework的框架,在這個框架裡,提供了一個SqlQuery的方法,這個方法很好用啊,以至於在EFCORE8裡面又新增了回來,不過不知道效能怎麼樣,我遇到的場景是透過SqlQuery查詢的時候,轉換很慢,我估計那背後大機率是使用反射造成的, 因為我的查詢可能有上十萬,甚至更多,就導致了這個轉換的過程及其耗時,以至於剛開始我是想透過Emit等方式去實現一個高效能轉換,可是到最後沒有去弄,因為我用了DataCommand去查詢,最後迴圈DataReader來實現硬賦值,這樣效能是最好,一下減少了好多秒,提升了80%,但也給了我一個靈感,一個實現簡易的型別轉換的靈感,所以在上週我就把程式碼寫了出來,不過由於工作的忙碌,今天才開始寫部落格,接下來就呈上。

    對了,有關EMIT和表示式樹的知識,諸位可以看我之前的部落格表示式樹,IL。其中IL有兩合集。

EMIT

    眾所周知,我們的c#程式碼在編譯器編譯,都會編譯成IL程式碼,最後再去透過JIT轉化為機器碼,執行在系統中去的,所以IL程式碼的效能是比c#程式碼高的,同時,學習的成本,編寫的成本也是機器高,我也是在自己感興趣,瞎琢磨,就會了一絲絲皮毛,很多人說IL難寫,其實,對於程式碼中的Opcodes那些,我只記一些常用的,對於剩下的,我都是在寫的時候才去看文件,總之呢,要學的東西有很多,但掌握了學習的方法,才是持之以恆的手段。

    接下來,就呈上IL程式碼,分為兩部分,一個是List轉List,一個是實體轉實體的。

    在這幾個例子中,所有的前提都是實體的屬性名稱是一樣的,如果需要擴充套件型別不一樣,或者哪些不轉換,從哪個屬性轉換到哪個屬性,就需要各位自己去擴充套件了,本來我是想寫這些的,,但是懶癌犯了,哈哈哈哈,需要各位看官自己動手了,以下程式碼,除了反射,其他的我都加了註釋,反射大家都看得懂。

    在下面的第一個方法,我們定義了執行轉換集合的方法,並返回了一個委託,我們在實際開發中,都可以返回委託,最終可以將方法快取起來,這樣在後續的時候直接呼叫,效能提升爆炸,因為你每次建立Emit方法的時候,耗時也會挺長的,在有時候,像哪些主流的Mapper,他們內部肯定都做了快取。下面的集合轉集合,大致的原理程式碼就是定義一個方法ConvertToType,返回型別是List<TR>,入參是List<T>,然後定義迴圈的開始結束變數,以及最終返回結果集,還有迴圈內部的時候,我們建立的變數,最終將這個變數新增到返回的結果集中,在往下就是拿入參集合的數量,定義迴圈開始和結束的label,在往下走就是,初始化返回值集合,賦值給本地的localRes變數,將0賦值給開始迴圈的變數,也就是for(int i=0),下面就是給結束的迴圈值賦值為入參集合的Count。

    下面的程式碼每行基本都有註釋,所以我在這裡也不做過多的講解。

    集合和單個的區別就在於集合是多了一個迴圈的主體,其他都和單個是一樣的,以及集合的程式碼塊中,我沒有新增try catch的程式碼塊。

    internal class EmitExecute<T, TR> : IExecute<T, TR> where T : class where TR : class, new()
    {
        public Func<List<T>, List<TR>> ExecuteList()
        {
            var dynamicMethod = new DynamicMethod("ConvertToType", typeof(List<TR>), new Type[] { typeof(List<T>) });
            #region 變數定義
            var ilMethod = dynamicMethod.GetILGenerator();
            var localBegin = ilMethod.DeclareLocal(typeof(int));//定義迴圈開始變數
            var localEnd = ilMethod.DeclareLocal(typeof(int));//結束變數
            var localres = ilMethod.DeclareLocal(typeof(List<TR>));//返回值
            var localSignleRes = ilMethod.DeclareLocal(typeof(TR));//變數
            var countMethod = typeof(List<T>).GetProperty("Count").GetGetMethod();
            #endregion
            var labelXunhuan = ilMethod.DefineLabel();//定義迴圈主體
            var labelEnd = ilMethod.DefineLabel();//定義結束標籤主體
            #region 例項化返回值
            ilMethod.Emit(OpCodes.Nop);
            ilMethod.Emit(OpCodes.Newobj, typeof(List<TR>).GetConstructor(Type.EmptyTypes));//初始化返回值變數
            ilMethod.Emit(OpCodes.Stloc, localres);//結果賦值給返回值變數變數
            #endregion
            #region 給迴圈的起始變數賦值 0是開始,count是獲取的入參的集合的count
            ilMethod.Emit(OpCodes.Ldc_I4_0);//將0載入到棧上
            ilMethod.Emit(OpCodes.Stloc, localBegin);//給迴圈的開始值begin賦值0
            ilMethod.Emit(OpCodes.Ldarg_0);//載入入參
            ilMethod.Emit(OpCodes.Callvirt, countMethod);//將入參的count載入到棧上
            ilMethod.Emit(OpCodes.Stloc, localEnd);//給結束變數賦值為集合的count
            ilMethod.Emit(OpCodes.Br, labelXunhuan);//無條件跳轉到迴圈的label,即開始迴圈
            #endregion
            #region 迴圈結束標籤
            ilMethod.MarkLabel(labelEnd);//標記這段程式碼是labelend的程式碼

            ilMethod.Emit(OpCodes.Ldloc, localres);//載入返回值變數
            ilMethod.Emit(OpCodes.Ret);//返回
            #endregion
            #region 迴圈主體
            ilMethod.MarkLabel(labelXunhuan);//標記是labelbegin的程式碼
            ilMethod.Emit(OpCodes.Ldloc, localBegin);//從棧中載入開始變數
            ilMethod.Emit(OpCodes.Ldloc, localEnd);//從棧中載入結束變數
            ilMethod.Emit(OpCodes.Bge, labelEnd);//比較第0個是否小於等於第一個

            //ilMethod.Emit(OpCodes.Call, typeof(List<People>).GetMethod("Add"));
            ilMethod.Emit(OpCodes.Newobj, typeof(TR).GetConstructor(Type.EmptyTypes));//建立people的例項化
            ilMethod.Emit(OpCodes.Stloc, localSignleRes);//結果賦值people變數
            var properties = typeof(T).GetProperties().Where(s => typeof(TR).GetProperties().Any(a => a.Name == s.Name)).ToList();
            if (properties != null)
            {
                foreach (var item in properties)
                {
                    var getMethod = typeof(T).GetProperty(item.Name).GetGetMethod();//獲取list【i】的species屬性
                    var setMethod = typeof(TR).GetProperty(item.Name).GetSetMethod();
                    ilMethod.Emit(OpCodes.Ldloc, localSignleRes);//載入定義好的people
                    ilMethod.Emit(OpCodes.Ldarg_0);//載入入參
                    ilMethod.Emit(OpCodes.Ldloc, localBegin);//載入迴圈的開始變數
                    ilMethod.Emit(OpCodes.Callvirt, typeof(List<T>).GetMethod("get_Item"));// 呼叫 List<Animal>.get_Item 方法,將結果壓入棧頂
                    ilMethod.Emit(OpCodes.Callvirt, getMethod);//拿到getitem的返回值的species屬性
                    ilMethod.Emit(OpCodes.Call, setMethod);//給people的species屬性賦值
                }
            }

            //ilMethod.Emit(OpCodes.Ldloc, localPeople);//載入定義好的people
            //ilMethod.Emit(OpCodes.Ldarg_0);//載入入參
            //ilMethod.Emit(OpCodes.Ldloc, localBegin);//載入迴圈的開始變數
            //ilMethod.Emit(OpCodes.Callvirt, typeof(List<T>).GetMethod("get_Item"));// 呼叫 List<Animal>.get_Item 方法,將結果壓入棧頂
            //ilMethod.Emit(OpCodes.Callvirt, getAMethod);//拿到getitem的返回值的species屬性
            //ilMethod.Emit(OpCodes.Call, property);//給people的species屬性賦值

            ilMethod.Emit(OpCodes.Ldloc, localres);//載入返回值
            ilMethod.Emit(OpCodes.Ldloc, localSignleRes);//載入people

            ilMethod.Emit(OpCodes.Call, typeof(List<TR>).GetMethod("Add"));//將people新增道返回值
            ilMethod.Emit(OpCodes.Ldc_I4_1);//將1載入到棧上
            ilMethod.Emit(OpCodes.Ldloc, localBegin);//從棧中載入開始變數
            ilMethod.Emit(OpCodes.Add);//相加
            ilMethod.Emit(OpCodes.Stloc, localBegin);//結果賦值給本地0個區域性變數
            ilMethod.Emit(OpCodes.Br, labelXunhuan);
            #endregion
            var func = dynamicMethod.CreateDelegate<Func<List<T>,List<TR>>>();
            return func;
        }

        public Func<T, TR> ExecuteSingle()
        {
            var dynamicMethod = new DynamicMethod("ConvertToType", typeof(TR), new Type[] { typeof(T) });
            var method = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) });//輸出字串
            var ilMethod = dynamicMethod.GetILGenerator();
            var localRes = ilMethod.DeclareLocal(typeof(TR));//變數
            var ex = typeof(Exception);
            var localEx = ilMethod.DeclareLocal(ex);
            var str = typeof(Exception).GetMethod("ToString");
            ilMethod.Emit(OpCodes.Nop);
            ilMethod.Emit(OpCodes.Newobj, typeof(TR).GetConstructor(Type.EmptyTypes));//初始化返回值變數  new tr();
            ilMethod.Emit(OpCodes.Stloc, localRes);//結果賦值給返回值變數變數var tr=new tr();
            var properties = typeof(T).GetProperties().Where(s => typeof(TR).GetProperties().Any(a => a.Name == s.Name)).ToList();
            if (properties != null)
            {
                ilMethod.BeginExceptionBlock();//try
                foreach (var item in properties)
                {
                    var getMethod = typeof(T).GetProperty(item.Name).GetGetMethod();//獲取list【i】的species屬性
                    var setMethod = typeof(TR).GetProperty(item.Name).GetSetMethod();
                    ilMethod.Emit(OpCodes.Ldloc, localRes);//載入定義好的people
                    ilMethod.Emit(OpCodes.Ldarg_0);//載入入參
                    ilMethod.Emit(OpCodes.Callvirt, getMethod);//拿到getitem的返回值的species屬性
                    ilMethod.Emit(OpCodes.Call, setMethod);//給people的species屬性賦值
                }
                ilMethod.BeginCatchBlock(ex);
                ilMethod.Emit(OpCodes.Stloc, localEx);// 將異常的ex賦值給我們的本地變數ex
                ilMethod.Emit(OpCodes.Ldloc, localEx);//載入ex變數
                ilMethod.Emit(OpCodes.Callvirt, str);//呼叫tostring()
                ilMethod.Emit(OpCodes.Call, method);//呼叫console.writeline方法列印出異常資訊
                ilMethod.Emit(OpCodes.Newobj, typeof(TR).GetConstructor(Type.EmptyTypes));
                ilMethod.Emit(OpCodes.Stloc, localRes);//結果賦值給返回值變數變數
                ilMethod.EndExceptionBlock();
            }
            ilMethod.Emit(OpCodes.Ldloc, localRes);//載入返回值
            ilMethod.Emit(OpCodes.Ret);
            var func = dynamicMethod.CreateDelegate<Func<T, TR>>();
            return func;
        }
    }
}

Expression

    接下來,是表示式樹的實現方式,表示式樹的其實和Emit的我感覺都差不多,不過和emit相比,肯定大家都喜歡寫Expression,畢竟是c#程式碼,寫起來比較舒適,在下面程式碼就是定義了入參的source,以及從source那指定索引的item,以及返回值res,異常的定義和異常的message,在下面就是迴圈兩個公共屬性的資訊,呼叫bind方法,從item的裡面拿出sourceproperty的屬性和targetproperty繫結,然後給res初始化,設定他的count為source的count,並且判斷如果source長度是0,就直接返回一個空的集合,下面有一個構造迴圈的方法,判斷index是否小於集合的count,如果不成立,直接呼叫break標籤,也就是我們的break關鍵字,如果成立,拿出對應的item,然後呼叫了MemberInit方法,初始化了一個TR,然後呼叫Add方法新增到返回的結果集合中,這樣就實現了一個一個的轉換,最後將所有的表示式整合為一個程式碼塊,透過Block再加入Try Catch,最終編譯成一個Func的委託。下面的單個的原理也是一樣的。

  internal class ExpressionExecute<T, TR> : IExecute<T, TR> where T : class where TR : class, new()
  {
      private List<PropertyInfo> properties;
      public ExpressionExecute()
      {
          properties = typeof(TR).GetProperties().Where(s => typeof(T).GetProperties().Any(a => a.Name == s.Name)).ToList();//獲取相同的屬性名稱
      }
      public Func<List<T>, List<TR>> ExecuteList()
      {            var sourceParam = Expression.Parameter(typeof(List<T>), "source");//定義入參
          var itemVar = Expression.Variable(typeof(T), "item");//定義從集合獲取的item變數
          var resVar = Expression.Variable(typeof(List<TR>), "res");//定義返回的結果變數
          var ex = Expression.Variable(typeof(Exception), "ex");
          var msg = Expression.Property(ex, "Message");

          var method = typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) });
          var memberBindings = new List<MemberBinding>();//memberbind
          var listExpressions = new List<Expression>();//expression

          foreach (var property in properties)
          {
              var sourceProperty = typeof(T).GetProperty(property.Name);
              var targetProperty = typeof(TR).GetProperty(property.Name);

              if (sourceProperty.PropertyType == targetProperty.PropertyType)//判斷型別
              {
                  memberBindings.Add(Expression.Bind(targetProperty, Expression.Property(itemVar, sourceProperty)));//相等直接bind add
              }
          }

          listExpressions.Add(Expression.Assign(resVar, Expression.New(typeof(List<TR>))));//設定res=new list<tr>
          listExpressions.Add(Expression.Assign(Expression.Property(resVar, "Capacity"), Expression.Property(sourceParam, "Count"))); //設定res.count==入參的count

          var indexVar = Expression.Variable(typeof(int), "index");//迴圈索引
          var breakLabel = Expression.Label("LoopBreak");//break標籤

          listExpressions.Add(Expression.IfThen(
              Expression.Equal(Expression.Property(sourceParam, "Count"), Expression.Constant(0)),//如果source.cout==0
              Expression.Return(breakLabel, resVar, typeof(List<TR>)) //直接 break 返回res
          ));
          //構造迴圈
          var innerLoop = CreateLoop(indexVar, sourceParam, itemVar,resVar,memberBindings,breakLabel);
          listExpressions.AddRange(new Expression[] { innerLoop , Expression.Label(breakLabel) , resVar });
          var body = Expression.Block(new[] { itemVar, indexVar, resVar }, listExpressions);//整合一個程式碼塊
          var tryCatch = Expression.TryCatch(body, new CatchBlock[] { Expression.Catch(ex, Expression.Block(Expression.Call(null, method, msg), Expression.New(typeof(List<TR>)))) });//try catch
          var lambda = Expression.Lambda<Func<List<T>, List<TR>>>(tryCatch, sourceParam);
          return lambda.Compile();
      }
      private LoopExpression CreateLoop(ParameterExpression indexVar,ParameterExpression sourceParam,ParameterExpression itemVar,ParameterExpression resVar,List<MemberBinding> memberBindings,LabelTarget breakLabel)
      {
          var addMethod = typeof(List<TR>).GetMethod("Add");//結果集的add方法
        return  Expression.Loop(
              Expression.IfThenElse(//如果index <count 進入block
                  Expression.LessThan(indexVar, Expression.Property(sourceParam, "Count")),
                  Expression.Block(
                      Expression.Assign(itemVar, Expression.Property(sourceParam, "Item", indexVar)),//設定item=source【index】
                      Expression.Call(resVar, addMethod, Expression.MemberInit(Expression.New(typeof(TR)), memberBindings)),//呼叫res.add方法
                      Expression.PostIncrementAssign(indexVar)//index=index+1
                  ),
                  Expression.Break(breakLabel)//如果index<count不成立,直接中斷迴圈
              )
          );
      }
      public Func<T, TR> ExecuteSingle()
      {
          var express = Expression.Parameter(typeof(T), "source");
          var memberBindings = new List<MemberBinding>();//memberbing
          var list = new List<Expression>();
          var action = new Func<string, string>(s => {
              return typeof(TR).GetProperty(s).Name;
          });//根據屬性名稱獲取屬性,由於property第二個引數必須 string or method,下方就只有他透過call的方式獲取屬性的name名稱
          int ipropertydx = 0;//相同屬性遍歷所以
          memberBindings.Clear();

          var peoples = Expression.New(typeof(TR));//建立新的tr例項
          foreach (var item in properties)//屬性遍歷
          {
              var property = Expression.Property(peoples, item.Name);//獲取tr例項屬性
              var memberInfo = typeof(TR).GetMember(item.Name).FirstOrDefault(); // 獲取 MemberInfo 物件
              if (memberInfo != null)
              {
                  var assignment = Expression.Bind(//將屬性和source[index].屬性關聯起來
                      memberInfo, Expression.Property(      //獲取source的屬性
                          express,
                              //呼叫上面的委託返回要拿的屬性名稱也就是A.name=B.name
                              Expression.Lambda<Func<string>>(
                              Expression.Call(Expression.Constant(action.Target), action.Method, //呼叫action拿名稱
                              Expression.Property(
                                  Expression.ArrayIndex(Expression.Constant(properties.ToArray()), Expression.Constant(ipropertydx)), "Name")//獲取properties的第ipropertydx的name名稱
                              )
                              ).Compile()  //編譯為func《string》委託
                              ()
                              ));
                  memberBindings.Add(assignment);//將每個people的每個屬性賦值的assignment新增進去
                  ipropertydx++;
              }
          }
          var memberInit = Expression.MemberInit(peoples, memberBindings);//將peoples初始化  初始化繫結的每一個成員
          var func = Expression.Lambda<Func<T, TR>>(memberInit, express).Compile();//編譯為委託
          return func;
      }
  }

反射

    反正,反射是很耗時的,少量情況還好,大量並不建議使用,雖然很好用,這裡我也只是做一個例子,讓我自己用,肯定優選前面兩個,這個程式碼更不用講了,懂得都懂,

  internal class ReflectionExecute<T, TR> : IExecute<T, TR> where T : class where TR : class,new()
  {
      public Func<List<T>, List<TR>> ExecuteList()
      {
          var res = new Func<List<T>, List<TR>>(s =>
          {
              var resList = new List<TR>();
              var properties = typeof(T).GetProperties().Where(s => typeof(TR).GetProperties().Any(a => a.Name == s.Name)).ToList();//獲取相同的屬性名稱
              foreach (var item in s)
              {
                  var tr = new TR();
                  foreach (var itemproperty in properties)
                  {
                      var val = itemproperty.GetValue(item, null);
                      if (val != null)
                      {
                          var setProperty = typeof(TR).GetProperty(itemproperty.Name);
                          setProperty.SetValue(tr, val);
                      }
                  }
                  resList.Add(tr);
              }
              return resList;
          });
          return res;
      }

      public Func<T, TR> ExecuteSingle()
      {
          var res = new Func<T, TR>(s =>
          {

              var properties = typeof(TR).GetProperties().Where(s => typeof(T).GetProperties().Any(a => a.Name == s.Name)).ToList();//獲取相同的屬性名稱
              var tr = new TR();
              foreach (var itemproperty in properties)
              {
                  var val = itemproperty.GetValue(s, new object[] { });
                  if (val != null)
                  {
                      var setProperty = typeof(TR).GetProperty(itemproperty.Name);
                      setProperty.SetValue(tr, val);
                  }
              }
              return tr;
          });
          return res;
      }
  }

測試

    這玩意,我自己簡單測試了一下,本想用benmark簡單跑一下,但是麻煩,就程式碼自己測試了一下,在第一次構建表示式樹的方法,會有些許耗時,但是在最後如果有快取方法,那效能不必Emit差,總之,效能方面優選Emit和表示式樹,反射就不做考慮。

總結

    趕鴨子上架,水了一篇部落格,如有疑問,可以隨時呼我,QQ934550201.

    程式碼地址:

    連結:https://pan.baidu.com/s/1vW9LPfYHmvk6Y08qEYA9sw
    提取碼:vj34

    

 

相關文章