Chapter 4 - Item 36 : Understand How Query Expressions Map to Method Calls

Effective C# (Covers C# 6.0), (includes Content Update Program): 50 Specific Ways to Improve Your C#, 3rd Edition By Bill Wagner 讀後心得

本節將介紹從簡單至複雜的 Linq query 方法,所有的 query 語法將被編譯器轉譯成 method call;接著才會選擇最適合的呼叫方法(根據型別)。

1. Where, Select.

// query
int[ ] numbers = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

var smallNumbers = from num in numbers
                   where num < 5
                   select num;

// method call, select operartion was optimized away.
var smallNumbers = numbers.Where( n => n < 5 );

由於 where 語句將立刻回傳元素,轉譯成 method call 後 select 將被優化省略。

// query
var allNumbers = from n in numbers
                 select n;

// method call, select opeartion can't be optimized.
var allNumbers = numbers.Select( n => n );

此時 select 將無法被優化。

// query
var smallNumbers = from n in numbers
                   where n < 5
                   select n * n;

// method call
var smallNumbers = numbers.Where( n => n < 5 ).
    Select( n => n * n );

select 回傳值透過自定義運算,同樣也無法優化省略。
    
而 select 較常應用於回傳匿名物件集合,回傳值型別可以彈性運用。

// query
var squares = from n in numbers
              select new
              {
                  Number = n,
                  Square = n * n
              };

// method call
var squares = numbers.Select( n =>
                new
                {
                    Number = n,
                    Square = n * n
                } );

2. OrderBy, ThenBy, OrderByDescending, ThenByDescending.  

var employees = new List<Employee>( );

// query
var people = from e in employees
             where e.Age > 30
             orderby e.LastName, e.FirstName, e.Age
             select e;

// method call
people = employees.Where( e => e.Age > 30 ).
    OrderBy( e => e.LastName ).
    ThenBy( e => e.FirstName ).
    ThenBy( e => e.Age );

取得的集合將依照所有 Employee Age 大於 30 的情況排序。先比對 LastName,若相同則接著比對 FirstName,再相同則比對 Age。
    
需注意以下的 query 會有完全不同的行為。 

// Not correct. Sorts the entire sequence three times.
people = from e in employees
         where e.Age > 30
         orderby e.LastName
         orderby e.FirstName
         orderby e.Age
         select e;

此 query 將會依照 LastName, FirstName, Age 依序完整排序,也就是集合執行了三次完整的排序!
    
有時我們也需要降冪排序,在 query 寫法中也很容易做到。

// seperate queries
people = from e in employees
         where e.Age > 30
         orderby e.LastName descending, e.FirstName, e.Age
         select e;

此時 LastName 將降冪排序,若發現相同會繼續比較 FirstName, Age。

Note:ThenBy 需接續 OrderBy 或 ThenBy,否則會編譯錯誤。   

3. GroupBy.

GroupBy 用來將物件依特定屬性作為鍵值分類集合。

var employees = new List<Employee>( );

var results = from e in employees
              group e by e.Department into d
              select new
              {
                  Department = d.Key,
                  Size = d.Count( )
              };

// First, query was translated into a nested query.
results = from d in from e in employees group e by e.Department
          select new
          {
              Department = d.Key,
              Size = d.Count( )
          };

// method call
results = employees.GroupBy( e => e.Department ).
    Select( e => new
    {
        Department = e.Key,
        Size = e.Count( )
    } );

此 query 將 Employee Department 作為鍵值,select 回傳匿名物件有兩個屬性,分別為:鍵值(Department)、符合對應鍵值的 Employee 數量(Size)。
    
query 也可以回傳目標鍵值的集合枚舉:    

// Get Employees in each group
var groupResults = from e in employees
                   group e by e.Department into d
                   select new
                   {
                       Department = d.Key,
                       Employees = d.AsEnumerable( )
                   };

// method call
groupResults = employees.GroupBy( e => e.Department ).
    Select( e => new
    {
        Department = e.Key,
        Employees = e.AsEnumerable( )
    } );

4. SelectMany, Join, GroupJoin.

SelectMany :   

int[ ] odds = { 1, 3, 5, 7 };
int[ ] evens = { 2, 4, 6, 8 };

// query
var values = from oddNumber in odds
             from evenNumber in evens
             select new
             {
                 oddNumber,
                 evenNumber,
                 Sum = oddNumber + evenNumber
             };

// flatten query by SelectMany
var values = odds.SelectMany( oddNumber => evens,
    ( oddNumber, evenNumber ) =>
    new
    {
        oddNumber,
        evenNumber,
        Sum = oddNumber + evenNumber
    } );

// query
var values = from oddNumber in odds
             from evenNumber in evens
             where oddNumber > evenNumber
             select new
             {
                 oddNumber,
                 evenNumber,
                 Sum = oddNumber + evenNumber
             };

// full query is translated into this statement.
var values = odds.SelectMany( oddNumber => evens,
    ( oddNumber, evenNumber ) =>
    new
    {
        oddNumber,
        evenNumber
    } ).
    Where( pair => pair.oddNumber > pair.evenNumber ).
    Select( pair =>
    new
    {
        pair.oddNumber,
        pair.evenNumber,
        Sum = pair.oddNumber + pair.evenNumber
    } );

當遇到三個以上的集合時,同樣利用 SelectMany 方法組合。

// query
var triples = from n in new int[ ] { 1, 2, 3 }
              from s in new string[ ] { "one", "two", "three" }
              from r in new string[ ] { "I", "II", "III" }
              select new
              {
                  Arabic = n,
                  Word = s,
                  Roman = r
              };

// method call
var numbers = new int[ ] { 1, 2, 3 };
var words = new string[ ] { "one", "two", "three" };
var raomanNumerals = new string[ ] { "I", "II", "III" };

var triples = numbers.SelectMany( n => words,
    ( n, s ) =>
    new
    {
        n,
        s
    } ).
    SelectMany( pair => raomanNumerals,
    ( pair, r ) =>
    new
    {
        Arabic = pair.n,
        Word = pair.s,
        Roman = r
    } );

Join : 

var numbers = new int[ ] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var labels = new string[ ] { "0", "0", "2", "3", "4", "5", "6", "7", "8", "9" };

// query
var query = from num in numbers
            join label in labels on num.ToString( ) equals label
            select new
            {
                num,
                label
            };

// method call
var query = numbers.Join( labels, num =>
    num.ToString( ),
    label => label,
    ( num, label ) =>
    new
    {
        num,
        label
    } );

此 query 將輸出在 labels 所有元素中,符合 num.ToString( ) == label 的匿名物件,在此例 { num = 0, label = "0" } 將會有兩個元素,num = 1 的情況由於找不到符合元素將不存在輸出集合中。
    
GroupJoin :

public class Project
{
    public int Id { get; set; }
}

public class Task
{
    public Project Parent { get; set; }
}

var projects = new List<Project>( );
var tasks = new Tasks( );

// query
var groups = from p in projects
             join t in tasks on p equals t.Parent
             into projectTasks
             select new
             {
                 Project = p,
                 projectTasks
             };

// method call
var groups = projects.GroupJoin( tasks,
    p => p, t => t.Parent, ( p, projectTasks ) =>
        new
        {
            Project = p,
            TaskList = projectTasks
        } );

此 query 將 tasks 元素裡的 Project 屬性與 Projects 中元素做比對,回傳的匿名物件屬性包括:Project, 符合 Task.Project == Project 的 Task 集合。   

結論:
1. 了解 query 如何轉譯成 method call。

2. 底層提供的 query 與 method call 已可滿足大部分需求;若需自定義 query,需留意 query 與對應的 method call 是否行為一致。