C# 的 LINQ 查詢簡介

「查詢」是指從資料來源中擷取資料的運算式。 不同的資料來源有不同的原生查詢語言,例如關聯式資料庫 SQL 和 XML 的 XQuery。 開發人員必須針對所需支援的每種資料來源類型或資料格式,學習新的查詢語言。 LINQ 為資料來源和格式類型提供一致的 C# 語言模型,以簡化這種情況。 在 LINQ 查詢中,您一律會使用 C# 物件。 您會使用相同的基本編碼模式,來查詢及轉換 XML 文件、SQL 資料庫、.NET 集合,以及當 LINQ 提供者可用時任何其他格式中的資料。

查詢作業的三個部分

所有的 LINQ 查詢作業都包含三個不同的動作:

  1. 取得資料來源。
  2. 建立查詢。
  3. 執行查詢。

下列範例示範查詢作業的三個部分在原始程式碼中的表示方式。 為了方便起見,此範例使用整數陣列作為資料來源;不過,相同的概念也適用於其他資料來源。 本文章的其他部分都會參考此範例。

// The Three Parts of a LINQ Query:
// 1. Data source.
int[] numbers = [ 0, 1, 2, 3, 4, 5, 6 ];

// 2. Query creation.
// numQuery is an IEnumerable<int>
var numQuery =
    from num in numbers
    where (num % 2) == 0
    select num;

// 3. Query execution.
foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}

下圖顯示完整的查詢作業。 在 LINQ 中,查詢的執行與查詢本身不同。 換句話說,您不會藉由建立查詢變數來擷取任何資料。

完整 LINQ 查詢作業的圖表。

資料來源

上述範例中的資料來源為陣列,可支援泛型 IEnumerable<T> 介面。 這表示可使用 LINQ 進行查詢。 查詢會在 foreach 陳述式中執行,而 foreach 則需要IEnumerableIEnumerable<T>。 支援 IEnumerable<T> 或衍生介面的類型,例如泛型 IQueryable<T> 稱為可查詢的類型

可查詢型別無須進行修改或特殊處理,即可作為 LINQ 資料來源。 若來源資料還不是記憶體中的可查詢型別,LINQ 提供者則須將其表示為可查詢型別。 例如,LINQ to XML 會將 XML 文件載入可查詢的 XElement 類型:

// Create a data source from an XML document.
// using System.Xml.Linq;
XElement contacts = XElement.Load(@"c:\myContactList.xml");

使用 EntityFramework 時,您會在 C# 類別與資料庫結構描述之間建立物件關聯式對應。 您可針對物件撰寫查詢,EntityFramework 會在執行階段處理與資料庫之間的通訊。 在下列範例中,Customers 代表資料庫中特定的資料表,而查詢結果 IQueryable<T> 的類型則衍生自 IEnumerable<T>

Northwnd db = new Northwnd(@"c:\northwnd.mdf");

// Query for customers in London.
IQueryable<Customer> custQuery =
    from cust in db.Customers
    where cust.City == "London"
    select cust;

如需如何建立特定資料來源類型的詳細資訊,請參閱各類 LINQ 提供者的文件。 但基本的規則很清楚:任何支援泛型 IEnumerable<T> 介面或所繼承介面的物件,皆可為 LINQ 資料來源,通常是 IQueryable<T>

注意

這些類型 (例如 ArrayList) 支援非泛型 IEnumerable 介面,也可作為 LINQ 資料來源。 如需詳細資訊,請參閱如何使用 LINQ 查詢 ArrayList (C#)

查詢

查詢可指定要從一或多個資料來源擷取的資訊。 查詢也可選擇性地指定該項資訊傳回之前應該如何排序、分組和成形。 查詢是儲存在查詢變數中,並以查詢運算式初始化。 您可以使用 C# 查詢語法來撰寫查詢。

上述範例中的查詢會傳回整數陣列中的所有偶數。 此查詢運算式包含三個子句︰fromwhereselect。 (如果您熟悉 SQL,您注意到這些子句的排序與 SQL 中的排序相反。)from 子句會指定資料來源,where 子句會套用篩選,而 select 子句會指定傳回項目的類型。 本節會詳細討論所有查詢子句。 但目前的重點是,LINQ 中的查詢變數本身不會採取任何動作,且不會傳回任何資料。 它只會儲存稍後執行查詢以產生結果時所需要的資訊。 如需如何建構查詢的詳細資訊,請參閱標準查詢運算子概觀 (C#) (英文)。

注意

查詢也可以使用方法語法來表示。 如需詳細資訊,請參閱 LINQ 中的查詢語法及方法語法

根據執行方式分類標準查詢運算子

標準查詢運算子方法的 LINQ to Objects 實作會以兩種主要方式之一執行:立即延後。 使用延後執行的查詢運算子可以另外細分成兩個分類︰資料流非資料流

立即

立即執行表示已讀取資料來源,並執行作業一次。 所有傳回純量結果的標準查詢運算子都會立即執行。 這類查詢的範例包括 CountMaxAverageFirst。 這些方法執行時並未使用明確的 foreach 陳述式,因為查詢本身必須使用 foreach 才能傳回結果。 這些查詢傳回的是單一的值,而不是 IEnumerable 集合。 您可以使用 Enumerable.ToListEnumerable.ToArray 方法來強制任何查詢立即執行。 立即執行可讓您重複使用查詢結果,而不是查詢宣告。 結果會擷取一次,然後加以儲存以供日後使用。 下列查詢會傳回來源陣列中的偶數計數:

var evenNumQuery =
    from num in numbers
    where (num % 2) == 0
    select num;

int evenNumCount = evenNumQuery.Count();

若要立即強制執行任何查詢,並快取其結果,可以呼叫 ToListToArray方法。

List<int> numQuery2 =
    (from num in numbers
        where (num % 2) == 0
        select num).ToList();

// or like this:
// numQuery3 is still an int[]

var numQuery3 =
    (from num in numbers
        where (num % 2) == 0
        select num).ToArray();

您也可以將 foreach 迴圈放在緊接著查詢運算式後方的位置,以強制執行查詢。 不過,藉由呼叫 ToListToArray,您也可以快取單一集合物件中的所有資料。

已延遲

延後執行表示不會在程式碼中宣告查詢的位置執行作業。 只有在列舉查詢變數時,才會執行這項作業,例如,透過使用 foreach 陳述式。 執行查詢的結果取決於執行查詢時的資料來源內容,而不是定義查詢時的資料來源內容。 如果多次列舉查詢變數,則每次都可能會有不同的結果。 幾乎傳回型別為 IEnumerable<T>IOrderedEnumerable<TElement> 的所有標準查詢運算子都會以延遲方式執行。 延遲的執行可提供查詢重複使用的功能,因為每次重複傳回查詢結果時,查詢都會從資料來源擷取更新的資料。 下列程式碼示範延後執行的範例:

foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}

foreach 陳述式也是擷取查詢結果的位置。 例如,在上述查詢中,反覆運算變數 num 會保留傳回序列中的每個值 (一次一個)。

因為查詢變數本身絕不會保留查詢結果,所以您可以重複執行它以擷取更新的資料。 舉例來說,個別的應用程式可能會持續更新資料庫。 在您的應用程式中,您可以建立一個擷取最新資料的查詢,並且每隔一段時間執行一次,以擷取更新的結果。

使用延後執行的查詢運算子可以另外分類為資料流和非資料流。

串流

資料流運算子在產生項目之前不需要讀取所有來源資料。 執行時,資料流運算子會在讀取並產生項目時 (適用時) 於每個來源項目上執行其運算。 除非產生結果項目,否則資料流運算子會繼續讀取來源項目。 這表示可能會讀取多個來源項目,以產生一個結果項目。

非資料流

非資料流運算子在產生結果項目之前必須讀取所有來源資料。 排序或分組這類作業會歸到此分類。 執行時,非資料流查詢運算子會讀取所有來源資料、將其放入資料結構中、執行作業,並產生結果項目。

分類表

下表會根據執行方法來分類每個標準查詢運算子方法。

注意

如果在兩個資料行中標記運算子,則作業包含兩個輸入序列,而且會以不同的方式評估每個序列。 在這些情況下,它一律是參數清單中以延後資料流方式評估的第一個序列。

標準查詢運算子 傳回類型 立即執行 延後資料流執行 延後非資料流執行
Aggregate TSource X
All Boolean X
Any Boolean X
AsEnumerable IEnumerable<T> X
Average 單一數值 X
Cast IEnumerable<T> X
Concat IEnumerable<T> X
Contains Boolean X
Count Int32 X
DefaultIfEmpty IEnumerable<T> X
Distinct IEnumerable<T> X
ElementAt TSource X
ElementAtOrDefault TSource? X
Empty IEnumerable<T> X
Except IEnumerable<T> X X
First TSource X
FirstOrDefault TSource? X
GroupBy IEnumerable<T> X
GroupJoin IEnumerable<T> X X
Intersect IEnumerable<T> X X
Join IEnumerable<T> X X
Last TSource X
LastOrDefault TSource? X
LongCount Int64 X
Max 單一數值、TSourceTResult? X
Min 單一數值、TSourceTResult? X
OfType IEnumerable<T> X
OrderBy IOrderedEnumerable<TElement> X
OrderByDescending IOrderedEnumerable<TElement> X
Range IEnumerable<T> X
Repeat IEnumerable<T> X
Reverse IEnumerable<T> X
Select IEnumerable<T> X
SelectMany IEnumerable<T> X
SequenceEqual Boolean X
Single TSource X
SingleOrDefault TSource? X
Skip IEnumerable<T> X
SkipWhile IEnumerable<T> X
Sum 單一數值 X
Take IEnumerable<T> X
TakeWhile IEnumerable<T> X
ThenBy IOrderedEnumerable<TElement> X
ThenByDescending IOrderedEnumerable<TElement> X
ToArray TSource[] 陣列 X
ToDictionary Dictionary<TKey,TValue> X
ToList IList<T> X
ToLookup ILookup<TKey,TElement> X
Union IEnumerable<T> X
Where IEnumerable<T> X

LINQ 至物件

「LINQ to Objects」是指直接搭配使用 LINQ 查詢和任何 IEnumerableIEnumerable<T> 集合。 您可以使用 LINQ 查詢任何可列舉的集合,例如 List<T>ArrayDictionary<TKey,TValue>。 集合可能是使用者所定義,或是由 .NET API 所傳回的類型。 在 LINQ 方法中,您會撰寫描述所要擷取內容的宣告式程式碼。 LINQ to Objects 提供使用 LINQ 進行程式設計的絕佳簡介。

LINQ 查詢還提供三種超越傳統 foreach 迴圈的主要優點:

  • 它們更加簡潔易懂 (尤其是在篩選多個條件時)。
  • 它們只要使用最少的應用程式程式碼,即可提供強大的篩選、排序及群組功能。
  • 它們只需要一點修改,甚至不用修改,便可以移植到其他資料來源。

您要對資料執行的作業越複雜,就越能體會使用 LINQ 取代傳統反覆項目技術的好處。

將查詢的結果儲存在記憶體中

查詢基本上是如何擷取和組織資料的一組指令。 要求結果中的每個後續項目時,會延遲執行查詢。 當您使用 foreach 逐一查看結果時,會將項目傳回為已存取。 若要評估查詢,並儲存其結果,而不執行 foreach 迴圈,則只要在查詢變數上呼叫下列其中一種方法即可︰

當您儲存查詢結果時,應將傳回的集合物件指派給新變數,如下列範例所示:

List<int> numbers = [1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20];

IEnumerable<int> queryFactorsOfFour =
    from num in numbers
    where num % 4 == 0
    select num;

// Store the results in a new variable
// without executing a foreach loop.
var factorsofFourList = queryFactorsOfFour.ToList();

// Read and write from the newly created list to demonstrate that it holds data.
Console.WriteLine(factorsofFourList[2]);
factorsofFourList[2] = 0;
Console.WriteLine(factorsofFourList[2]);

另請參閱