︿
Top

2016年2月19日 星期五

C# LINQ: SelectMany() 擴充方法

緣起

最近在研讀 ASP.NET MVC 5 : 網站開發美學 Ch.03 時, 發現對於 SelectMany() 這個函式仍然不是很瞭解, 於是查了 MSDN 上的範例, 再加上配合 .NET Framework 源碼 一併進行 debug, 終於有了一些概念; 故撰寫本文, 以進行整理.

完整範例, 請由此下載.


過程

說明相關程式檔案建立過程, 最後的版本, 可自行下載本範例. 在範例程式碼裡, 都有作了細節的說明, 可以參考.

步驟1

自行建立 MyEnumerable 類別, 將 .NET Framework 源碼中 SelectMany() 相關擴充方法收錄至該類別, 以供除錯時, 得以明確看到程式執行過程.
總共有 3 個 overloading 的擴充方法,
(1) 修改方法名稱, 在每個方法加上 My, 以與 .NET Framework 的源碼的原有方法作區隔
(2) 將有 Error.xxxx 的程式段註解掉,
註: .NET Framework 源碼 連結: ( http://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,8f3471331178bcb0 )

Case 1:

SelectMany() 的參數說明:
// 參數1: this IEnumerable<TSource> source --> 即 IEnumerable<PetOwner>
// 參數2: Func<TSource, IEnumerable<TResult>> selector --> 即 Func<PetOwner, IEnumerable<String>>
// source: PetOwner[] 陣列 (共 3 個元素)
// selector: petOwner => petOwner.Pets, 其中, petOwner 為 PetOwner 類別的物件實體
public static IEnumerable<TResult> MySelectMany<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>> selector)

其實由 SelectMany() 實在看不太出來究竟回傳什麼, 由 SelectManyIterator() 來看, 就清楚多了.
foreach (TResult subElement in selector(element))
{
    yield return subElement;
}
[註]: selector 為一個 delegate: Func<TSource, IEnumerable<TResult>>,
其傳入參數為 (element); 其實作為呼叫端的  petOwner => petOwner.Pets



#region CASE 1 的 SelectMany() 源碼
//參數1: this IEnumerable<TSource> source    --> 即 IEnumerable<PetOwner>
//參數2: Func<TSource, IEnumerable<TResult>> selector --> 即 Func<PetOwner, IEnumerable<String>>
//source: PetOwner[] 陣列 (共 3 個元素)
//selector: petOwner => petOwner.Pets, 其中, petOwner 為 PetOwner 類別的物件實體
public static IEnumerable<TResult> MySelectMany<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>> selector)
{
    //if (source == null) throw Error.ArgumentNull("source");
    //if (selector == null) throw Error.ArgumentNull("selector");
    return MySelectManyIterator<TSource, TResult>(source, selector);
}

static IEnumerable<TResult> MySelectManyIterator<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>> selector)
{
    //source: PetOwner[] 陣列 (共 3 個元素)
    //element: PetOwner[] 陣列裡的每個元素, 即 PetOwner 的物件實體
    //selector: petOwner => petOwner.Pets, 其中, petOwner 即為 element
    foreach (TSource element in source)
    {
        //執行 selector(element) 之後, 會產出 petOwner.Pets 回傳
        foreach (TResult subElement in selector(element))
        {
            yield return subElement;
        }
    }
}
#endregion


Case 2:

SelectMany() 的參數說明:
//參數1: this IEnumerable<TSource> source --> 即 IEnumerable<PetOwner>
//參數2: Func<TSource, int, IEnumerable<TResult>> selector --> 即 Func<PetOwner, int, IEnumerable<String>>
//source: PetOwner[] 陣列 (共 4 個元素)
//selector: (petOwner, index) => petOwner.Pets.Select(pet => index + pet), 其中, petOwner 為 PetOwner 類別的物件實體, index 為 int
public static IEnumerable<TResult> MySelectMany<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, int, IEnumerable<TResult>> selector)

其實由 SelectMany() 實在看不太出來究竟回傳什麼, 由 SelectManyIterator() 來看, 就清楚多了.
foreach (TResult subElement in selector(element, index))
{
    yield return subElement;
}
[註]: selector 為一個 delegate: Func<TSource, int, IEnumerable<TResult>>
其參數為 (element, index); 其實作為呼叫端的 (petOwner, index) => petOwner.Pets.Select(pet => index + pet)



#region  CASE 2 的 SelectMany() 源碼

//參數1: this IEnumerable<TSource> source     --> 即 IEnumerable<PetOwner>
//參數2: Func<TSource, int, IEnumerable<TResult>> selector --> 即 Func<PetOwner, int, IEnumerable<String>>
//source: PetOwner[] 陣列 (共 4 個元素)
//selector: (petOwner, index) => petOwner.Pets.Select(pet => index + pet), 其中, petOwner 為 PetOwner 類別的物件實體, index 為 int
public static IEnumerable<TResult> MySelectMany<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, int, IEnumerable<TResult>> selector)
{
    //  if (source == null) throw Error.ArgumentNull("source");
    //  if (selector == null) throw Error.ArgumentNull("selector");
    return MySelectManyIterator<TSource, TResult>(source, selector);
}

static IEnumerable<TResult> MySelectManyIterator<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, int, IEnumerable<TResult>> selector)
{
    //source: PetOwner[] 陣列 (共 4 個元素)
    //element: PetOwner[] 陣列裡的每個元素, 即 PetOwner 的物件實體
    //selector: (petOwner, index) => petOwner.Pets.Select(pet => index + pet); 其中, petOwner 即為 element, index 為 int
    //  註: index 代表陣列元素的位置, 由 0 起算
    int index = -1;
    foreach (TSource element in source)
    {
        checked { index++; }
        foreach (TResult subElement in selector(element, index))
        {
            yield return subElement;
        }
    }
}
#endregion

Case 3:

SelectMany() 的參數說明:
//參數1: this IEnumerable<TSource> source    --> 即 IEnumerable<PetOwner>
//參數2: Func<TSource, IEnumerable<TCollection>> collectionSelector --> 即 Func<PetOwner, IEnumerable<String>>
//參數3: Func<TSource, TCollection, TResult> resultSelector  --> 即 Func<PetOwner, String, IEnumerable<String>>
//source: PetOwner[] 陣列 (共 4 個元素)
//collectionSelector: petOwner => petOwner.Pets
//resultSelector:  (petOwner, petName) => new { petOwner, petName }
public static IEnumerable<TResult> MySelectMany<TSource, TCollection, TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector)

其實由 SelectMany() 實在看不太出來究竟回傳什麼, 由 SelectManyIterator() 來看, 就清楚多了.
//把 element 傳入 collectionSelector 執行
//petOwner => petOwner.Pets  ---> element => element.Pets
foreach (TCollection subElement in collectionSelector(element))
{
    //把 element, subElement 傳入 resultSelector 執行
    //(petOwner, petName) => new { petOwner, petName } ---> (element, subElement) => new { element, subElement }
    yield return resultSelector(element, subElement);
}
[註]:
(1) collectionSelector 為一個 delegate: Func<TSource, IEnumerable<TCollection>>
其參數為 (element); 其實作為呼叫端的 petOwner => petOwner.Pets
(2) resultSelector 為一個 delegate: Func<TSource, TCollection, TResult>
其參數為 (element, subElement), 其實作為呼叫端的  (petOwner, petName) => new { petOwner, petName }

#region  CASE 3 的 SelectMany() 源碼

//參數1: this IEnumerable<TSource> source        --> 即 IEnumerable<PetOwner>
//參數2: Func<TSource, IEnumerable<TCollection>> collectionSelector  --> 即 Func<PetOwner, IEnumerable<String>>
//參數3: Func<TSource, TCollection, TResult> resultSelector    --> 即 Func<PetOwner, String, IEnumerable<String>>
//source: PetOwner[] 陣列 (共 4 個元素)
//collectionSelector: petOwner => petOwner.Pets
//resultSelector:  (petOwner, petName) => new { petOwner, petName }
public static IEnumerable<TResult> MySelectMany<TSource, TCollection, TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector)
{
    // if (source == null) throw Error.ArgumentNull("source");
    // if (collectionSelector == null) throw Error.ArgumentNull("collectionSelector");
    // if (resultSelector == null) throw Error.ArgumentNull("resultSelector");
    return MySelectManyIterator<TSource, TCollection, TResult>(source, collectionSelector, resultSelector);
}

static IEnumerable<TResult> MySelectManyIterator<TSource, TCollection, TResult>(IEnumerable<TSource> source, Func<TSource, IEnumerable<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector)
{
    //source: PetOwner[] 陣列 (共 4 個元素)
    //element: PetOwner[] 陣列裡的每個元素, 即 PetOwner 的物件實體
    //collectionSelector: petOwner => petOwner.Pets
    //resultSelector:  (petOwner, petName) => new { petOwner, petName }
    foreach (TSource element in source)
    {
        //把 element 傳入 collectionSelector 執行 
        //petOwner => petOwner.Pets  ---> element => element.Pets
        foreach (TCollection subElement in collectionSelector(element))
        {
            //把 element, subElement 傳入 resultSelector 執行 
            //(petOwner, petName) => new { petOwner, petName }  ---> (element, subElement) => new { element, subElement }
            yield return resultSelector(element, subElement);
        }
    }
}

#endregion


步驟2

建立 PetOwner 類別

class PetOwner
{
    public string Name { get; set; }
    public List<String> Pets { get; set; }
}


步驟3

在 Program.cs, 加入 3 個 static methods, 並修改 Main() 方法, 以呼叫那 3 個 methods

(1) SelectManyEx1()

public static void SelectManyEx1()
{
    PetOwner[] petOwners =
   { new PetOwner { Name="Higa, Sidney",
      Pets = new List<string>{ "Scruffy", "Sam" } },
     new PetOwner { Name="Ashkenazi, Ronen",
      Pets = new List<string>{ "Walker", "Sugar" } },
     new PetOwner { Name="Price, Vernette",
      Pets = new List<string>{ "Scratches", "Diesel" } } };

    // Query using SelectMany().
    IEnumerable<string> query1 = petOwners.MySelectMany(petOwner => petOwner.Pets);     // 呼叫自行由 .NET Framework 複製過來的擴充方法
    //IEnumerable<string> query1 = petOwners.SelectMany(petOwner => petOwner.Pets);  // 呼叫 .NET Framework 的擴充方法

    Console.WriteLine("Using SelectMany():");

    // Only one foreach loop is required to iterate 
    // through the results since it is a
    // one-dimensional collection.
    // 只需要 1 個 foreach 迴圈, 就可以選取第2層 Pets 的元素資料
    foreach (string pet in query1)
    {
        Console.WriteLine(pet);
    }

    // This code shows how to use Select() 
    // instead of SelectMany().
    IEnumerable<List<String>> query2 =
        petOwners.Select(petOwner => petOwner.Pets);

    Console.WriteLine("\nUsing Select():");

    // Notice that two foreach loops are required to 
    // iterate through the results
    // because the query returns a collection of arrays.
    // 需要 2 個 foreach 迴圈 
    foreach (List<String> petList in query2)
    {
        foreach (string pet in petList)
        {
            Console.WriteLine(pet);
        }
        Console.WriteLine();
    }

    //輸出:
    //Using SelectMany():
    //Scruffy
    //Sam
    //Walker
    //Sugar
    //Scratches
    //Diesel
    //
    //Using Select():
    //Scruffy
    //Sam
    //
    //Walker
    //Sugar
    //
    //Scratches
    //Diesel 
}

(2) SelectManyEx2()

public static void SelectManyEx2()
{
    PetOwner[] petOwners =
   { new PetOwner { Name="Higa, Sidney",
      Pets = new List<string>{ "Scruffy", "Sam" } },
     new PetOwner { Name="Ashkenazi, Ronen",
      Pets = new List<string>{ "Walker", "Sugar" } },
     new PetOwner { Name="Price, Vernette",
      Pets = new List<string>{ "Scratches", "Diesel" } },
     new PetOwner { Name="Hines, Patrick",
      Pets = new List<string>{ "Dusty" } } };

    // Project the items in the array by appending the index 
    // of each PetOwner to each pet's name in that petOwner's 
    // array of pets.
    //
    // IEnumerable<string> query =
    //  petOwners.SelectMany((petOwner, index) =>
    //         petOwner.Pets.Select(pet => index + pet));
    //
    IEnumerable<string> query =
        petOwners.MySelectMany((petOwner, index) =>
                                 petOwner.Pets.Select(pet => index + pet));

    foreach (string pet in query)
    {
        Console.WriteLine(pet);
    }
    Console.WriteLine();

    //輸出:
    //
    // 0Scruffy
    // 0Sam
    // 1Walker
    // 1Sugar
    // 2Scratches
    // 2Diesel
    // 3Dusty 
}

(3) SelectManyEx3()

public static void SelectManyEx3()
{
    PetOwner[] petOwners =
   { new PetOwner { Name="Higa",
      Pets = new List<string>{ "Scruffy", "Sam" } },
     new PetOwner { Name="Ashkenazi",
      Pets = new List<string>{ "Walker", "Sugar" } },
     new PetOwner { Name="Price",
      Pets = new List<string>{ "Scratches", "Diesel" } },
     new PetOwner { Name="Hines",
      Pets = new List<string>{ "Dusty" } } };

    // Project the pet owner's name and the pet's name.
    //
    //var query =
    // petOwners
    // .SelectMany(petOwner => petOwner.Pets, (petOwner, petName) => new { petOwner, petName })
    // .Where(ownerAndPet => ownerAndPet.petName.StartsWith("S"))
    // .Select(ownerAndPet =>
    //   new
    //   {
    //    Owner = ownerAndPet.petOwner.Name,
    //    Pet = ownerAndPet.petName
    //   }
    // );
    //
    var query =
        petOwners
        .MySelectMany(petOwner => petOwner.Pets,
                        (petOwner, petName) => new { petOwner, petName }
                    )
        .Select(ownerAndPet =>
                new
                {
                    Owner = ownerAndPet.petOwner.Name,
                    Pet = ownerAndPet.petName
                }
        );
    //
    //var query =
    // petOwners
    // .MySelectMany ( petOwner => petOwner.Pets, 
    //     (petOwner, petName) => new { petOwner, petName }
    //    ) ;
    //     

    // Print the results.
    foreach (var obj in query)
    {
        Console.WriteLine(obj);
    }

    //本例輸出: (without Where() condition)
    // {Owner=Higa, Pet=Scruffy}
    // {Owner=Higa, Pet=Sam}
    // {Owner=Ashkenazi, Pet=Walker}
    // {Owner=Ashkenazi, Pet=Sugar}
    // {Owner=Price, Pet=Scratches}
    // {Owner=Price, Pet=Diesel}
    // {Owner=Hines, Pet=Dusty}

    //MSDN範例輸出: (with Where() condition)
    // {Owner=Higa, Pet=Scruffy}
    // {Owner=Higa, Pet=Sam}
    // {Owner=Ashkenazi, Pet=Sugar}
    // {Owner=Price, Pet=Scratches}
}

(4) Main()

static void Main(string[] args)
{
    // CASE 1
    Console.WriteLine("====== CASE 1 ======");
    SelectManyEx1();
    // CASE 2
    Console.WriteLine("====== CASE 2 ======");
    SelectManyEx2();
    // CASE 3
    Console.WriteLine("====== CASE 3 ======");
    SelectManyEx3();

    Console.ReadLine();
}

總結

要瞭解 SelectMany() 這個置於 Enumerable.cs 的擴充方法, 必須要有泛型及委派的基礎, 不然, 還真的不容易看懂.

筆者學藝不精, 不確定 SelectMany() 在實務上用到的機會是否很大, 但可作為對泛型及委派觀念再次釐清之用.

MSDN 的這個範例, 可以應用在 Master Detail 結構的資料存取上, 例如: OrderMaster, OrderDetail,

參考文件



.

沒有留言:

張貼留言