本文將為大家介紹 LINQ 取得資料來源中單一項目的運算子:First、Last、ElementAt、Single,以及相匹配的 FirstOrDefault、LastOrDefault、ElementAtOrDefault、SingleOrDefault 運算子。
自學筆記這系列是我自己學習的一些心得分享,歡迎指教。這系列的分享,會以 C# + 我比較熟的 Net 3.5 環境為主。
另外本系列預計至少會切成【打地基】和【語法應用】兩大部分做分享。打地基的部分,講的是 LINQ 的組成元素,這部分幾乎和 LINQ 無關,反而是 C# 2.0、C# 3.0 的一堆語言特性,例如:型別推斷、擴充方法、泛型、委派等等,不過都會把分享的範圍限制在和 LINQ 應用有直接相關功能。
PS. LINQ 自學筆記幾乎所有範例,都可直接複製到 LINQPad 4 上執行(大多是用 Statements 和 Program 模式)。因為它輕巧好用,功能強大,寫範例很方便,請大家自行到以下網址下載最新的 LINQPad:http://www.LINQpad.net/。
在 LINQ 技術中,有設計一組專門用來取出來源序列中單一項目的運算子,也就是 First、Last、ElementAt、Single,它們同時也都搭配了一組取不到資料就輸出來源資料型別預設值的運算子:FirstOrDefault、LastOrDefault、ElementAtOrDefault、SingleOrDefault,這些就是本文的主題。
請注意,這八個標準查詢運算子都不支援查詢語法(Query syntax),必須用方法語法(Method syntax)才能使用,而且這八個運算子都是立即執行(Immediately execution)查詢的運算子,使用時請留心。
最常用、最通用的就是 First、FirstOrDefault 運算子,它們會回傳來源序列中的第一個元素,或符合條件的第一個元素。它們都有兩個多載方法:
public static TSource First<TSource>(
this IEnumerable<TSource> source
)
public static TSource First<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate
)
public static TSource FirstOrDefault<TSource>(
this IEnumerable<TSource> source
)
public static TSource FirstOrDefault<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate
)
兩個運算子的第一個方法都不用傳入參數,都是 IEnumerable<TSource> 的擴充方法,可以直接呼叫;第二個擴充方法則是支援我們傳入一個委派方法(Func<TSource, bool> predicate),自行定義要取出符合怎樣條件的第一筆資料。
先談談 First 和 FirstOrDefault 的差別是什麼:First 運算子,若取不到資料,會在執行時期拋出 InvalidOperationException 例外,但是 FirstOrDefault 則會回傳 TSource 的預設值。這個差別可以同時套用在 Last、ElementAt、Single、LastOrDefault、ElementAtOrDefault、SingleOrDefault 運算子身上,後續就不再特別說明此差異。
我們先看呼叫第一個多載方法的範例:
void Main()
{
//建立訂單資料來源
var orders = new List<Order>(){
new Order {CustomerId = 3, OrderDate = new DateTime(2011, 10, 9), Total = 2940},
new Order {CustomerId = 2, OrderDate = new DateTime(2012, 10, 10), Total = 3849},
new Order {CustomerId = 1, OrderDate = new DateTime(2011, 12, 1), Total = 500},
new Order {CustomerId = 1, OrderDate = new DateTime(2012, 2, 28), Total = 1234},
new Order {CustomerId = 2, OrderDate = new DateTime(2012, 5, 20), Total = 9520}
};
var queryFirst = orders.First();
queryFirst.Dump("First");
//orders 中沒有顧客編號 4 的資料,用 First 會出現錯誤,所以要用 FirstOrDefault
var queryFD = orders.Where(o => o.CustomerId == 4).FirstOrDefault();
queryFD.Dump("FirstOrDefault");
}
//訂單基本資料類別
public class Order
{
public int CustomerId { get; set; }
public DateTime OrderDate { get; set; }
public double Total { get; set; }
public override string ToString()
{
return string.Format("CustomerId = {0}, OrderDate = {1}, Total = {2}",
CustomerId, OrderDate, Total);
}
}
上述程式碼,我們建立一個序列儲存訂單資料(orders),然後定義第一個查詢(queryFirst),直接取出第一筆訂單資料,因為沒有做排序,所以 First 運算子抓到的是第一個加入清單中的訂單資料。接著我們再定義第二個查詢(queryFD),先用 Where 運算子篩選出顧客編號(CustomerId) 4 的訂單資料,再用 FirstOrDefault 運算子取出第一筆資料,但實際上訂單資料中沒有顧客編號 4 的資料,所以查詢結果會回傳 null。此處若是改用 First 運算子,則會出現 InvalidOperationException 錯誤。
再來看呼叫第二個多載方法的範例,請注意,我們直接沿用上述的範例程式碼,所以不再貼出 Order 類別,比較不注浪費篇幅:
void Main()
{
//建立訂單資料來源
var orders = new List<Order>(){
new Order {CustomerId = 3, OrderDate = new DateTime(2011, 10, 9), Total = 2940},
new Order {CustomerId = 2, OrderDate = new DateTime(2012, 10, 10), Total = 3849},
new Order {CustomerId = 1, OrderDate = new DateTime(2011, 12, 1), Total = 500},
new Order {CustomerId = 1, OrderDate = new DateTime(2012, 2, 28), Total = 1234},
new Order {CustomerId = 2, OrderDate = new DateTime(2012, 5, 20), Total = 9520}
};
var queryFirst2 = orders.First(o => o.Total < 2000);
queryFirst2.Dump("First 第二個多載方法");
var queryFD2 = orders.FirstOrDefault(o => o.CustomerId == 4);
queryFD2.Dump("FirstOrDefault 第二個多載方法");
}
上述程式碼,我們定義第一個查詢(queryFirst2),直接在訂單資料(orders)上調用 First 運算子,但是我們傳入一個 Lambda 運算式,設定篩選出訂單金額(Total)小於 2000 元的資料,因為沒有指定排序,所以 First 運算子會取出符合條件的第一筆資料,也就是上述畫面的第一個表格內容。接著我們定義第二個查詢(queryFD2),直接在訂單資料上調用 FirstOrDefault 運算子,並傳入一個 Lambda 運算式,設定篩選出顧客編號 4 的訂單資料,因為沒有此顧客的訂單資料,所以會回傳預設值,也就是 null。
Last、LastOrDefault 運算子和 First、FirstOrDefault 用法完全相同,只是這組運算子是取來源序的最後一筆資料。請注意,Last、LastOrDefault 運算子不支援 LINQ to Entities 和 LINQ to SQL。
public static TSource Last<TSource>(
this IEnumerable<TSource> source
)
public static TSource Last<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate
)
public static TSource LastOrDefault<TSource>(
this IEnumerable<TSource> source
)
public static TSource LastOrDefault<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate
)
void Main()
{
//建立訂單資料來源 - LINQ to Objects
var orders = new List<Order>(){
new Order {CustomerId = 3, OrderDate = new DateTime(2011, 10, 9), Total = 2940},
new Order {CustomerId = 2, OrderDate = new DateTime(2012, 10, 10), Total = 3849},
new Order {CustomerId = 1, OrderDate = new DateTime(2011, 12, 1), Total = 500},
new Order {CustomerId = 1, OrderDate = new DateTime(2012, 2, 28), Total = 1234},
new Order {CustomerId = 2, OrderDate = new DateTime(2012, 5, 20), Total = 9520}
};
var queryLast = orders.Last();
queryLast.Dump("Last");
var queryLD = orders.Where(o => o.CustomerId == 4).LastOrDefault();
queryLD.Dump("LastOrDefault");
var queryLast2 = orders.Last(o => o.Total < 2000);
queryLast2.Dump("Last 第二個多載方法");
var queryLD2 = orders.LastOrDefault(o => o.CustomerId == 4);
queryLD2.Dump("LastOrDefault 第二個多載方法");
//建立訂單資料來源 - LINQ to Entities
// var OrderEntities = from o in Orders where o.Freight > 700 select new {o.CustomerID, o.OrderID, o.Freight, o.ShipCountry};
// OrderEntities.Last();
}
//訂單基本資料類別
public class Order
{
public int CustomerId { get; set; }
public DateTime OrderDate { get; set; }
public double Total { get; set; }
public override string ToString()
{
return string.Format("CustomerId = {0}, OrderDate = {1}, Total = {2}",
CustomerId, OrderDate, Total);
}
}
上述程式碼,我用沿用先前 First 運算子的程式碼,只是改成 Last 運算子,大家可以比較一下輸出結果。另外程式面中有兩行註解掉的程式碼:
// var OrderEntities = from o in Orders where o.Freight > 700 select new {o.CustomerID, o.OrderID, o.Freight, o.ShipCountry};
// OrderEntities.Last();
[code]
其實就是在建立 LINQ to Entities 的資料來源,並在來源上調用 Last 運算子,但是執行時會發生 NotSupportedException 例外。
第三個主題是 ElementAt 和 ElementAtOrDefault 運算子。請注意,ElementAt、ElementAtOrDefault 運算子不支援 LINQ to Entities 和 LINQ to SQL。
[code]
public static TSource ElementAt<TSource>(
this IEnumerable<TSource> source,
int index
)
public static TSource ElementAtOrDefault<TSource>(
this IEnumerable<TSource> source,
int index
)
這兩個運算子都只有一個簽名碼,也就是傳入一個 int 型別的參數,告訴 LINQ 我們要取的是來源序列中,第幾個索引值的項目。同樣的,若索引值不存在,ElementAt 會拋出例外,ElementAtOrDefault 會回傳 TSource 預設值:
void Main()
{
//建立訂單資料來源 - LINQ to Objects
var orders = new List<Order>(){
new Order {CustomerId = 3, OrderDate = new DateTime(2011, 10, 9), Total = 2940},
new Order {CustomerId = 2, OrderDate = new DateTime(2012, 10, 10), Total = 3849},
new Order {CustomerId = 1, OrderDate = new DateTime(2011, 12, 1), Total = 500},
new Order {CustomerId = 1, OrderDate = new DateTime(2012, 2, 28), Total = 1234},
new Order {CustomerId = 2, OrderDate = new DateTime(2012, 5, 20), Total = 9520}
};
var queryElemantAt = orders.ElementAt(1);
queryElemantAt.Dump("ElemantAt");
var queryElemantAtD = orders.Where(o => o.CustomerId == 4).ElementAtOrDefault(1);
queryElemantAtD.Dump("ElemantAtOrDefault");
}
//訂單基本資料類別
public class Order
{
public int CustomerId { get; set; }
public DateTime OrderDate { get; set; }
public double Total { get; set; }
public override string ToString()
{
return string.Format("CustomerId = {0}, OrderDate = {1}, Total = {2}",
CustomerId, OrderDate, Total);
}
}
上述程式碼,第一個查詢使用 ElementAt 運算子,指定要取出索引值為 1 的項目,輸出結果如上圖第一個表格內容;第二個查詢使用 ElementAtOrDefault 運算子,對一個空的資料來源指定要取出索引值為 1 的項目,結果回傳 null。
最後是 Single、SingleOrDefault 運算子。這兩個運算子有幾個特性要注意:
.Net 3.5 的 LINQ to Entities、LINQ to SQL 都不支援,但是 .Net 4.0 以上就支援了。
Single 運算子,若資料來源為空值,會拋出 ArgumentNullException 例外;若資料來源包含多個項目,會拋出 InvalidOperationException 例外。換言之,要正確取出結果,來源序列必須只有一筆資料。
SingleOrDefault 運算子,若資料來源為空值,則會輸出 TSource 型別預設值;若資料來源包含多個項目,會拋出 InvalidOperationException 例外。
public static TSource FirstOrDefault(
this IEnumerable source
)
public static TSource FirstOrDefault(
this IEnumerable source,
Func<TSource, bool> predicate
)
0
public static TSource FirstOrDefault<TSource>(
this IEnumerable<TSource> source
)
public static TSource FirstOrDefault<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate
)
1
public static TSource FirstOrDefault<TSource>(
this IEnumerable<TSource> source
)
public static TSource FirstOrDefault<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate
)
2
上述程式碼,第一和第三個查詢(querySingle、querySingle2),差別只是把過濾來源序列成為單筆資料的條件是放在 Where 運算子,或是在 Single 第二個多載方法中;同樣地,第二、四個查詢也只是把篩選條件放在 Where 運算子或 SingleOrDefault 運算子中而已。