.NET 中一項突破性的創新是 LINQ(Language Integrated Query,語言集成查詢),這組語言擴展讓你能夠不必離開舒適的 C# 語言執行查詢。
LINQ 定義了用於構建查詢表達式的關鍵字。這些查詢表達式能夠對數據進行選擇、過濾、排序、分組和轉換。借助各種 LINQ 擴展,你可以對不同的數據源使用相同的查詢表達式。
雖然你可以在任意地方使用 LINQ ,但是只有 ASP.NET 應用程序中最可能把 LINQ 用作數據庫組件的一部分。你可以和 ADO.NET 數據訪問代碼一起使用 LINQ ,或者借助 LINQ to Entities 取代 ADO.NET 數據訪問代碼。
LINQ 基礎
接近 LINQ 最簡單的方法時了解它是如何針對內存集合工作的,這就是 LINQ to Objects,最簡單形式的 LINQ。
就本質而言,LINQ to Objects 能夠使用聲明性的 LINQ 表達式代替邏輯(如 foreach 塊):
EmployeeDB db = new EmployeeDB();
protected void btnForeach_Click(object sender, EventArgs e)
{
List<EmployeeDetails> employees = db.GetEmployees();
List<EmployeeDetails> matches = new List<EmployeeDetails>();
foreach (EmployeeDetails employee in employees)
{
if (employee.LastName.StartsWith("D"))
{
matches.Add(employee);
}
}
gridEmployees.DataSource = matches;
gridEmployees.DataBind();
}
protected void btnLINQ_Click(object sender, EventArgs e)
{
List<EmployeeDetails> employees = db.GetEmployees();
IEnumerable<EmployeeDetails> matches;
matches = from employee in employees
where employee.LastName.StartsWith("D")
select employee;
gridEmployees.DataSource = matches;
gridEmployees.DataBind();
}
延遲執行
使用 foreach 塊的代碼和使用 LINQ 表達式的代碼的一個明顯的區別在於它們處理匹配集合類型方式。對於 foreach ,匹配的集合被創建為一個特定類型的集合(強類型 List<T>),在 LINQ 的示例中,匹配的集合僅通過它所實現的 IEnumerable<T> 接口暴露。
產生這種差別的原因在於 LINQ 使用了延遲執行。可能和你預期的不同,匹配的對象並不是一個包含了匹配的 EmployeeDetails 對象的直觀集合,而是一個特殊的 LINQ 對象,能夠在你需要的時候抓取數據。
根據查詢表達式的不同,LINQ 表達式可以返回不同的對象,例如:WhereListIterator<T>、UnionIterator<T>、SelectIterator<T> 等,因為通過 IEnumerable<T> 接口和結果交互,因此不必要知道代碼所使用的具體迭代類。
LINQ 是如何工作的
- 要使用 LINQ ,需要創建一個 LINQ 表達式
- LINQ 表達式的返回值是一個實現了 IEnumerable<T> 的迭代器對象
- 對迭代器對象進行枚舉時,LINQ 執行它的工作
問:LINQ 是如何執行表達式的?為了產生過濾結果,它究竟做了什么?
答:依據你所查詢的數據類型的不同而不同。LINQ to Entities 把 LINQ 表達式轉換為數據庫命令,所以 LINQ to Entities 需要打開一個數據庫連接並執行一次數據庫查詢以獲得你所請求的數據。如果是前一個示例中使用的是 LINQ to Objects,LINQ 執行的過程就簡單多了,實際上此時 LINQ 只是使用了一個 foreach 循環從頭到尾遍歷集合。
LINQ 表達式
雖然重新調整了子句的順序,但 LINQ 表達式和 SQL 查詢表面上還是很相似。
所有 LINQ 表達式都必須有一個指定數據源的 from 子句並有一個表示要獲取的數據的 select 子句(或者一個定義了數據要放入組的 group 子句)。
from 子句要放在最前面,from 子句確定了兩部分信息。緊隨 in 之后的單詞表明了數據源,緊隨 from 之后的單詞為數據源中的每個個體提供一個假名:
matches = from employee in employees
下面是一個簡單的 LINQ 查詢,從 employees 集合中獲取所有數據:
matches = from employee in employees
select employee;
提示:
可以在微軟的 101 LINQ 示例中(http://code.msdn.microsoft.com/101-LINQ-Samples-3fb9811b)找到各種表達式的示例。
1. 投影
可以修改 select 子句獲取一組數據。
// Sample 1
IEnumerable<string> matches;
matches = from employee in employees
select employee.FirstName;
// Sample 2
IEnumerable<string> matches;
matches = from employee in employees
select employee.FirstName + employee.LastName ;
當你選中信息時,可以對數值數據或字符串數據使用標准的 C# 操作符對齊進行修改。更有意思的是,可以動態定義一個新類以封裝返回的信息。C# 匿名類可以做到這一點,技巧是向 select 子句中添加一個 new 關鍵字並把選擇的內容以對象的形式賦給屬性:
var matches = from employee in employees
select new { First = employee.FirstName, Last = employee.LastName };
這個表達式在執行的時候返回一組隱式創建的類的對象。你不會看到類的定義並且不能把實例傳給方法調用,因為它由編譯器生成,並且具有自動創建的無意義的名稱。不過,你可以在本地使用該類,訪問 First 和 Last 屬性,甚至結合數據綁定使用它(此時 ASP.NET 使用反射根據屬性名稱獲取對應的值)。
把正在查詢的數據轉換為各種結構的能力被稱為投影。
引用獨立的對象也要使用關鍵字 var ,比如對前面的結果進行迭代:
foreach (var employee in matches)
{
// 可以讀取 First 和 Last
}
當然了,執行投影的時候,並非只能使用匿名類。你可以正式定義類型,然后在表達式中使用它:
public class EmployeeName
{
public string FirstName { get; set; }
public string LastName { get; set; }
public EmployeeName(string firstName, string lastName)
{
this.FirstName = firstName;
this.LastName = lastName;
}
}
IEnumerable<EmployeeName> matches = from employee in employees
select new EmployeeName { FirstName = employee.FirstName,
LastName = employee.LastName };
上述的表達式之所以能夠工作,是因為 FirstName 和 LastName 屬性可以被公共訪問且不是只讀的。創建 EmployeeName 對象后 LINQ 設置這些屬性。另外,你也可以在 EmployeeName 類名稱后的括號里為參數化的構造函數提供其他的參數:
IEnumerable<EmployeeName> matches = from employee in employees
select new EmployeeName(employee.FirstName, employee.LastName);
2. 過濾和排序
IEnumerable<EmployeeDetails> matches;
matches = from employee in employees
where employee.LastName.StartsWith("D")
select employee;
where 子句接受一個條件表達式,它針對每個項目進行計算。如果結果為 true ,該項目就被包含到結果中。不過,LINQ 使用相同的延遲執行模型,也就是說,直到對結果集進行迭代時才會對 where 子句進行計算。
你或許已經猜到,能夠用邏輯與(&&)以及邏輯或(||)操作符組合多個條件表達式並且能夠使用關系操作符(如 <、<=、>、>=):
IEnumerable<Product> matches;
matches = from product in products
where product.UnitsInStock > 0 && product.UnitPrice > 3.00
select product;
LINQ 表達式一個有意思的特性是讓你能夠隨時調用自己的方法。例如,可以創建一個檢查員工的函數 TestEmployee(),根據它是否在結果集中返回 true 或 false:
private bool TestEmployee(EmployeeDetails employee)
{
return employee.LastName.StartsWith("D");
}
然后,可以這樣使用:
IEnumerable<EmployeeDetails> matches;
matches = from employee in employees
where TestEmployee(employee)
select employee;
orderby 操作符同樣很直觀。它的模型基於 SQL 里的查詢語句。你只要為排序提供一個或多個以逗號分隔的值列表(最后可加 decending 降序):
IEnumerable<EmployeeDetails> matches;
matches = from employee in employees
orderby employee.LastName,employee.FirstName
select employee;
注解:
所有實現了 IComparable 的類型都支持排序,它是 .NET 最核心的數據類型之一(如 數值、日期、字符串)。你也可以傳遞一個自定義的 IComparable 對象對數據進行排序。
3. 分組和聚合
分組讓你把大量的信息濃縮為精簡的概要。
分組是一種投影,因為結果集中的對象和數據源集合中的對象是不一樣的。例如,假設你正在處理一組 Product 對象,並決定把它們放到特定價格的組中。最終的結果是分組對象的 IEnumerable<T> 集合,其中每個對象代表某個價格區間內的特定產品。每個組實現 System.Linq 命名空間的 IGrouping<T,K> 接口。
使用分組,首先需要做兩個決定:
- 創建分組的條件
- 每個組顯示什么信息
第一個任務比較簡單。使用 group、by 以及 into 關鍵字選擇要分組的對象,確定如何分組並決定引用每個分組時要使用的假名:
var matches = from employee in employees
group employee by employee.TitleOfCourtesy into g
...
// 在 LINQ 中使用 g 作為分組假名是一個常見的約定
具有相同數據的對象被放到了同一個組里。要把數據按數值范圍進行分組,需要編寫一段計算代碼以便為每個組產生相同的數值。例如,要把產品按每個價格區間是 50 元 進行分組:
var matches = from product in products
group product by (int)(product.UnitPrice / 50) into g
...
現在,所有幾個低於 50 元的產品的分組鍵為 0,而價格在 50 - 100 之間的產品的分組鍵為 1,以此類推。
得到分組后,還要確定分組的結果返回什么樣的信息。每個組以實現了 IGrouping<T,K> 接口的對象的形式暴露給代碼。例如,前一個表達式創建 IGrouping<int,Product> 類型的組,也就是說,分組的鍵值類型是整形,而其中的元素類型是 Product 。
IGrouping<T,K> 接口只提供一個屬性 Key ,它用於返回創建組的值。例如,如果要創建顯示每個 TitleOfCourtesy 組的 TitleOfCourtesy 的字符串列表,應該使用這樣的表達式:
var matches = from emp in employees
group emp by emp.TitleOfCourtesy into g
select g.Key;
提示:
對於這個示例,也可以使用 IEnumerable<string> 替代 var 關鍵字,因為最終的結果是一系列字符串。然后,通常在分組查詢中使用 var 關鍵字,因為通常需要使用投影和匿名類獲取更多有用的匯總信息。
另外,也可以返回整個組:
var matches = from emp in employees
group emp by emp.TitleOfCourtesy into g
select g;
這對數據綁定沒有任何用處。因為 ASP.NET 不能夠顯示關於每個組的任何有用信息。但是,它讓你可以很方便的對每個組的數據進行自由迭代:
foreach (IGrouping<string, EmployeeDetails> group in matches)
{
foreach (EmployeeDetails emp in group)
{
// do something
}
}
這段代碼說明即使創建了組,還是能夠靈活的訪問組里的每個項目。
從更實用的角度來說,可以使用聚合函數對組里的數據進行計算。LINQ 聚合函數模仿了過去可能已用到的數據庫聚合函數,允許對組中的元素進行技術和匯總,獲取最小值、最大值及平均值。
下面的示例返回一個匿名類型,它包含分組的鍵值以及分組中對象的個數,使用一個嵌入的方法 Count():
var matches = from emp in employees
group emp by emp.TitleOfCourtesy into g
select new { Title = g.Key, Employees = g.Count() };
上一個示例有一點需要注意,它使用了一個擴展方法。從本質上說,擴展方法是 LINQ 的一組核心功能,它們不是通過專門的 C# 操作符公開,而是需要直接調用這些方法。
擴展方法和普通方法的區別在於擴展方法不是定義在使用該方法的類里。LINQ 有一個 System.Linq.Enumerable 類,它定義了幾十個擴展方法,這些方法可被所有實現了 IEnumerable<T> 的對象調用。
除了 Count(),LINQ 還定義了大量可在分組中應用的強大的擴展方法,比如聚合函數 Max()、Min()、Average()等。使用了這些方法的 LINQ 表達式會更加復雜,因為它們還使用了另一個被稱為 lambda 表達式的 C# 特性,它允許為擴展方法提供其他參數。對於 Max()、Min()、Average(),lambda 表達式允許你指定用於計算的屬性。
這個示例用於計算每個類別中項目的最高價格、最低價格以及平均價格:
var categories = from p in products
group p by p.Category into g
select new
{
Category = g.Key,
MaxPrice = g.Max(p => p.UnitPrice),
MinPrice = g.Min(p => p.UnitPrice),
AvgPrice = g.Average(p => p.UnitPrice)
};
揭秘 LINQ 表達式
雖然 LINQ 使用了新的 C# 關鍵字(如 from、in 和 select),但這些關鍵字的實現是由其他類提供的。實際上,所有的 LINQ 查詢都被轉換為一組方法的調用。除了借助轉換,還可以直接調用這些方法:
var matches = from emp in employees
select emp;
// 上面的表達式可以改寫成這樣:
var matches = employees.Select(employee => employee);
這里所用的語法不太常見。代碼看起來像是在調用 employees 集合的 Select()方法。然而,employees 集合是一個普通的 List<T> 集合,它並沒有包含這個方法。相反,Select()是一個擴展方法,它被自動提供給所有的 IEnumerable<T> 類。
1. 擴展方法
擴展方法讓你能夠在一個類里定義方法,然后就好像它也在其他類里定義了那樣調用它。LINQ 擴展方法定義在了 System.Linq.Enumerable 類里,但是所有的 IEnumerable<T> 對象都可調用。
注解:
因為 LINQ 擴展方法是定義在 System.Linq.Enumerable 類里的,因此該類必須處於可用范圍。
查看一下 Select()方法的定義:
public static IEnumerable<TResult> Select<TSource, TResult>(
this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{ ... }
擴展方法需要遵守一些規則:
- 所有的擴展方法都必須是靜態的
- 擴展方法可用返回任意的數據類型並可以接收任意個數的參數
- 第一個參數必須是對調用擴展方法的對象的引用(並且之前跟着關鍵字 this)
- 該參數的數據類型決定了擴展方法可用的類
Select()方法可以被所有實現了 IEnumerable<T> 的類的示例調用(this IEnumerable<TSource> source 參數決定)。另一個參數,用於獲取正在選擇的信息段的委托。返回值是一個 IEnumerable<T> 對象。
2. lambda 表達式
lambda 表達式在 C# 里是基於方法的 LINQ 表達式的新語法。lambda 表達式像這樣傳遞給 Select()方法:
matches = employees.Select(employee => employee);
當 Select()被調用時,employees 對象作為第一個參數傳遞,它是查詢的源。第二個參數需要一個指向某個方法的委托。這個方法執行選擇任務,且被集合里的每個元素調用。
Select()方法接收一個委托。你可以提供一個普通委托(它指向定義在類里的其他地方的命名方法),但這樣做的話會使你的代碼變的冗長。
一個更簡單的解決辦法是使用匿名方法,匿名方法以 delegate 開頭,然后是方法簽名的聲明,隨后的花括號里是該方法的代碼。如果使用匿名方法,前面的示例看起來應該是這個樣子的:
IEnumerable<EmployeeDetails> matches = employees
.Select(
delegate(EmployeeDetails emp)
{
return emp;
}
);
lambda 表達式就是讓這類代碼看起來更加簡練的一種方式。lambda 表達式由以 => 分隔的兩部分組成。第一部分表示匿名方法接收的參數,對於這個示例,lambda 表達式接收集合里的每個對象並通過名為 employee 的引用暴露它們。lambda 表達式的第二部分定義要返回的值。
下面這個顯式的 LINQ 表達式從每個 employee 里析取數據並封裝成一個匿名類型返回:
var matches = employees
.Select(
delegate(EmployeeDetails employee)
{
return new
{
First = employee.FirstName,
Last = employee.LastName
};
}
);
現在,你可以用 lambda 表達式來簡化代碼:
var matches = employees.Select(employee =>
new { First = employee.FirstName, Last = employee.LastName });
3. Multipart 表達式
當然,多數 LINQ 表達式要比我們這里講過的示例更加復雜。一個更為現實的 LINQ 表達式可能會加入排序或過濾。
比如下面這段代碼:
matches = from employee in employees
where employee.LastName.StartsWith("D")
select employee;
可以用顯示的語法把這個表達式重寫為:
matches = employees
.Where(employee => employee.LastName.StartsWith("D"))
.Select(employee => employee);
顯式 LINQ 語法的一個好處是它使操作符順序更加明確。對於前一個示例,可以很清楚的看到它從 employees 集合開始,然后調用 Where(),最后調用 Select()。如果要使用更多的操作符,將會面臨更長的一組方法調用。
Where()提供一個 lambda 表達式驗證每個元素,如果它應該包含在結果中,則返回 true。
Select()提供一個 lambda 表達式用於把每個數據項轉換為你期望的形式。
大多數情況下,都使用隱式語法創建 LINQ 表達式。但是,偶爾還是要用到顯式語法。例如,需要向擴展方法傳遞一個不被隱式 LINQ 語法支持的參數時。
理解表達式如何映射到方法的調用、擴展方法如何綁定到 IEnumerable<T> 對象、lambda 表達式怎樣封裝過濾、排序、投影等。這些使得 LINQ 的內部工作細節變得清晰。