C#中的表示式樹

風靈使發表於2018-07-07

本人之前從未接觸過表示式樹的概念,所以特意從網上找到兩篇這方面的資料學習了下。本文為閱讀筆記性質部落格!

表示式樹是.NET 3.5之後引入的,它是一個強大靈活的工具(比如用在LINQ中構造動態查詢)。

先來看看Expression類的API介面:

using System.Collections.ObjectModel;

namespace System.Linq.Expressions
{
    // Summary:
    //     Represents a strongly typed lambda expression as a data structure in the
    //     form of an expression tree. This class cannot be inherited.
    //
    // Type parameters:
    //   TDelegate:
    //     The type of the delegate that the System.Linq.Expressions.Expression<tdelegate>
    //     represents.
    public sealed class Expression<tdelegate> : LambdaExpression
    {
        // Summary:
        //     Compiles the lambda expression described by the expression tree into executable
        //     code.
        //
        // Returns:
        //     A delegate of type TDelegate that represents the lambda expression described
        //     by the System.Linq.Expressions.Expression<tdelegate>.
        public TDelegate Compile();
    }
}

表示式樹的語法如下:

Expression<Func<type,returnType>> = (param) => lamdaexpresion;

我們先來看一個簡單例子:

Expression<Func<int, int, int>> expr = (x, y) => x+y;

這就是一個表示式樹了。使用Expression Tree Visualizer工具(直接除錯模式下看也可以,只不過沒這個直觀)在除錯模式下檢視這個表示式樹(就是一個物件),如下:

exp_tree

可以看到表示式樹主要由下面四部分組成:

1、Body 主體部分
2、Parameters 引數部分
3、NodeType 節點型別
4、Lambda表示式型別

對於前面舉的例子,主體部分即x+y,引數部分即(x,y)Lambda表示式型別是Func<Int32, Int32, Int32>。注意主體部分可以是表示式,但是不能包含語句,如下這樣:

Expression<Func<int, int, int>> expr = (x, y) => { return x+y; };

會報編譯錯誤“Lambada expression with state body cannot be converted to expression tree”:即帶有語句的Lambda表示式不能轉換成表示式樹。

用前面的方法雖然可以建立表示式樹,但是不夠靈活,如果要靈活構建表示式樹,可以像下面這樣:

ParameterExpression exp1 = Expression.Parameter(typeof(int), "a");
ParameterExpression exp2 = Expression.Parameter(typeof(int), "b");

BinaryExpression exp = Expression.Multiply(exp1,exp2);
var lamExp = Expression.Lambda<Func<int, int, int>>(exp, new ParameterExpression[] { exp1, exp2 });

exp1、exp2即表示式樹的引數,exp是表示式樹的主體。如果我利用Reflector反編譯Expression<Func<int, int, int>> expr = (x, y) => { return x+y; };得到下面的C#程式碼:

ParameterExpression CS$0$0000;
ParameterExpression CS$0$0001;
Expression<Func<int, int, int>> expr = Expression.Lambda<Func<int, int, int>>(Expression.Multiply(CS$0$0000 = Expression.Parameter(typeof(int), "x"), CS$0$0001 = Expression.Parameter(typeof(int), "y")), new ParameterExpression[] { CS$0$0000, CS$0$0001 });

可以看到它基本和上面的手動構建程式碼一致。再來看一個簡單的例子:

Expression<Func<Customer, bool>> filter =
    cust => Equal(Property(cust,"Region"),"North");

可以用下面的程式碼手動構建效果等同於上面的表示式樹:


// declare a parameter of type Customer named cust

ParameterExpression custParam = Expression.Parameter(

    typeof(Customer), "custParam");

// compare (equality) the Region property of the

// parameter against the string constant "North"

BinaryExpression body = Expression.Equal(

    Expression.Property(custParam, "Region"),

    Expression.Constant("North", typeof(string)));

// formalise this as a lambda

Expression<Func<Customer, bool>> filter =

    Expression.Lambda<Func<Customer, bool>>(body, cust);

然後我們可以通過表示式樹的Compile方法將表示式樹編譯成Lambda表示式,如下:

Func<Customer, bool> filterFunc = filter.Compile();

但是Compile呼叫過程涉及動態程式碼生成,所以出於效能考慮最好只呼叫一次,然後快取起來。或者像下面這樣在靜態構造塊中使用(也只會呼叫一次):


public static class Operator<T>
{
    private static readonly Func<T, T, T> add;
    public static T Add(T x, T y)
    {
        return add(x, y);
    }
    static Operator()
    {
        var x = Expression.Parameter(typeof(T), "x");
        var y = Expression.Parameter(typeof(T), "y");
        var body = Expression.Add(x, y);
        add = Expression.Lambda<Func<T, T, T>>(
            body, x, y).Compile();
    }
}

Expression類包含下面幾類靜態方法(.NET 3.5中):

Arithmetic: Add, AddChecked, Divide, Modulo, Multiply, MultiplyChecked, Negate, NegateChecked, Power, 
Subtract, SubtractChecked, UnaryPlus

Creation: Bind, ElementInit, ListBind, ListInit, MemberBind, MemberInit, New, NewArrayBounds, NewArrayInit

Bitwise: And, ExclusiveOr, LeftShift (<<), Not, Or, RightShift (>>)

Logical: AndAlso (&&), Condition (? :), Equal, GreaterThan, GreaterThanOrEqual, LessThan, 
LessThanOrEqual, NotEqual, OrElse (||), TypeIs

Member Access: ArrayIndex, ArrayLength, Call, Field, Property, PropertyOrField

Other: Convert, ConvertChecked, Coalesce (??), Constant, Invoke, Lambda, Parameter, TypeAs, Quote

下面我們類似前面過載一個淺拷貝的例子(比使用反射開銷小):


using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ExpressionTreeLab
{
    class Program
    {
        static void Main(string[] args)
        {
            var p = new Person()
                {
                    Name = "jxq",
                    Age = 23
                };
            var shallowCopy = Operator<Person>.ShallowCopy(p);
            shallowCopy.Name = "feichexia";
            Console.WriteLine(shallowCopy.Name);
            Console.WriteLine(p.Name);

            Console.ReadKey();
        }

        public class Person
        {
            public string Name { get; set; }
            public int Age { get; set; }
        }

        public static class Operator<T>
        {
            private static readonly Func<T, T> ShallowClone; 

            public static T ShallowCopy(T sourcObj)
            {
                return ShallowClone.Invoke(sourcObj);
            }

            static Operator()
            {
                var origParam = Expression.Parameter(typeof(T), "orig");

                // for each read/write property on T, create a  new binding 
                // (for the object initializer) that copies the original's value into the new object 
                var setProps = from prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
                                        where prop.CanRead && prop.CanWrite
                                        select (MemberBinding)Expression.Bind(prop, Expression.Property(origParam, prop));

                var body = Expression.MemberInit( // object initializer 
                    Expression.New(typeof(T)), // ctor 
                    setProps // property assignments 
                );

                ShallowClone = Expression.Lambda<Func<T, T>>(body, origParam).Compile();
            }
        }
    }
}

繼續看Expression.AndAlso的使用,它可以用來替代類似下面這種多條件與的情況:

Func<Person, Person, bool> personEqual = (person1, person2) => person1.Name == person2.Name && person1.Age == person2.Age;
 if(personEqual(p1, p2))
{
    Console.WriteLine("兩個物件所有屬性值都相等!");
}

程式碼如下:

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace ExpressionTreeLab
{
    class Program
    {
        static void Main(string[] args)
        {
            var p1 = new Person()
                {
                    Name = "jxq",
                    Age = 23
                };
            var p2 = new Person()
                {
                    Name = "jxq",
                    Age = 23
                };

            if (Operator<Person>.ObjectPropertyEqual(p1, p2))
            {
                Console.WriteLine("兩個物件所有屬性值都相等!");
            }

            Console.ReadKey();
        }

        public class Person
        {
            public string Name { get; set; }
            public int Age { get; set; }
        }

        public static class Operator<T>
        {
            private static readonly Func<T, T, bool> PropsEqual; 

            public static bool ObjectPropertyEqual(T obj1, T obj2)
            {
                return PropsEqual.Invoke(obj1, obj2);
            }

            static Operator()
            {
                var x = Expression.Parameter(typeof(T), "x");
                var y = Expression.Parameter(typeof(T), "y");

                // 獲取型別T上的可讀Property
                var readableProps = from prop in typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance)
                                        where prop.CanRead
                                        select prop;

                Expression combination = null;
                foreach (var readableProp in readableProps)
                {
                    var thisPropEqual = Expression.Equal(Expression.Property(x, readableProp),
                                                         Expression.Property(y, readableProp));

                    if(combination == null)
                    {
                        combination = thisPropEqual;
                    }
                    else
                    {
                        combination = Expression.AndAlso(combination, thisPropEqual);
                    }
                }

                if(combination == null)   // 如果沒有需要比較的東西,直接返回false
                {
                    PropsEqual = (p1, p2) => false;
                }
                else
                {
                    PropsEqual = Expression.Lambda<Func<T, T, bool>>(combination, x, y).Compile();
                }
            }
        }
    }
}

.NET 4.0中擴充套件了一些Expression的靜態方法,使得編寫動態程式碼更容易:

Mutation: AddAssign, AddAssignChecked, AndAssign, Assign, DivideAssign, ExclusiveOrAssign, LeftShiftAssign, ModuloAssign, MultiplyAssign, MultiplyAssignChecked, OrAssign, PostDecrementAssign, PostIncrementAssign, PowerAssign, PreDecrementAssign, PreIncrementAssign, RightShiftAssign, SubtractAssign, SubtractAssignChecked

Arithmetic: Decrement, Default, Increment, OnesComplement

Member Access: ArrayAccess, Dynamic

Logical: ReferenceEqual, ReferenceNotEqual, TypeEqual

Flow: Block, Break, Continue, Empty, Goto, IfThen, IfThenElse, IfFalse, IfTrue, Label, Loop, Return, Switch, SwitchCase, Unbox, Variable

Exceptions: Catch, Rethrow, Throw

Debug: ClearDebugInfo, DebugInfo

下面是一個利用表示式樹編寫動態程式碼的例子(迴圈列印0到9):


using System;
using System.Linq.Expressions;

namespace ExpressionTreeLab
{
    class Program
    {
        static void Main(string[] args)
        {
            var exitFor = Expression.Label("exitFor"); // jump label
            var x = Expression.Variable(typeof(int), "x");
            var body = 
                Expression.Block(
                    new[] { x }, // declare scope variables
                    Expression.Assign(x, Expression.Constant(0, typeof(int))), // init
                    Expression.Loop(
                        Expression.IfThenElse(
                            Expression.GreaterThanOrEqual( // test for exit
                                x,
                                Expression.Constant(10, typeof(int))
                            ),
                            Expression.Break(exitFor), // perform exit
                            Expression.Block( // perform code
                                Expression.Call(
                                    typeof(Console), "WriteLine", null, x),
                                Expression.PostIncrementAssign(x)
                            )
                        ), exitFor
                     )  // Loop ends
                 );

            var runtimeLoop = Expression.Lambda<Action>(body).Compile();
            runtimeLoop();

            Console.Read();
        }

    }
}

另外WhereIn擴充套件實現如下,如果前面的例子都熟悉了的話,這個自然也很容易看懂了:


    /// <summary>
    ///   使之支援Sql in語法
    /// </summary>
    /// <typeparam name = "T"></typeparam>
    /// <typeparam name = "TValue"></typeparam>
    /// <param name = "query"></param>
    /// <param name = "obj"></param>
    /// <param name = "values"></param>
    /// <returns></returns>
    public static IQueryable<T> WhereIn<T, TValue>(this IQueryable<T> query, Expression<Func<T, TValue>> obj, IEnumerable<TValue> values)
    {
        return query.Where(BuildContainsExpression(obj, values));
    }

    private static Expression<Func<TElement, bool>> BuildContainsExpression<TElement, TValue>(
        Expression<Func<TElement, TValue>> valueSelector, IEnumerable<TValue> values)
    {
        if (null == valueSelector)
        {
            throw new ArgumentNullException("valueSelector");
        }
        if (null == values)
        {
            throw new ArgumentNullException("values");
        }
        var p = valueSelector.Parameters.Single();
        if (!values.Any()) return e => false;

        var equals = values.Select(value => (Expression) Expression.Equal(valueSelector.Body, Expression.Constant(value, typeof (TValue))));
        var body = equals.Aggregate(Expression.Or);
        return Expression.Lambda<Func<TElement, bool>>(body, p);
    }

呼叫方式如下:

db.Users.WhereIIn(u => u.Id, new int[] { 1, 2, 3 });

表示式和表示式樹

  1. 表示式是匿名委託,本不是委託,是通過顯示或隱式的轉成委託例項

  2. 表示式樹是一種資料結構,是不可執行程式碼,它需要在c#程式碼中編譯成sql語句,然後再sql庫中執行查詢操作表示式樹種的每個節點本身表示一個表示式

  3. 表示式轉換成表示式樹 Expression<表示式>

  4. 對於IEnumerable(即在記憶體中查詢)要用func<>(表示式(委託)),對於委託而言直接執行即可
    對於Queryable(在庫裡查詢),傳入引數是表示式樹,得先解析翻譯成sql語句,然後在外部庫中執行

  5. 將表示式樹編譯成可執行的委託 表示式樹名.Compile()(引數)
  6. 表示式的屬性:
    (1).body 表示式的主體
    (2)parameters 表示式的引數
    (3)NodeType 表示式的返回引數
    (4)Type 表示式型別 如Func<int,int>
    

相關文章