共用方式為


撰寫 C# LINQ 查詢以查詢資料

入門語言整合查詢(LINQ)文件中的大多數查詢都使用 LINQ 宣告式查詢語法。 C# 編譯程式會將查詢語法轉譯成方法呼叫。 這些方法的呼叫實作標準查詢運算子。 它們有像是 WhereSelectGroupByJoinMaxAverage 這樣的名字。 你可以直接用方法語法來呼叫它們,而不是查詢語法。

查詢語法和方法語法在語意上相同,但查詢語法通常更簡單且更容易閱讀。 你必須將部分查詢表達為方法呼叫。 例如,您必須使用方法呼叫,來表示可擷取符合所指定條件的項目數的查詢。 您也必須針對擷取來源序列中具有最大值的項目的查詢,使用方法呼叫。 System.Linq 命名空間中標準查詢運算子的參考文件一般會使用方法語法。 熟悉如何在查詢和查詢表達式中使用方法語法。

標準查詢運算子擴充方法

下列範例示範簡單「查詢運算式」以及撰寫為「方法查詢」的語意對等查詢。

int[] numbers = [ 5, 10, 8, 3, 6, 12 ];

//Query syntax:
IEnumerable<int> numQuery1 =
    from num in numbers
    where num % 2 == 0
    orderby num
    select num;

//Method syntax:
IEnumerable<int> numQuery2 = numbers
    .Where(num => num % 2 == 0)
    .OrderBy(n => n);

foreach (int i in numQuery1)
{
    Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
foreach (int i in numQuery2)
{
    Console.Write(i + " ");
}

這兩個範例的輸出完全相同。 查詢變數的類型在這兩種形式中都相同:IEnumerable<T>

在運算式的右側,請注意 where 子句現在會在 numbers 物件 (其類型為 IEnumerable<int>) 上被表示為執行個體方法。 如果您熟悉泛型 IEnumerable<T> 介面,就會知道它沒有 Where 方法。 不過,如果您在 Visual Studio IDE 中叫用 IntelliSense 完成清單,您不只會看到 Where 方法,還會看到許多其他方法 (例如 SelectSelectManyJoinOrderby)。 這些方法會實作標準查詢運算子。

顯示 Intellisense 中所有標準查詢運算子的螢幕擷取畫面。

雖然 IEnumerable<T> 看起來似乎包含更多方法,但事實上並沒有。 標準查詢運算子會實作為擴充方法。 延伸成員會「擴展」已存在的型別,它們可以像該型別的成員一樣被調用。 標準的查詢運算子會擴展 IEnumerable<T>,這就是為什麼你可以寫成 numbers.Where(...)。 您可以在呼叫擴充功能之前,先使用 using 指示詞將擴充功能帶入範圍。

欲了解更多關於延伸成員的資訊,請參閱 延伸成員。 如需標準查詢運算子的詳細資訊,請參閱標準查詢運算子概觀 (C#)。 部分 LINQ 提供者,例如 Entity Framework 和 LINQ to XML,不僅實作了其標準查詢運算子,還針對其他類型除了 IEnumerable<T> 外的擴充成員。

Lambda 運算式

在上述範例中,條件表達式(num % 2 == 0)會以行內引數的形式傳遞至 Enumerable.Where 方法:Where(num => num % 2 == 0). 此行內表達式是 lambda 表達式。 這是一種撰寫程式碼的便捷方法,否則必須以更繁瑣的形式編寫程式碼。 運算子左側的 num 是輸入變數,其對應到查詢運算式中的 num。 編譯器可以推斷 num 類型,因為它知道 numbers 是泛型 IEnumerable<T> 類型。 Lambda 主體與查詢語法中的運算式或任何其他 C# 運算式或語句的表達方式相同。 它可以包含方法呼叫和其他複雜的邏輯。 傳回值是表達式結果。 你只能用方法語法表達某些查詢,而其中一些查詢需要 lambda 表達式。 Lambda 運算式是 LINQ 工具箱中功能強大且彈性的工具。

查詢的編寫性

在上述程式碼範例中,使用小數點運算子於呼叫 Enumerable.OrderBy時來叫用 Where 方法。 Where 會產生篩選的序列,然後 Orderby 會排序 Where 所產生的序列。 因為查詢會傳回 IEnumerable,所以您可以將方法呼叫鏈結在一起,以在方法語法中撰寫它們。 當您使用查詢語法撰寫查詢時,編譯器會執行此撰寫。 因為查詢變數不會儲存查詢的結果,所以您隨時都可以修改它或使用它做為新查詢的基礎 (即使在執行它之後也一樣)。

下列範例示範一些基本 LINQ 查詢,使用之前提到的每種方法。

注意

這些查詢作用於記憶體中的集合;然而,其語法與 LINQ 對實體及 LINQ 對 XML 的語法相同。

範例 - 查詢語法

大多數查詢都用 查詢語法 來建立 查詢表達式。 下列範例示範三個查詢運算式。 第一個查詢運算式示範如何使用 where 子句套用條件來篩選或限制結果。 它會傳回值大於 7 或小於 3 的來源序列中的所有項目。 第二個運算式示範如何排序傳回的結果。 第三個運算式示範如何根據索引鍵來分組結果。 此查詢會根據單字的第一個字母來傳回兩個群組。

List<int> numbers = [ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 ];

// The query variables can also be implicitly typed by using var

// Query #1.
IEnumerable<int> filteringQuery =
    from num in numbers
    where num is < 3 or > 7
    select num;

// Query #2.
IEnumerable<int> orderingQuery =
    from num in numbers
    where num is < 3 or > 7
    orderby num ascending
    select num;

// Query #3.
string[] groupingQuery = ["carrots", "cabbage", "broccoli", "beans", "barley"];
IEnumerable<IGrouping<char, string>> queryFoodGroups =
    from item in groupingQuery
    group item by item[0];

查詢類型是 IEnumerable<T>。 所有這些查詢都可以使用 var 撰寫,如以下範例所示:

var query = from num in numbers...

在每個上述範例中,除非您逐一查看 foreach 陳述式或其他陳述式中的查詢變數,否則不會實際執行查詢。

範例 - 方法語法

你必須將某些查詢操作表達為方法呼叫。 最常見的此類方法是傳回單一數值的方法,例如 SumMaxMinAverage 等等。 在任何查詢中,這些方法都放在最後,因為它們回傳單一值,無法作為更多查詢操作的來源。 下列範例示範查詢運算式中的方法呼叫:

List<int> numbers1 = [ 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 ];
List<int> numbers2 = [ 15, 14, 11, 13, 19, 18, 16, 17, 12, 10 ];

// Query #4.
double average = numbers1.Average();

// Query #5.
IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);

若方法有 System.Action 或 參數 System.Func<TResult> ,則以 lambda 表達式形式提供這些參數,如下範例所示:

// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);

在先前的查詢中,只有查詢 #4 會立即執行,因為它回傳的是單一值,而非一般 IEnumerable<T> 集合。 方法本身會使用 foreach 或類似的程式碼來計算其值。

你可以用隱式型別(含式型別) var來撰寫上述每一個查詢,如下範例所示:

// var is used for convenience in these queries
double average = numbers1.Average();
var concatenationQuery = numbers1.Concat(numbers2);
var largeNumbersQuery = numbers2.Where(c => c > 15);

範例 - 混合查詢與方法語法

這個範例示範如何在查詢子句結果上使用方法語法。 將查詢表達式置於括號內,然後套用點運算子並呼叫該方法。 在下列範例中,查詢 #7 會傳回其值介於 3 與 7 之間的數字計數。

// Query #7.

// Using a query expression with method syntax
var numCount1 = (
    from num in numbers1
    where num is > 3 and < 7
    select num
).Count();

// Better: Create a new variable to store
// the method call result
IEnumerable<int> numbersQuery =
    from num in numbers1
    where num is > 3 and < 7
    select num;

var numCount2 = numbersQuery.Count();

因為查詢 #7 會傳回單一值,而不是集合,所以會立即執行查詢。

你可以使用含var的隱式類型來寫出前一個查詢,如下:

var numCount = (from num in numbers...

你可以用方法語法寫成如下:

var numCount = numbers.Count(n => n is > 3 and < 7);

你可以使用一種明確類型的方法來寫,如下:

int numCount = numbers.Count(n => n is > 3 and < 7);

在執行階段動態指定述詞篩選

在某些情況下,您要到執行階段才知道在 where 子句中必須套用多少述詞至來源項目。 動態指定多個述詞篩選的其中一個方式是使用 Contains 方法,如下列範例所示。 查詢會根據執行查詢時的 id 值而傳回不同的結果。

int[] ids = [ 111, 114, 112 ];

var queryNames = from student in students
                 where ids.Contains(student.ID)
                 select new
                 {
                     student.LastName,
                     student.ID
                 };

foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Garcia: 114
    O'Donnell: 112
    Omelchenko: 111
 */

// Change the ids.
ids = [ 122, 117, 120, 115 ];

// The query will now return different results
foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Adams: 120
    Feng: 117
    Garcia: 115
    Tucker: 122
 */

注意

此範例使用下列資料來源和數據:

record City(string Name, long Population);
record Country(string Name, double Area, long Population, List<City> Cities);
record Product(string Name, string Category);
static readonly City[] cities = [
    new City("Tokyo", 37_833_000),
    new City("Delhi", 30_290_000),
    new City("Shanghai", 27_110_000),
    new City("São Paulo", 22_043_000),
    new City("Mumbai", 20_412_000),
    new City("Beijing", 20_384_000),
    new City("Cairo", 18_772_000),
    new City("Dhaka", 17_598_000),
    new City("Osaka", 19_281_000),
    new City("New York-Newark", 18_604_000),
    new City("Karachi", 16_094_000),
    new City("Chongqing", 15_872_000),
    new City("Istanbul", 15_029_000),
    new City("Buenos Aires", 15_024_000),
    new City("Kolkata", 14_850_000),
    new City("Lagos", 14_368_000),
    new City("Kinshasa", 14_342_000),
    new City("Manila", 13_923_000),
    new City("Rio de Janeiro", 13_374_000),
    new City("Tianjin", 13_215_000)
];

static readonly Country[] countries = [
    new Country ("Vatican City", 0.44, 526, [new City("Vatican City", 826)]),
    new Country ("Monaco", 2.02, 38_000, [new City("Monte Carlo", 38_000)]),
    new Country ("Nauru", 21, 10_900, [new City("Yaren", 1_100)]),
    new Country ("Tuvalu", 26, 11_600, [new City("Funafuti", 6_200)]),
    new Country ("San Marino", 61, 33_900, [new City("San Marino", 4_500)]),
    new Country ("Liechtenstein", 160, 38_000, [new City("Vaduz", 5_200)]),
    new Country ("Marshall Islands", 181, 58_000, [new City("Majuro", 28_000)]),
    new Country ("Saint Kitts & Nevis", 261, 53_000, [new City("Basseterre", 13_000)])
];

您可以使用 if... elseswitch 這類控制流程陳述式,以在預先決定的替代查詢之間進行選取。 在下列範例中,如果 studentQuery 的執行階段值為 whereoddYear,則 true 會使用不同的 false 子句。

void FilterByYearType(bool oddYear)
{
    IEnumerable<Student> studentQuery = oddYear
        ? (from student in students
           where student.Year is GradeLevel.FirstYear or GradeLevel.ThirdYear
           select student)
        : (from student in students
           where student.Year is GradeLevel.SecondYear or GradeLevel.FourthYear
           select student);
    var descr = oddYear ? "odd" : "even";
    Console.WriteLine($"The following students are at an {descr} year level:");
    foreach (Student name in studentQuery)
    {
        Console.WriteLine($"{name.LastName}: {name.ID}");
    }
}

FilterByYearType(true);

/* Output:
    The following students are at an odd year level:
    Fakhouri: 116
    Feng: 117
    Garcia: 115
    Mortensen: 113
    Tucker: 119
    Tucker: 122
 */

FilterByYearType(false);

/* Output:
    The following students are at an even year level:
    Adams: 120
    Garcia: 114
    Garcia: 118
    O'Donnell: 112
    Omelchenko: 111
    Zabokritski: 121
 */

處理查詢運算式中的 Null 值

本例示範如何處理來源集合中可能有的 Null 值。 如 IEnumerable<T> 的物件集合,可以包含值為 null 的元素。 如果來源集合為 null 或包含其值為 null 的元素,而且您的查詢不處理 null 值,則當您執行查詢時會擲回 NullReferenceException

下列範例會使用這些類型和靜態資料陣列:

record Product(string Name, int CategoryID);
record Category(string Name, int ID);
static Category?[] categories =
[
    new ("brass", 1),
    null,
    new ("winds", 2),
    default,
    new ("percussion", 3)
];

static Product?[] products =
[
    new Product("Trumpet", 1),
    new Product("Trombone", 1),
    new Product("French Horn", 1),
    null,
    new Product("Clarinet", 2),
    new Product("Flute", 2),
    null,
    new Product("Cymbal", 3),
    new Product("Drum", 3)
];

您可以謹慎撰寫程式碼以避免發生 Null 參考例外狀況,如下例所示︰

var query1 = from c in categories
             where c != null
             join p in products on c.ID equals p?.CategoryID
             select new
             {
                 Category = c.Name,
                 Name = p.Name
             };

在上例中,where 子句會篩選掉類別序列中的所有 Null 項目。 這項技術不影響 join 子句中的 Null 檢查。 因為 Products.CategoryIDint? 類型 (即 Nullable<int> 的速記),所以具有 Null 的條件運算式在此範例中可以運作。

在 join 子句中,如果僅有一個比較索引鍵是可為 Null 的實值型別,則可以在查詢運算式中將其他的比較索引鍵轉換成可為 Null 的型別。 在下列範例中,假設 EmployeeID 是包含 int? 類型值的資料行:

var query =
    from o in db.Orders
    join e in db.Employees
        on o.EmployeeID equals (int?)e.EmployeeID
    select new { o.OrderID, e.FirstName };

在每個範例中,都會使用 equals 查詢關鍵字。 您也可以使用模式比對,其中包含 is nullis not null 的模式。 LINQ 查詢中不建議使用這些模式,因為查詢提供者可能無法正確解譯新的 C# 語法。 查詢提供者是一種程式庫,可將 C# 查詢運算式轉譯成原生資料格式,例如 Entity Framework Core。 查詢提供者會實作 System.Linq.IQueryProvider 介面,以建立實作 System.Linq.IQueryable<T> 介面的資料來源。

處理查詢運算式中的例外狀況

你可以在查詢表達式的情境中呼叫任何方法。 請勿在查詢運算式中呼叫任何可能產生副作用 (例如修改資料來源的內容或擲回例外狀況) 的方法。 此範例顯示如何避免在查詢運算式中呼叫方法時引發例外狀況,卻不違反處理例外狀況的一般 .NET 方針。 這些指引指出,當你了解為什麼在特定情境中出現特定例外時,處理該例外是可以接受的。 如需詳細資訊,請參閱例外狀況的最佳做法

最後一個範例顯示如何處理這種當您在查詢執行期間必須擲回例外狀況的情況。

下例示範如何將例外狀況處理程式碼移到查詢運算式之外。 只有當方法不依賴查詢本地的變數時,才能用這種方式重構。 處理查詢運算式以外的例外狀況比較容易。

// A data source that is very likely to throw an exception!
IEnumerable<int> GetData() => throw new InvalidOperationException();

// DO THIS with a datasource that might
// throw an exception.
IEnumerable<int>? dataSource = null;
try
{
    dataSource = GetData();
}
catch (InvalidOperationException)
{
    Console.WriteLine("Invalid operation");
}

if (dataSource is not null)
{
    // If we get here, it is safe to proceed.
    var query = from i in dataSource
                select i * i;

    foreach (var i in query)
    {
        Console.WriteLine(i.ToString());
    }
}

catch (InvalidOperationException) 前述範例的區塊中,請以適合你應用的方式處理(或不處理)例外。

在某些情況下,對於查詢中拋出的例外,最好的回應可能是立即停止查詢執行。 下例示範如何處理可能從查詢主體內擲回的例外狀況。 假設 SomeMethodThatMightThrow 可能造成需要停止執行查詢的例外狀況。

try 區塊會括住 foreach 迴圈,不是括住查詢本身。 foreach 迴圈是執行查詢的點。 執行查詢時會擲回執行階段例外狀況。 因此,請在迴圈中 foreach 處理他們。

// Not very useful as a general purpose method.
string SomeMethodThatMightThrow(string s) =>
    s[4] == 'C' ?
        throw new InvalidOperationException() :
        $"""C:\newFolder\{s}""";

// Data source.
string[] files = ["fileA.txt", "fileB.txt", "fileC.txt"];

// Demonstration query that throws.
var exceptionDemoQuery = from file in files
                         let n = SomeMethodThatMightThrow(file)
                         select n;

try
{
    foreach (var item in exceptionDemoQuery)
    {
        Console.WriteLine($"Processing {item}");
    }
}
catch (InvalidOperationException e)
{
    Console.WriteLine(e.Message);
}

/* Output:
    Processing C:\newFolder\fileA.txt
    Processing C:\newFolder\fileB.txt
    Operation is not valid due to the current state of the object.
 */

捕捉你預期會引發的例外情況,並在finally區塊中執行必要的清理。

另請參閱