C#高階程式設計六十六天----表示式樹總結

風靈使發表於2018-07-08

表示式樹總結

基礎

表示式樹提供了一個將可執行程式碼轉換成資料的方法.如果你要在執行程式碼之前修改或轉換此程式碼,那麼它是很有用的.有其是當你要將C#程式碼—-如LINQ查詢表示式轉換成其他程式碼在另一個程式—-如SQL資料庫裡操作它.

表示式樹的語法:

考慮下面簡單的Lambda表示式:

Func<int,int,int>function=(a,b)=>a+b;

這個語法包含三個部分:

  1. 一個宣告 : Func<int,int,int>function

  2. 一個等號 : =

  3. 一個Lambda表示式 : (a,b)=>a+b

變數function指向兩個數字相加的原聲可執行程式碼.上面散的Lambda表示式表示一個簡短的如下的手寫方法:

public int function(int a,int b)
{
return a+b;
}

上面的方法或lambda表示式都可以這樣呼叫:

int c=function(3,4);

當上面的方法呼叫後,變數c將被設成3+4,變成7

上面宣告中第一步委託型別Func是在System名稱空間中為我們定義好的:

public delegate TResult FUnc<T1,T2,TResult>(T1 arg1,T2 arg2);

這個程式碼看上去很複雜,但他在這裡只是用來幫助我們定義變數function,變數function賦值為非常簡單的兩個數字相加的Lambda表示式.及時你不懂委託和泛型,仍然應該清楚這是一個宣告可執行程式碼變數引用的方法.在這個例子裡它指向一個非常簡單的可執行程式碼.

將程式碼轉換到資料中

我們已經看到怎麼宣告一個指向原聲可執行程式碼的變數.表示式樹不是可執行程式碼,他是一種資料結構,那麼我們怎從表示式的原聲帶嗎轉換成表示式樹?怎麼從程式碼轉換成資料?

LINQ提供一個簡單的用法將程式碼轉換到名叫表示式樹的資料結構.首先新增名稱空間:Linq.Expression

現在我們建立一個表示式樹

Expression<Func<int,int,int>>expression=(a,b)=>a+b;

和上例一樣的Lambda表示式用來轉換到型別為Expression<T>的表示式樹.標示expression不是可執行程式碼了他還是一個名叫表示式樹的資料結構.

Visual Studio 2008的samples包含一個叫ExpressionTreeVisualizer的程式。它可以用來呈現表示式樹。圖1你可以看到一個展示上面簡單表示式語句的對話方塊截圖。注意,對話方塊上面部分顯示的是lambda表示式,下面是用TreeView控制元件顯示的其組成部分
這裡寫圖片描述

編寫程式碼來探索表示式樹

我們的例子是一個Expression<TDelegate>.Exoression<TDelegate>類有四個屬性:

Body:得到表示式的主體

Parameters:得到lambda 表示式的引數

NodeType:獲取樹的節點的ExpressionType.共45種不同的值,包含所有表示式節點各種可能的型別,例如返貨常量,例如返回引數,例如取兩個值的小值(<),例如取兩個值的大值(>),例如將值相加(+),等等.

Type:獲取表示式的一個靜態型別.在這個例子裡,表示式的型別是Func<int,int,int>.

如果我們摺疊圖1的節點,Expression<TDelegate>的四個屬性便顯示的很清楚:
這裡寫圖片描述

圖2:將樹節點摺疊起來,你可以很容易的看到Expression<TDelegate>類的四個主要屬性.

你可以使用這四個屬性開始探索表示式樹.例如,你可以通過這樣找到引數的名稱:

Console.WriteLine(“引數1 : {0} , 引數2 : {1}”,expression.Parameters[0],expression.Parameters[1]);

這句程式碼取出值a和b:

引數1: a ,引數2: b

這個很容易在圖1的ParameterExpression節點找到

讓我們在接下來的程式碼探索表示式的Body,在這個例子裡是(a+b):

BinaryExpression body = (BinaryExpression)expression.Body;

ParameterExpression left = (ParameterExpression)body.Left;

ParameterExpression right = (ParameterExpression)body.Right;

Console.WriteLine(expression.Body);

Console.WriteLine(" 表示式左邊部分: " + "{0}{4} 節點型別: {1}{4} 表示式右邊部分: {2}{4} 型別: {3}{4}", left.Name, body.NodeType, right.Name, body.Type, Environment.NewLine);

這段程式碼產生如下輸入:

(a + b)
  表示式左邊部分: a
  節點型別:  Add
  表示式右邊部分: b
  型別: System.Int32

同樣,你會發現很容易在圖1的Body節點中找到這些資訊。

通過探索表示式樹,我們可以分析表示式的各個部分發現它的組成。你可以看見,我們的表示式的所有元素都展示為像節點這樣的資料結構。表示式樹是程式碼轉換成的資料。

編譯一個表示式:將資料轉換換程式碼

如果我們可以將程式碼轉換到資料,那麼我們也應該能將資料轉換會程式碼.這裡是讓編譯器將表示式樹轉換到可執行程式碼的簡單程式碼.

int result=expression.Compile()(3,5);

Console.WriteLine(result);

這段程式碼會輸出值8,跟本文最初生命的Lambda函式的執行結果一樣.

IQueryable<T>和表示式

現在至少你有一個抽象的概念理解表示式樹,現在是時候回來理解其在LINQ中的關鍵作用了,有其是在LINQ to SQL中.花點時間考慮這個標準的LINQ to SQL查詢表示式:

var query = from c in db.Customers 

            where c.City == "Nantes" 

            select new { c.City, c.CompanyName };

你可能知道,這裡LINQ表示式返回的變數queryIQuerable型別,這裡是IQueryable型別的定義:

public interface IQueryable:IEnumerable
{
Type ElementType{get;}

Expression Expression{get;}

IQueryProvider Provider{get;}
}

可以看到,IQueryable包含一個型別為Expression的屬性,ExpressionExpression<T>的基類.IQueryable的例項被設計成擁有一個相關的表示式樹.它是一個等同於查詢表示式中的可執行程式碼的資料結構.

為什麼要將LINQ to SQL查詢表示式轉換成表示式樹呢?

表示式樹是一個用來表示可執行程式碼的資料結構.但到目前為止我們仍然存在一個核心問題,那就是我們為什麼要這麼做?

一個LINQ to SQL查詢不是在你的C#程式裡執行的.相反,他被轉換成SQL,通過網路傳送,最後在資料庫伺服器上執行.換句話說,下面的程式碼實際上從來不會在你的程式裡執行:

var query=from c in db.Customers

where c.City==”BeiJing”

select new {c.City,c.CompantName};

他首先被轉換成下面的SQL語句然後在伺服器上執行:

SELECT [t0].[City], [t0].[CompanyName]

FROM [dbo].[Customers] AS [t0] 

WHERE [t0].[City] = @p0

從查詢表示式的程式碼轉換成SQL查詢語句—-他可以通過字串形式被髮送到其他程式.在這裡,這個程式恰好是SQL Server資料庫.像這樣將資料結構轉換到SQL顯然比直接從原聲IL或可執行程式碼轉換到SQL要容易的多.這有些誇大問題的難度,只要事項轉換0和1的序列到SQL.

現在是時候將你的查詢表示式轉換成SQL,描述查詢的表示式樹是分解並解析了的,就像分解簡單的Lambda表示式樹一樣.當然,解析LINQ to SQL表示式樹的演算法很複雜,但規則是一樣的,一旦瞭解了表示式樹的各部分,那麼LINQ開始斟酌以最好的方式生成返回被請求的資料的SQL語句.

表示式樹被建立是為了製造一個像查詢表示式轉換成字串以傳遞給其他程式並在那裡執行這樣的轉換任務,就是這麼簡單.沒有什麼特別的.只是簡單的:把程式碼,轉換成資料,然後分析資料發現其組成部分,最後轉換成可以傳遞到其他程式的字串.

於查詢來自編譯器封裝的抽象的資料結構,編譯器可以獲取任何它想要的資訊。它不要求執行查詢要在特定的順序,或用特定的方式。相反,它可以分析表示式樹,尋找你要做的是什麼,然後再決定怎麼去做。至少在理論上,我們可以自由的考慮各種因素,比如網路狀況,資料庫負載,結果集是否有效,等等。在實際中LINQ to SQL不考慮所有這些因素,但它理論上可以自由的做幾乎所有想做的事。此外,人們可以通過表示式樹將自己編寫的程式碼,分析並轉換成跟LINQ to SQL提供的完全不同的東西。

IQueryable<T>IEnumerable<T>

正如你知道的,LINQ to Objects的查詢表示式返回IEnumerable<T>而不是IQueryable<T>.為什麼LINQ to Objects使用IEnumerable<T>LINQ to SQL使用IQueryable<T>?

這裡是IEnumerable<T>的定義:

public interface IEnumerable<T> : IEnumerable 
{ 

   IEnumerator<T> GetEnumerator();

}

正如你看到的,IEnumerable<T>並不包含型別為Expression的屬性。這指出LINQ to ObjectsLINQ to SQL的根本區別。後者大量使用了表示式樹,但LINQ to Objects很少使用。

為什麼表示式樹不是LINQ to Objects的標準部分?雖然答案不一定會馬上出現,但這是很有意義的一旦你發現這個問題。

考慮這個簡單LINQ to Objects查詢表示式:

List<int> list = new List<int>() { 1, 2, 3 };

var query = from number in list

            where number < 3 

            select number;

這個LINQ查詢返回在我們的list中比3小的數字;就是說,這裡返回數字1和2。顯然沒有必要將查詢轉換成字串來順序傳遞給其他程式並獲取正確的結果。相反,可以直接轉換查詢表示式為可執行的.NET程式碼。這裡並不需要將它轉換成字串或對它執行任何其他複雜操作。

可是這有點理論化,在實際中某些特殊情況下其分隔線可能有些模糊,總體上講規則相當簡單:

如果程式碼可以在程式裡執行那麼可以使用名為IEnumerable<T>的簡單型別完成任務

如果你需要將查詢表示式轉換成將傳遞到其他程式的字串,那麼應該使用IQueryable<T>和表示式樹。

LINQ to Amazon這樣的專案需要將查詢表示式轉換成web service呼叫執行外部程式,通常使用IQueryable<T>和表示式樹。LINQ to Amazon將它的查詢表示式轉換成資料,通過web service傳遞給另一個甚至不是C#寫的程式。將C#程式碼轉換成到某些能傳遞到web service的東西時,表示式樹內在的抽象是非常有用的。要在程式內執行的程式碼,仍然可以經常使用而拋開表示式樹。例如下面的查詢使用IEnumerable<T>,因為它呼叫到當前程式的.NET反射API

var query = from method in typeof(System.Linq.Enumerable).GetMethods()

            orderby method.Name

            group method by method.Name into g

            select new { Name = g.Key, Overloads = g.Count() };

概要:

本文覆蓋了表示式樹的一些基本情況。通過將程式碼轉換成資料,這些資料結構揭示並描繪表示式的組成部分。從最小的概念上講,理解表示式樹是相當簡單的。它獲取可執行表示式並獲取其組成部分放入樹形資料結構。例如,我們檢測這個簡單的

表示式:

(a,b) => a + b;

通過研究來源於這個表示式的樹,你能看到建立樹的基本規則,見圖1。

你同樣可以看到表示式樹在LINQ to SQL裡扮演非常重要的角色。尤其,他們是LINQ to SQL查詢表示式用來獲取邏輯的資料抽象。解析並分析此資料得到SQL語句,然後傳送到伺服器。

LINQ使查詢C#語言的一個普通類即有型別檢查也有智慧感知。其程式碼是型別檢查和智慧感知的,必須使用正確的C#語法,它能直接轉換到可執行程式碼,就像任何其他C#程式碼一樣被轉換和執行。表示式樹使將可執行程式碼轉換成能傳遞到伺服器的SQL語句相對容易。

查詢返回IEnumerable<T>優於IQueryable<T>表示不使用表示式樹。作為一般性規則,可以這麼說:LINQ查詢在程式內執行時不需要表示式樹,當程式碼在程式外執行時可以利用表示式樹。

擴充套件:Expression Tree Visualizer是一個整合子啊VS中的工具,用於在執行時以樹狀結構顯示出指定的Expression

使用方法:

下載安裝包:http://www.fishlee.net/service/download/589

解壓後根據你的VS版本選擇合適目錄下的 ExpressionTreeVisualizer.dll 檔案,複製到你的VS安裝目錄下的Common7\Packages\Debugger\Visualizers 目錄裡

重啟VS

注意,只有在Debug的時候才能看到,Expression表示式樹的例項

表示式樹表示樹狀資料結構的程式碼,樹狀結構中的每個節點都是一個表示式,例如一個方法呼叫或類似x<y的二元運算

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;

namespace 表示式樹
{
    class Program
    {
        static void Main(string[] args)
        {
            //利用Lambda表示式建立表示式樹
            Expression<Func<int, int, int, int>> expr = (x, y, z) => (x + y) / z;

            //編譯表示式樹,該方法將表示式樹表示的程式碼編譯成一個可執行委託
            int res = expr.Compile()(1, 2, 3);

            Console.WriteLine("利用Lambda表示式建立表示式樹 : " + res);

            //使用LambdaExpression構建可執行的程式碼
            Func<int, int, int, int> fun = (x, y, z) => (x + y) / z;
            Console.WriteLine("使用LambdaExpression構建可執行的程式碼 : " + fun(1, 2, 3));

            //動態構建表示式樹
            ParameterExpression pe1 = Expression.Parameter(typeof(int), "x");
            ParameterExpression pe2 = Expression.Parameter(typeof(int), "y");
            ParameterExpression pe3 = Expression.Parameter(typeof(int), "z");
            var body = Expression.Divide(Expression.Add(pe1, pe2), pe3);
            var w = Expression.Lambda<Func<int, int, int, int>>(body, new ParameterExpression[]
                {
                    pe1,pe2,pe3
                });


            Console.WriteLine("動態構建表示式樹 ; " + w.Compile()(1, 2, 3));


            List<Entity> list = new List<Entity> { new Entity { Id1 = 1 }, new Entity { Id1 = 2 }, new Entity { Id1 = 3 } };
            //IQueryable<T>的擴充套件方法,WhereIn的實現
            var d = list.AsQueryable().WhereIn(o => o.Id1, new int[] { 1, 2 });
            d.ToList().ForEach(o =>
            {
                Console.WriteLine(o.Id1);
            });
            Console.ReadKey();
        }
    }
    public class Entity
    {
        public Object Id;
        public int Id1;
        public string Name { set; get; }


    }
    public static class CC
    {
        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);
        }
    }
}

相關文章