標籤:
剛接觸LINQ的人往往覺得運算式樹狀架構很不容易理解。通過這篇文章我希望大家看到它其實並不像想象中那麼難。您只要有普通的LINQ知識便可以輕鬆理解本文。
運算式樹狀架構提供一個將可執行代碼轉換成資料的方法。如果你要在執行代碼之前修改或轉換此代碼,那麼它是非常有價值的。尤其是當你要將C#代碼----如LINQ查詢運算式轉換成其他代碼在另一個程式----如SQL資料庫裡操作它。
但是我在這裡顛倒順序,在文章最後你很容易發現為什麼將代碼轉換到資料中去很有用。首先我需要提供一點背景知識。讓我們開始看看相關的建立運算式樹狀架構的簡單文法。
運算式樹狀架構的文法
考慮下面簡單的Lambda運算式:
Func<int, int, int> function = (a,b) => a + b;這個語句包含三個部分:
- 一個聲明: Func<int, int, int> function
- 一個等號: =
- 一個lambda運算式: (a,b) => a + b;
變數function指向兩個數字相加的原生可執行代碼。上面三步的lambda運算式表示一個簡短的如下的手寫方法:
public int function(int a, int b){ return a + b;}
上面的方法或lambda運算式都可以這樣調用:
int c = function(3, 5);
當方法調用後,變數c將被設成3+5,即8。
上面聲明中第一步委託類型Func是在System命名空間中為我們定義好的:
public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
這個代碼看上去很複雜,但它在這裡只是用來幫我們定義變數function,變數function賦值為非常簡單的兩個數字相加的lambda運算式。即使你不懂委託和泛型,你仍然應該清楚這是一個聲明可執行代碼變數引用的方法。在這個例子裡它指向一個非常簡單的可執行代碼。
將代碼轉換到資料中
在上一節,你看到怎麼聲明一個指向原生可執行代碼的變數。運算式樹狀架構不是可執行代碼,它是一種資料結構。那麼我們怎麼從運算式的原生代碼轉換成運算式樹狀架構?怎麼從代碼轉換成資料?
LINQ提供一個簡單文法用來將代碼轉換到名叫運算式樹狀架構的資料結構。首先添加using語句引入Linq.Expressions命名空間:
using System.Linq.Expressions;
現在我們可以建立一個運算式樹狀架構:
Expression<Func<int, int, int>> expression = (a,b) => a + b;
跟上個例子一樣的lambda運算式用來轉換到類型為Expression<T>的運算式樹狀架構。標識expression不是可執行代碼;它是一個名叫運算式樹狀架構的資料結構。
Visual Studio 2008的samples包含一個叫ExpressionTreeVisualizer的程式。它可以用來呈現運算式樹狀架構。圖1你可以看到一個展示上面簡單運算式語句的對話方塊。注意,對話方塊上面部分顯示的是lambda運算式,下面是用TreeView控制項顯示的其組成部分。
圖1:VS2008 C# Samples中的ExpressionTreeVisualizer建立一個運算式樹狀架構的象徵性的輸出
編寫代碼來探索運算式樹狀架構
我們的例子是一個Expression<TDelegate>。Expression<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運算式返回的變數query是IQueryable類型。這裡是IQueryable類型的定義:
public interface IQueryable : IEnumerable { Type ElementType { get; } Expression Expression { get; } IQueryProvider Provider { get; }}
你可以看見,IQueryable包含一個類型為Expression的屬性,Expression是Expression<T>的基類。IQueryable的執行個體被設計成擁有一個相關的運算式樹狀架構。它是一個等同於查詢運算式中的可執行代碼的資料結構。
花點時間考慮圖3。你可能需要點擊它使圖片原尺寸顯示。這是本節開始的查詢運算式的運算式樹狀架構的可視化顯示。此圖使用ExpressionTreeVisualizer建立,就像我使用它在圖1建立基礎的lambda運算式樹狀架構一樣。
圖3:此複雜的運算式樹狀架構由上面的範例LINQ to SQL查詢運算式產生。(點擊圖片看查看大圖)
為什麼要將LINQ to SQL查詢運算式轉換成運算式樹狀架構呢?
你已經學習了運算式樹狀架構是一個用來表示可執行代碼的資料結構。但到目前為止我們還沒有回答一個核心問題,那就是為什麼我們要做這樣的轉換。這個問題是我們在本文開始時提出來的,現在是時候回答了。
一個LINQ to SQL查詢不是在你的C#程式裡執行的。相反,它被轉換成SQL,通過網路發送,最後在資料庫伺服器上執行。換句話說,下面的代碼實際上從來不會在你的程式裡執行:
var query = from c in db.Customers
where c.City == "Nantes"
select new { c.City, c.CompanyName };
它首先被轉換成下面的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 Objects和LINQ 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查詢在程式內執行時不需要運算式樹狀架構,當代碼在程式外執行時可以利用運算式樹狀架構。
註:本文轉自http://www.cnblogs.com/tianfan/
運算式樹狀架構-理解與進階