LINQ 的標準查詢操作符

Northwnd db = new Northwnd(@"c:\northwnd.mdf");
IEnumerable<Customer> results = db.ExecuteQuery<Customer>
("SELECT contactname FROM customers WHERE city = {0}",
    "London");


LINQ 的標準查詢操作符
John Papa

代碼下載位置: DataPoints2008_03.exe (958 KB)
Browse the Code Online
  目錄
操作符和 LINQ
操作符類型
Lambda 表達式
First 和 Single
聚合、層次結構和投影
投影和排序
限定符和轉換
結束語

語言集成查詢 (LINQ) 允許開發人員通過強類型化語法使用 Microsoft? .NET Framework 3.5 代碼編寫類似 SQL 的查詢。然後,各種 LINQ 提供程序,如 LINQ to Objects(可利用它根據對象層次結構編寫查詢)和 LINQ to Entities(可利用它根據實體框架的概念模型編寫查詢)可根據代表數據存儲的細微差別來有效處理這些查詢。
除強類型化語法外,LINQ 查詢還具有一個標準查詢操作符庫來增強其功能。這些標準查詢操作符對序列進行運算並可執行各種運算,如確定序列中是否存在某個值以及對序列運行合計函數(如求和)。
在本月的專欄中,我將使用 LINQ 來執行實際的查詢和運算(會用到 LINQ to Objects 和 LINQ to Entities)。我將查詢一個實體集合並使用其導航屬性深入研究一組具備層次結構的實體。我還會爲您演示如何對數組和集合應用多個標準查詢操作符。並展示如何使用 lambda 表達式強化 LINQ 的標準查詢操作符,以及如何利用它們來從序列解析特定信息並對序列執行復雜的邏輯運算。本專欄的下載中提供有所有代碼示例(請參見 msdn.microsoft.com/msdnmag/code08.aspx)。


操作符和 LINQ
LINQ 自身功能非常強大,無論使用的是 LINQ to XML、LINQ to DataSets、LINQ to Entities、LINQ to Objects 還是附帶的任何其他 LINQ 提供程序。LINQ 的核心功能在於其強類型化查詢語法,它可用於任意此類提供程序。當將 LINQ 與一個或多個標準查詢操作符結合使用時,會得到一個功能更爲強大的工具集,從而可精細地控制一組數據。
標準查詢操作符在 System.Linq 命名空間中的 System.Core.dll 程序集中作爲靜態類 Enumerable 和 Queryable 的擴展方法存在,並且可用於實現 IEnumerable<T> 或 IQueryable<T> 的對象。這樣它們就能使用 LINQ to Entities 和 LINQ to SQL 之類的提供程序對各類對象執行運算,從內存中的集合和數組(序列)到遠程數據庫。
可輕鬆地確定處理特定任務時所擁有的操作符。如果要在 LINQ 查詢中使用操作符,可使用 Queryable 靜態類可用擴展方法中的操作符。如果要對實現 IEnumerable<T> 的序列使用操作符,可使用 Enumerable 靜態類中的一個擴展方法。但是,請記住:並非 Queryable 類中的所有操作符都適用於基礎數據存儲,因此運行時可能不支持某些操作符。


操作符類型
操作符有多種類型(使用對象瀏覽器查看 Enumerable 和 Queryable 類即可找到所有操作符)。圖 A 以字母順序顯示了不同類型操作符的分類。可利用它來大致瞭解一下操作符所提供的功能。我將使用 LINQ to Objects 和 LINQ to Entities 展示一小組此類操作符,以顯示它們如何爲實際應用程序帶來好處。
  Figure A Categories of Operators

操作符 說明
聚合  
Aggregate 對序列執行一個自定義方法
Average 計算數值序列的平均值
Count 返回序列中的項目數(整數)
LongCount 返回序列中的項目數(長型)
Min 查找數字序列中的最小數
Max 查找數字序列中的最大數
Sum 彙總序列中的數字
連接  
Concat 將兩個序列連成一個序列
轉換  
Cast 將序列中的元素轉換成指定類型
OfType 篩選序列中指定類型的元素
ToArray 從序列返回一個數組
ToDictionary 從序列返回一個字典
ToList 從序列返回一個列表
ToLookup 從序列返回一個查詢
ToSequence 返回一個 IEnumerable 序列
元素  
DefaultIfEmpty 爲空序列創建默認元素
ElementAt 返回序列中指定索引的元素
ElementAtOrDefault 返回序列中指定索引的元素,或者如果索引超出範圍,則返回默認值
First 返回序列中的第一個元素
FirstOrDefault 返回序列中的第一個元素,或者如果未找到元素,則返回默認值
Last 返回序列中的最後一個元素
LastOrDefault 返回序列中的最後一個元素,或者如果未找到元素,則返回默認值
Single 返回序列中的單個元素
SingleOrDefault 返回序列中的單個元素,或者如果未找到元素,則返回默認值
相等  
SequenceEqual 比較兩個序列看其是否相等
生成  
Empty 生成一個空序列
Range 生成一個指定範圍的序列
Repeat 通過將某個項目重複指定次數來生成一個序列
分組  
GroupBy 按指定分組方法對序列中的項目進行分組
聯接  
GroupJoin 通過歸組將兩個序列聯接在一起
Join 將兩個序列從內部聯接起來
排序  
OrderBy 以升序按值排列序列
OrderByDescending 以降序按值排列序列
ThenBy 升序排列已排序的序列
ThenByDescending 降序排列已排序的序列
Reverse 顛倒序列中項目的順序
分區  
Skip 返回跳過指定數目項目的序列
SkipWhile 返回跳過不滿足表達式項目的序列
Take 返回具有指定數目項目的序列
TakeWhile 返回具有滿足表達式項目的序列
投影  
Select 創建部分序列的投影
SelectMany 創建部分序列的一對多投影
限定符  
All 確定序列中的所有項目是否滿足某個條件
Any 確定序列中是否有任何項目滿足條件
Contains 確定序列是否包含指定項目
限制  
Where 篩選序列中的項目
設置  
Distinct 返回無重複項目的序列
Except 返回代表兩個序列差集的序列
Intersect 返回代表兩個序列交集的序列
Union 返回代表兩個序列交集的序列

Lambda 表達式
許多標準查詢操作符在對序列執行運算時都使用 Func 委託來處理單個元素。Lambda 表達式可與標準查詢操作符結合使用以代表委託。lambda 表達式是創建委託實現的簡略表達形式,並可用於匿名委託適用的所有場合。C# 和 Visual Basic? .NET 均支持 Lambda 表達式。但是,必須注意:由於 Visual Basic .NET 尚不支持匿名方法,Lambda 表達式可能僅包含一個語句。
讓我們來看看如何對一個整數數組使用 Single 操作符。這個整數數組的每個元素代表 2 的 1 到 10 次方。先創建此數組,然後使用 Single 操作符來檢索滿足 Lambda 表達式中指定條件的單個整數元素:
複製代碼
int[] nums = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 };
int singleNum = nums.Single(x => x > 16 && x < 64);
Console.WriteLine(singleNum.ToString());

Lambda 表達式包含多個關鍵部分。Lambda 表達式首先定義傳入委託的變量。在以上代碼示例中,x(在 => 操作符左側聲明)是參數,代表傳遞給它的 nums 數組中的每個元素。Lambda 表達式的剩餘部分代表數組中每個元素的評估邏輯。可使用匿名委託輕鬆地重新編寫以上表達式,如下所示:
複製代碼
int singleNum = nums.Single<int>(
  delegate(int x) {return (x > 16 && x < 64); }
) ;

但是,此代碼的可讀性不及 Lambda 表達式。C# 2.0 引入了可使委託的傳遞稍微輕鬆些的匿名委託;但是,Lambda 表達式的簡潔語法可使其更加簡單。


First 和 Single
如果必須從序列中提取一個值,First、FirstOrDefault、Single 和 SingleOrDefault 操作符都非常有用。First 方法返回序列中的第一個元素。First 有一個重載方法,可使用它來傳入 Lambda 表達式來代表一個條件。例如,如果要返回整數序列中整數元素大於 50 的第一個元素,可使用以下代碼示例:
複製代碼
int[] nums = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 };
int num1 = nums.First<int>();
int num2 = nums.First<int>(x => x > 50);
int num3 = nums.FirstOrDefault<int>(x => x > 5000);

Console.WriteLine(
  num1.ToString() + "-" +
  num2.ToString() + "-" +
  num3.ToString());

此代碼會查找第一個元素 (1)、大於 50 的第一個元素 (64) 以及大於 5,000 的第一個元素。由於數組中沒有元素滿足第三個 Lambda 表達式(數組中無整數大於 5,000),則如果代碼使用的是 First 操作符而非 FirstOrDefault,則會引發異常。在使用 FirstOrDefault 操作符時,如果沒有元素滿足 Lambda 表達式,則會返回 0。First 操作符也可用於 LINQ to Entities 查詢,如下所示:
複製代碼
using (Entities entities = new Entities())
{
  var query = (from c in entities.Customers
               select c).First(c => c.City.Equals("London"));
  Console.WriteLine(query.CompanyName);
}

在此示例中,將返回 London 城中的第一個客戶。正如您所看到的,當 First 方法用於各種 LINQ 提供程序(在本例中爲 LINQ to Objects 和 LINQ to Entities)時,所用的語法並不會更改。
在 LINQ to Entities 上下文中,First 操作符非常有用,尤其是您知道會從查詢返回單個記錄時。例如,您可能有個查詢,它常在給出 CustomerID 時獲取一條客戶記錄。這種情況總是返回 0 或 1 條記錄,因此,得到一個序列不如就得到一個實體本身。換句話說,您寧願獲取 Customer 實體而非 1 個 Customer 實體序列。First 方法在某種怦下非常有用,如以下代碼段所示。(由於實體框架不會嘗試在客戶端和服務器之間分發單個查詢的執行,並且 LINQ to Entities 不支持 Single 方法,因此使用 First 方法是個輕鬆的替代方法。)
複製代碼
using (Entities entities = new Entities())
{
  var query = (from c in entities.Customers
               where c.CustomerID.Equals("BOLID")
               select c).First();
  Console.WriteLine(query.CompanyName);
}

聚合、層次結構和投影
在 LINQ to Entities 查詢中使用聚合操作符(如 Sum)可有助於簡化查詢。例如,以下代碼檢索訂單總額大於 $10,000 的一個訂單序列:
複製代碼
using (Entities entities = new Entities())
{
  var query = from o in entities.Orders
              where o.OrderDetails.Sum(
                od => od.UnitPrice * od.Quantity) >= 10000
              select o;
  foreach (Orders order in query)
    Console.WriteLine(order.OrderID);

由於 LINQ 可查詢層次結構實體集合,因此標準查詢操作符也可用於對嵌套實體序列執行運算。當必須計算或詢問派生數據時,這一點非常有用。派生數據可能僅存在於其基本窗體中,如客戶訂單的詳細信息僅包含單價和數量值。在本例中,未在模型中的任何位置提供代表訂單總金額的聚合數據。然而,通過在 LINQ 查詢中應用 Sum 操作符,仍可檢索消費金額超過 $20,000 的所有客戶,如下所示:
複製代碼
using (Entities entities = new Entities())
{
  var query = from c in entities.Customers
              where c.Orders.Sum(
                o => o.OrderDetails.Sum(
                  od => od.UnitPrice * od.Quantity)) >= 25000
              select c;
  foreach (Customers customer in query)
    Console.WriteLine(customer.CompanyName);
}

此示例展示瞭如何在 LINQ 查詢的多個層次應用標準查詢操作符。查詢最終會返回一個 Customers 實體序列,但爲達到此目的,它必須首先深入每個客戶的訂單以及每個訂單的訂單詳細信息獲取所需數據,這樣纔可以計算每項的價格,彙總每個訂單的項目,然後彙總每個客戶的總額。
Count 操作符是另一聚合標準查詢操作符。可通過使用以下代碼確定有多少客戶的消費金額超過 $25,000:
複製代碼
using (Entities entities = new Entities())
{
  var query = (from c in entities.Customers
               where c.Orders.Sum(
                 o => o.OrderDetails.Sum(
                   od => od.UnitPrice * od.Quantity)) >= 25000
               select c).Count();
  Console.WriteLine(query);
}

可使用 Max 操作符來確定最佳客戶。以下代碼示例將返回消費最高的客戶所花費的金額。它在層次結構的多個層級中組合使用 Sum 和 Max 聚合操作符:
複製代碼
using (Entities entities = new Entities())
{
  var query = (from c in entities.Customers
               select new
               {
                 c.CustomerID,
                 Total = c.Orders.Sum(
                   o => o.OrderDetails.Sum(od => od.UnitPrice))
               }).Max(c2 => c2.Total);
  Console.WriteLine(query);
}

投影和排序
您可能還注意到我在之前的示例中暗藏了一個投影。在使用 Max 操作符之前,LINQ 查詢並不返回客戶列表。而是會返回一個投影,此投影創建了包含 CustomerID 屬性和 Total 屬性(客戶的整個消費金額)的一個新實體。投影是 LINQ 必不可少的一部分,如前一示例所示,將它們投影到序列中後,就可使用標準查詢操作符來進一步處理它們。
圖 1 顯示瞭如何創建一個新實體投影,其中包含 CustomerID 和客戶的訂單總金額(使用之前討論的 Sum 操作符)。圖 1 還使用 OrderByDescending 操作符來按計算總額對投影實體序列進行排序。如果兩個客戶總額相同,還會使用另一排序操作符來進一步定義順序。例如,還可使用以下代碼修正圖 1 中的 foreach 語句以進一步限定排序規則:
  Figure 1 Aggregates, Projections, and Ordering
複製代碼
using (Entities entities = new Entities())
{
  var query = from c in entities.Customers
              where c.Orders.Sum(
                o => o.OrderDetails.Sum(od => od.UnitPrice)) > 0
              select new
              {
                c.CustomerID,
                Total = c.Orders.Sum(
                  o => o.OrderDetails.Sum(od => od.UnitPrice))
              };
  foreach (var item in query.OrderByDescending(x => x.Total))
    Console.WriteLine(item.CustomerID + " == " + item.Total);
}


複製代碼
foreach (var item in
  query.OrderByDescending(x => x.Total)
  .ThenBy(x => x.CustomerID))
{
  Console.WriteLine(item.CustomerID + " == " + item.Total);
}

在該代碼段中,我添加了 ThenBy 操作符和一個 Lambda 表達式,以表示序列應首先按 Total 屬性降序排列,然後按投影的 CustomerID 屬性升序排列。


限定符和轉換
如果需要確定序列中是否存在某個值,可使用標準查詢操作符 Any。限定符(如 Any、All 和 Contains)會搜索元素序列,並評估序列是否滿足 lambda 表達式的條件。如果需檢查序列以確定某些事宜(例如:是否存在來自特定地址的客戶、所有客戶是否來自同一國家或者任意其他分析確定性問題),它將非常有用。
例如,以下 LINQ 查詢會檢查是否來自 United Kingdom 的所有客戶都位於 London。它使用限定符 All 並將其傳遞給僅評估城市是否爲 London 的 lambda 表達式。如果序列中的每個元素都滿足此條件並且 lambda 表達式返回 true,然後 All 操作符會返回 true:
複製代碼
using (Entities entities = new Entities())
{
  bool allUKCustomerAreFromLondon = (from c in entities.Customers
                                     where c.Country == "UK"
                                     select c).All(
                                       c => c.City.Equals("London"));
  Console.WriteLine(allUKCustomerAreFromLondon ? "Yes" : "No");
}           

需在此查詢中詢問的另一問題是序列中是否有來自 United Kingdom 的 Cowes 的實體。對於此問題,可使用 Any 限定符來計算序列,如下所示:
複製代碼
using (Entities entities = new Entities())
{
  bool isOneUKCustomerFromCowes = (from c in entities.Customers
                                   where c.Country == "UK"
                                   select c).Any(
                                     c => c.City.Equals("Cowes"));
  Console.WriteLine(isOneUKCustomerFromCowes? "Yes" : "No");
}

Contains 操作符在評估序列中是否包括您所查找的項目時類似於 Any 操作符。Any 操作符可確定序列的某個項中是否存在某個值,而 Contains 操作符則確定序列中是否存在特定項目實例。例如,在將某個對象添加到序列中之前,您可能希望確保序列中並未包含該對象。圖 2 展示瞭如何檢查。
  Figure 2 Using Contains and Conversion
複製代碼
using (Entities entities = new Entities())
{
    Customers customerBSBEV = (from c in entities.Customers
                               where c.CustomerID == "BSBEV"
                               select c).First();

    var customersUK = from c in entities.Customers
                      where c.Country == "UK"
                      select c;

    bool isCustomerInSequence = customersUK.Contains(customerBSBEV);

    Console.WriteLine(isCustomerInSequence? "Yes" : "No");
}


請注意:在圖 2 中,首先針對 BSBEV 客戶檢索 Customers 實體。然後,檢索客戶來自 United Kingdom 的 Customers 實體序列。最後,使用 Contains 操作符來檢查 Customers 序列是否包含 customerBSBEV 變量的實例。
圖 2 中所顯示的 Contains 操作符實現適用於可基於其實際實例信心十足地比較對象的場合。但是,如果需要 Contains 操作符根據邏輯標識進行測試又該如何呢?幸運的是,Contains 操作符包含一個重載,可使用它來傳遞實現 IEqualityComparer<T> 接口的對象。要根據 CustomerID 使用 Contains,可按如下所示重新編寫圖 2 中的代碼:
複製代碼
using (Entities entities = new Entities())
{
  ...

  bool isCustomerInSequence = customersUK.Contains(customerBSBEV,
    new CustomerComparer());

  Console.WriteLine(isCustomerInSequence? "Yes" : "No");
}

其中 CustomerComparer 定義爲
複製代碼
private class CustomerComparer : IEqualityComparer<Customers>
{
  public bool Equals(Customers x, Customers y) {
    if (x == null || y == null)
      return false;
    return x.CustomerID.Equals(y.CustomerID);
  }

  ...
}

結束語
有許多標準查詢操作符均可定義爲 Enumerable 和 Queryable 序列類的擴展方法。如我之前所示,這些操作符有助於擴展 LINQ 的功能。我還展示了結合使用多個 .NET Framework 3.5 新增強功能(包括 lambda 表達式、LINQ、實體框架和隱式類型化變量)來更加輕鬆地編寫功能強大的代碼和邏輯。

發佈了18 篇原創文章 · 獲贊 1 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章