我們在使用C#編程的時候,經常使用反射來動態呼叫方法,但有時候需要動態的生成方法,下面介紹使用運算式樹的方式來自動生成方法,并呼叫,
首先需要說明什么是運算式,熟悉Linq的程式猿都用過類似于下面的代碼:t=>t.Length<=25;
在C#中=>代表這是一個Lambda運算式,它用來對陣列進行查詢,統計,排序,去重的功能非常有用,而運算式樹就是通過動態的創建一個Lambda的方式來實作相關的功能,
下面是一個類似于JS中apply函式的示例,
使用運算式樹,一定要參考System.Linq.Expressions;其中的Expression類有很多的方法可以定義一個方法所需要的所有東西,
public class CommonTest
{
public object TestMethodCall(int age, string name)
{
Console.WriteLine($"{name}'s Age is {age}");
return true;
}
public object TestExpression(MethodInfo method, object[] parameters, CommonTest instance)
{
//最終生成的運算式樣式(m,p)=>{return (object)m.method(p);}
//定義兩個引數運算式
ParameterExpression mParameter = Expression.Parameter(typeof(CommonTest), "m");//定義一個名稱為m的引數
ParameterExpression pParameter = Expression.Parameter(typeof(object[]), "p");//定義一個名稱為p的引數
ParameterInfo[] tParameter = method.GetParameters();//獲取到方法的所有引數
Expression[] rParameter = new Expression[tParameter.Length];//定義一個與方法引數長度相同的運算式容器,因為在呼叫方法的時候需要使用的是運算式,不是直接使用方法的引數串列
for (int i = 0; i < rParameter.Length; i++)
{
BinaryExpression pExpression = Expression.ArrayIndex(pParameter, Expression.Constant(i));//從方法中獲取到對應索引的引數
UnaryExpression uExpression = Expression.Convert(pExpression, tParameter[i].ParameterType);//將此引數的型別轉化成實際引數的型別
rParameter[i] = uExpression;//將對應的引數運算式添加到引數運算式容器中
}
MethodCallExpression mcExpression = Expression.Call(mParameter,method, rParameter);//呼叫方法,因為是實體方法所以第一個引數必須是m,如果是靜態方法,那么第一個引數就應該是null
UnaryExpression reExpression = Expression.Convert(mcExpression, typeof(object));//將結果轉換成object,因為要動態的呼叫所有的方法,所以回傳值必須是object,如果是無回傳值的方法,則不需要這一步
return Expression.Lambda<Func<CommonTest, object[], object>>(reExpression, mParameter, pParameter).Compile()(instance, parameters);//將方法編譯成一個Func委托,并執行他
}
}
以上的代碼的呼叫方式如下:
CommonTest ct = new CommonTest();
MethodInfo mi = typeof(CommonTest).GetMethod("TestMethodCall");
var r = ct.TestExpression(mi, new object[] { 25, "SC" }, ct);
此方法也是C#MVC中呼叫控制器中的Action的原理代碼,其最大的作用是不管目標Action擁有多少個引數,最后呼叫都只需要一個object[]的引數,避免了直接使用反射呼叫,但是不確定引數個數的困難,
使用Expression不僅可以實習以上的類似于MVC原理的代碼,也可以對運算式樹進行決議,可以實作ORM底層的Sql構成,但此出不再進行詳解,有興趣可以百度查詢運算式樹的決議,
運算式樹實作的缺點是功能實作復雜,除錯困難,建議在實作之前先將需要實作的功能使用C#語法撰寫出來,再按照對應的格式通過運算式樹來實作,這樣相對簡單一些,
下面是使用運算式輸出一個99乘法表,
以下是實作的結果

首先是通過正常的方式來實作,代碼如下:
for (int i = 1; i <= 9; i++)
{
for (int j = 1; j <= i; j++)
{
int total = i * j;
Console.Write($"{i} * {j} = {total}\t");
}
Console.WriteLine();
}
Console.ReadKey();
下面是使用運算式樹實作相同功能的代碼:
/// <summary>
/// 使用運算式樹實作99乘法表
/// </summary>
public void TestMultiple()
{
LabelTarget labOut = Expression.Label();//用于跳出外部回圈的標志
LabelTarget labIn = Expression.Label();//用于跳出內部回圈的標志
ParameterExpression iParameter = Expression.Parameter(typeof(int), "i");//定義外部回圈的變數,類似于int i;
ParameterExpression jParameter = Expression.Parameter(typeof(int), "j");//定義內部回圈的變數,類似于int j;
ParameterExpression rParameter = Expression.Parameter(typeof(int), "result");//定義用于保存i*j的結果的變數
MethodInfo writeString = typeof(Console).GetMethod("Write", BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(string) }, null);//獲取Write方法
MethodInfo writeInt = typeof(Console).GetMethod("Write", BindingFlags.Static | BindingFlags.Public, null, new Type[] { typeof(int) }, null);//獲取Write方法
Expression expResult = Expression.Block(
new[] { iParameter, jParameter, rParameter },
Expression.Assign(iParameter, Expression.Constant(1)),//為i賦初始值,類似于i=1;
Expression.Loop(Expression.Block(//此處開始外部回圈,運算式只能實作while回圈,不能實作for回圈
Expression.IfThenElse(Expression.LessThanOrEqual(iParameter, Expression.Constant(9)),//定義執行的條件,類似于if(i<=9){
//外部if為真的時候執行以下代碼
Expression.Block(
Expression.Assign(jParameter, Expression.Constant(1)),//為j賦初始值,類似于j=1;
Expression.Loop(Expression.Block(//此處開始內部回圈
Expression.IfThenElse(Expression.LessThanOrEqual(jParameter, iParameter),//定義執行的條件,類似于if(j<=i){
//內部if為真的時候執行以下代碼
Expression.Block(
Expression.Assign(rParameter, Expression.Multiply(iParameter, jParameter)),//此處用于計算i*j的結果,并進行賦值,類似于result=i*j
//列印出結果,類似于Console.Write("i * j = " + result + "\t")
Expression.Call(null, writeInt, jParameter),
Expression.Call(null, writeString, Expression.Constant(" * ")),
Expression.Call(null, writeInt, iParameter),
Expression.Call(null, writeString, Expression.Constant(" = ")),
Expression.Call(null, writeInt, rParameter),
Expression.Call(null, writeString, Expression.Constant("\t")),
Expression.PostIncrementAssign(jParameter)//j自增長,類似于j++
),
//內部if為假的時候執行以下代碼
Expression.Break(labIn))//此處跳出內部回圈)
), labIn),
Expression.Block(
Expression.Call(null, writeString, Expression.Constant("\n")),//此處列印換行符,類似于Console.WriteLine();
Expression.PostIncrementAssign(iParameter))//i自增長,類似于i++
)
//外部if為假的時候執行以下代碼
, Expression.Break(labOut))//此處跳出外部回圈
), labOut));
Expression.Lambda<Action>(expResult).Compile()();
}
以上兩段代碼實作的效果相同,可以看出運算式樹實作相同的功能的復雜程度遠遠超出普通的方式,正常10行的代碼,運算式樹整整用了42行代碼才實作,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/122543.html
標籤:C#
