查詢運算式基本概念

本文介紹與 C# 中查詢運算式相關的基本概念。

什麼是查詢?它有哪些功能?

「查詢」是一組指令,描述要從一個或多個指定資料來源擷取的資料以及所傳回資料應該具有的組織結構和組織。 查詢與其產生的結果不同。

一般而言,來源資料也會以邏輯方式組織成一系列相同類型的項目。 例如,SQL 資料庫資料表包含一系列的資料列。 在 XML 檔案中,有一「序列」的 XML 元素 (雖然 XML 元素都是以階層方式組織成樹狀結構)。 記憶體內部集合包含一系列的物件。

從應用程式的觀點來看,原始來源資料的特定類型和結構並不重要。 應用程式一律會將來源資料視為 IEnumerable<T>IQueryable<T> 集合。 例如,在 LINQ to XML 中,來源資料會顯示為 IEnumerable<XElement>。

如果指定此來源序列,則查詢可能會執行三個事項之一:

  • 擷取項目子集,以產生新序列,而不需要修改個別項目。 查詢接著可能會以各種方式排序或分組所傳回的序列,如下列範例所示 (假設 scoresint[]):

    IEnumerable<int> highScoresQuery =
        from score in scores
        where score > 80
        orderby score descending
        select score;
    
  • 擷取上述範例中的一系列項目,但將它們轉換為新類型的物件。 例如,查詢可能只會從數據源中的特定客戶記錄擷取姓氏。 或者,它可能會擷取完整記錄,接著先使用這筆記錄來建構另一個記憶體內部物件類型,或甚至 XML 資料,再產生最終結果序列。 下列範例示範從 intstring 的投影。 請注意 highScoresQuery 的新類型。

    IEnumerable<string> highScoresQuery2 =
        from score in scores
        where score > 80
        orderby score descending
        select $"The score is {score}";
    
  • 擷取來源資料的單一值,例如︰

    • 符合特定條件的項目數。

    • 具有最大或最小值的項目。

    • 符合條件或所指定項目集中特定值總和的第一個項目。 例如,下列查詢會傳回 scores 整數陣列中大於 80 的分數︰

      var highScoreCount = (
          from score in scores
          where score > 80
          select score
      ).Count();
      

      在上述範例中,請記住使用括號括住查詢運算式,再呼叫 Enumerable.Count 方法。 您也可以使用新的變數來儲存具體結果。

      IEnumerable<int> highScoresQuery3 =
          from score in scores
          where score > 80
          select score;
      
      var scoreCount = highScoresQuery3.Count();
      

在上述範例中,在呼叫 Count 時執行查詢,因為 Count 必須逐一查看結果,才能判斷 highScoresQuery 所傳回的項目數。

什麼是查詢運算式?

「查詢運算式」是以查詢語法表示的查詢。 查詢運算式是第一類語言建構。 它就像任何其他運算式一樣,可以用於 C# 運算式有效的任何內容。 查詢運算式包含以 SQL 或 XQuery 類似的宣告式語法所撰寫的一組子句。 每個子句接著會包含一個或多個 C# 運算式,而且這些運算式本身可能是查詢運算式或包含查詢運算式。

查詢運算式的開頭必須是 from 子句,結尾則必須是 selectgroup 子句。 在第一個 from 子句與最後一個 selectgroup 子句之間,它可以包含下列其中一個或多個選擇性子句︰whereorderbyjoinlet,甚至是額外的 from 子句。 您也可以使用 into 關鍵字,讓 joingroup 子句的結果作為相同查詢運算式中其他查詢子句的來源。

查詢變數

在 LINQ 中,查詢變數是儲存「查詢」 而非查詢「結果」 的任何變數。 更具體來說,在 foreach 陳述式中逐一查看查詢變數或直接呼叫其 IEnumerator.MoveNext() 方法時,查詢變數一律是將產生一序列元素的可列舉類型。

注意

本文中的範例會使用下列數據源和範例數據。

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)])
];

下列程式碼範例示範簡單查詢運算式,內含一個資料來源、一個篩選子句、一個排序子句,而且不需轉換來源項目。 select 子句會結束查詢。

// Data source.
int[] scores = [90, 71, 82, 93, 75, 82];

// Query Expression.
IEnumerable<int> scoreQuery = //query variable
    from score in scores //required
    where score > 80 // optional
    orderby score descending // optional
    select score; //must end with select or group

// Execute the query to produce the results
foreach (var testScore in scoreQuery)
{
    Console.WriteLine(testScore);
}

// Output: 93 90 82 82

在上述範例中,scoreQuery 是「查詢變數」,這有時指的就是「查詢」。 查詢變數不會儲存 foreach 迴圈中所產生的任何實際結果資料。 執行 foreach 陳述式時,透過查詢變數 scoreQuery 不會傳回查詢結果。 而是會透過反覆運算變數 testScore 傳回。 可以在第二個 foreach 迴圈中逐一查看 scoreQuery 變數。 只要未修改過它或資料來源,就會產生相同的結果。

查詢變數可能會儲存以查詢語法、方法語法或兩者組合表示的查詢。 在下列範例中,queryMajorCitiesqueryMajorCities2 都是查詢變數︰

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)
];

//Query syntax
IEnumerable<City> queryMajorCities =
    from city in cities
    where city.Population > 100000
    select city;

// Execute the query to produce the results
foreach (City city in queryMajorCities)
{
    Console.WriteLine(city);
}

// Output:
// City { Population = 120000 }
// City { Population = 112000 }
// City { Population = 150340 }

// Method-based syntax
IEnumerable<City> queryMajorCities2 = cities.Where(c => c.Population > 100000);

另一方面,下列兩個範例則是示範並非查詢變數的變數 (即使每個變數都是使用查詢進行初始化)。 它們會儲存結果,因此不是查詢變數:

var highestScore = (
    from score in scores
    select score
).Max();

// or split the expression
IEnumerable<int> scoreQuery =
    from score in scores
    select score;

var highScore = scoreQuery.Max();
// the following returns the same result
highScore = scores.Max();
var largeCitiesList = (
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city
).ToList();

// or split the expression
IEnumerable<City> largeCitiesQuery =
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city;
var largeCitiesList2 = largeCitiesQuery.ToList();

查詢變數的明確和隱含類型

這份文件通常會提供查詢變數的明確類型,以顯示查詢變數與 select 子句之間的類型關聯性。 不過,您也可以使用 var關鍵字,指示編譯器在編譯時期推斷查詢變數 (或任何其他區域變數) 的類型。 例如,也可以使用隱含類型來表示本文章先前所顯示的查詢範例:

var queryCities =
    from city in cities
    where city.Population > 100000
    select city;

在上述範例中,var 的使用是選擇性的。 無論是隱含或是明確類型,queryCities 都是 IEnumerable<City>

啟動查詢運算式

查詢運算式的開頭必須是 from 子句。 它會同時指定資料來源和範圍變數。 範圍變數代表正在周遊來源序列時來源序列中的每個連續項目。 根據資料來源中的項目類型,範圍變數是強類型。 在下列範例中,因為 countriesCountry 物件陣列,所以範圍變數的類型也是 Country。 因為範圍變數是強類型,所以您可以使用點運算子來存取該類型的任何可用成員。

IEnumerable<Country> countryAreaQuery =
    from country in countries
    where country.Area > 500000 //sq km
    select country;

除非使用分號或 continuation 子句結束查詢,否則範圍變數會在範圍內。

查詢運算式可能會包含多個 from 子句。 來源序列中的每個元素本身就是集合或包含集合時,請使用其他 from 子句。 例如,假設您有 Country 物件集合,各包含名為 CitiesCity 物件。 若要查詢每個 Country 中的 City 物件,請使用兩個 from 子句,如下所示︰

IEnumerable<City> cityQuery =
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city;

如需詳細資訊,請參閱 from 子句

結束查詢運算式

查詢運算式的結尾必須是 group 子句或 select 子句。

group 子句

使用 group 子句,產生依所指定的索引鍵所組織的一系列群組。 索引鍵可以是任何資料類型。 例如,下列查詢會建立包含一或多個 Country 物件的群組序列,其索引鍵是 char 類型且值為國家/地區名稱的第一個字母。

var queryCountryGroups =
    from country in countries
    group country by country.Name[0];

如需分組的詳細資訊,請參閱 group 子句

select 子句

使用 select 子句來產生所有其他類型的序列。 簡單 select 子句只會產生一系列相同類型的物件,作為資料來源中所包含的物件。 在此範例中,資料成員包含 Country 物件。 orderby 子句只會將項目排序為新順序,而 select 子句會產生一系列的已排序 Country 物件。

IEnumerable<Country> sortedQuery =
    from country in countries
    orderby country.Area
    select country;

select 子句可以用來將來源資料轉換為新類型的序列。 這項轉換也稱為「投影」。 在下列範例中,select 子句會「投影」一序列的匿名型別,只包含原始元素中欄位的子集。 使用物件初始設定式,可以初始化新物件。

var queryNameAndPop =
    from country in countries
    select new
    {
        Name = country.Name,
        Pop = country.Population
    };

因此在此範例中,因為查詢會產生匿名型別,所以 var 是必要的。

如需 select 子句可用來轉換來源資料之所有方式的詳細資訊,請參閱 select 子句

into 的接續

您可以在 selectgroup子句中使用 into 關鍵字,以建立可儲存查詢的暫存識別碼。 必須在分組或選取作業之後對查詢執行其他查詢作業時,請使用 into 子句。 在下列範例中,countries 會根據 1 千萬範圍中的個體進行分組。 建立這些群組之後,其他子句會篩選掉部分群組,接著依遞增順序排序群組。 若要執行這些其他作業,則需要 countryGroup 所代表的接續。

// percentileQuery is an IEnumerable<IGrouping<int, Country>>
var percentileQuery =
    from country in countries
    let percentile = (int)country.Population / 10_000_000
    group country by percentile into countryGroup
    where countryGroup.Key >= 20
    orderby countryGroup.Key
    select countryGroup;

// grouping is an IGrouping<int, Country>
foreach (var grouping in percentileQuery)
{
    Console.WriteLine(grouping.Key);
    foreach (var country in grouping)
    {
        Console.WriteLine(country.Name + ":" + country.Population);
    }
}

如需詳細資訊,請參閱 into

篩選、排序和聯結

在開始 from 子句與結束 selectgroup 子句之間,所有其他子句 (wherejoinorderbyfromlet) 都是選擇性的。 在查詢主體中,可能不會使用任何選擇性子句或使用多次。

where 子句

使用 where 子句,會根據一個或多個述詞運算式來篩選掉來源資料中的項目。 下列範例中的 where 子句會有一個具有兩個條件的述詞。

IEnumerable<City> queryCityPop =
    from city in cities
    where city.Population is < 200000 and > 100000
    select city;

如需詳細資訊,請參閱 where 子句

orderby 子句

使用 orderby 子句,依遞增或遞減順序來排序結果。 您也可以指定次要排序順序。 下列範例會使用 Area 屬性,以對 country 物件執行主要排序。 它接著會使用 Population 屬性來執行次要排序。

IEnumerable<Country> querySortedCountries =
    from country in countries
    orderby country.Area, country.Population descending
    select country;

ascending 是選擇性關鍵字;如果未指定任何順序,則為預設排序順序。 如需詳細資訊,請參閱 orderby 子句

join 子句

使用 join 子句,會根據每個項目中所指定索引鍵之間的相等比較來建立某個資料來源中的項目與另一個資料來源中的項目的關聯和 (或) 將它們合併。 在 LINQ 中,會對項目為不同類型的物件序列執行聯結作業。 聯結兩個序列之後,必須使用 selectgroup 陳述式來指定要儲存在輸出序列中的元素。 您也可以使用匿名類型,將每個相關聯項目集的屬性合併到輸出序列的新類型。 下列範例會關聯 prod 物件,而其 Category 屬性符合 categories 字串陣列中的其中一個分類。 會篩選掉 Category 不符合 categories 中任何字串的產品。select 陳述式會投影其屬性取自 catprod 的新類型。

var categoryQuery =
    from cat in categories
    join prod in products on cat equals prod.Category
    select new
    {
        Category = cat,
        Name = prod.Name
    };

您也可以使用 into 關鍵字將 join 作業的結果儲存到暫存變數,來執行群組聯結。 如需詳細資訊,請參閱 join 子句

let 子句

使用 let 子句,將運算式的結果 (例如方法呼叫) 儲存在新的範圍變數中。 在下列範例中,範圍變數 firstName 會儲存 Split 所傳回的字串陣列的第一個元素。

string[] names = ["Svetlana Omelchenko", "Claire O'Donnell", "Sven Mortensen", "Cesar Garcia"];
IEnumerable<string> queryFirstNames =
    from name in names
    let firstName = name.Split(' ')[0]
    select firstName;

foreach (var s in queryFirstNames)
{
    Console.Write(s + " ");
}

//Output: Svetlana Claire Sven Cesar

如需詳細資訊,請參閱 let 子句

查詢運算式中的子查詢

查詢子句本身可能會包含查詢運算式,有時稱為「子查詢」。 每個子查詢的開頭都會是它自己的 from 子句,而子句不一定會指向第一個 from 子句中的相同資料來源。 例如,下列查詢示範用於 select 陳述式以擷取分組作業結果的查詢運算式。

var queryGroupMax =
    from student in students
    group student by student.Year into studentGroup
    select new
    {
        Level = studentGroup.Key,
        HighestScore = (
            from student2 in studentGroup
            select student2.ExamScores.Average()
        ).Max()
    };

如需詳細資訊,請參閱在分組作業上執行子查詢

另請參閱