[.NET]快快樂樂學LINQ系列-Select() 簡介

[.NET]快快樂樂學LINQ系列-Select() 簡介

前言

上一篇介紹了第一個 LINQ to Objects 的 API: Where() ,這一篇則是介紹另外一個很常用的 API: Select() 。

 

需求

假設我們希望從一群人裡,篩選出來他們所有的姓名。該怎麼設計呢?

context 端的程式碼,如下所示:

image

image

 

沒有 LINQ 時怎麼做

要取得一群人的姓名,想當然爾就是透過 foreach 將這一群人展開,並將他們的姓名一個一個加入結果集合中。

上面的描述,轉換為程式碼也相當簡單,如下所示:

        private static IEnumerable<string> GetNames()
        {
            var people = GetPeople();

            foreach (var person in people)
            {
                yield return person.Name;
            }
        }

上述的程式碼,其實充斥在各個系統中,只是常見的模樣可能是 var result = new List<string>(); 與 result.Add(name); 而已。

yield return 則是具備延遲執行的效果。

讓我們思考一下:

1. 倘若需求要的不是 Name, 而是 Age 呢?難不成我們還要寫一個類似的 function ,差異只是一個回傳 Age 的結果,一個是回傳 Name 的結果?

        private static IEnumerable<int> GetAges()
        {
            var people = GetPeople();

            foreach (var person in people)
            {
                yield return person.Age;
            }
        }

這太愚蠢了!能不能讓呼叫端自己來決定要回傳的結果是什麼?

2. 倘若不只是單純取得 Person 上的 property, 而是可以傳入一個 function 來決定傳入 person 後所運算完的結果呢?例如,傳入 person, 要取得的是這個人的所有訂單。如下所示:

        private static IEnumerable<IEnumerable<Order>> GetOrdersFromPeople()
        {
            var people = GetPeople();

            foreach (var person in people)
            {
                yield return GetOrders(person);
            }
        }

如果有這樣的彈性需求,那原本的程式碼還能怎樣優化呢?其實就是用 delegate 來提供彈性。

來看一下上述的需求用 LINQ 有多簡單。

 

有 LINQ 時,只要這麼做

當有 LINQ 時,只需要呼叫 Select() 的方法,將希望取得的結果透過 delegate 傳進去即可。

取得 people 的 Name 集合,只需要呼叫 Select(x => x.Name) ,如下所示:

        private static IEnumerable<string> GetNames()
        {
            var people = GetPeople();

            return people.Select(person => person.Name);

            //foreach (var person in people)
            //{
            //    yield return person.Name;
            //}
        }

如果需要的是 Age, 只需要改傳入的 delegate 參數即可,如下所示:

        private static IEnumerable<int> GetAges()
        {
            var people = GetPeople();

            return people.Select(person => person.Age);

            //foreach (var person in people)
            //{
            //    yield return person.Age;
            //}
        }

那如果是剛剛要取得每一個人所屬的訂單呢?一樣,既然是 delegate, 要怎麼搞,就隨呼叫端開心,如下所示:

        private static IEnumerable<IEnumerable<Order>> GetOrdersFromPeople()
        {
            var people = GetPeople();

            return people.Select(person => GetOrders(person));
            
            //foreach (var person in people)
            //{
            //    yield return GetOrders(person);
            //}
        }

LINQ 用起來,就是這麼輕鬆寫意,一行搞定!這也是為什麼前哨戰要花篇幅介紹 delegate 跟 Lambda 的原因了。接下來我們來說明 Select() 的本質究竟是個什麼東西。

 

Select() 的 Signature

下圖是 Select() 在 MSDN 文件上的 Signature:

image

第一個參數與上一篇介紹 Where() 時一樣, LINQ to Objects 大部分都是針對集合來做巡覽以達到目的,因此針對 IEnumerable<TSource> 進行擴充,在實作的骨子裡就是透過 GetEnumerator() 與 iterator.MoveNext(), iterator.Current 來巡覽。

foreach 其實就是 IEnumerable, IEnumrator, enumerable.GetEnumerator(), iterator.MoveNext(), iterator.Current 的封裝。

第二個參數則比較特別一些,使用到了兩個 generic type ,分別是 TSource 與 TResult 。TSource 顧名思義就是 source 中 element 的型別。而 TResult 呢?則是 TSource 「投射」(projection)後結果的型別。

回傳的結果型別則是 IEnumerable<TResult> ,也就是每一個投射的結果。

投射或是 project ,其實感覺都不是這麼直覺,因此,我更喜歡用 var y = f(x); 來說明 projection 。對應到 signature ,應該是 TResult y = selector(TSource x);

因此,Select() 一言以蔽之:Select() 就是將 source 中每一個 element ,經過 selector 這個 delegate 後所產出的結果,一一回傳

 

簡單實作自己的 Select()

有了上一篇 Where() 的實作經驗,加上這一篇最前面所介紹沒有 LINQ 時的寫法,相信各位讀者要自己實作出 Select() 的方法內容並不困難。只要把沒有 LINQ 中,該彈性的部份抽出來成為 Func<TSource, TResult> 的 delegate 即可。當然,我們一樣要考慮 argument 的防呆與延遲執行的特性。

實作的程式碼如下:

namespace JoeyLinq
{
    public static class MyEnumerable
    {
        public static IEnumerable<TResult> Select<TSource, TResult>(
            this IEnumerable<TSource> source, Func<TSource, TResult> selector)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            if (selector == null)
            {
                throw new ArgumentNullException("selector");
            }

            return InternalSelect(source, selector);
        }

        private static IEnumerable<TResult> InternalSelect<TSource, TResult>(
            IEnumerable<TSource> source, Func<TSource, TResult> selector)
        {
            foreach (var item in source)
            {
                yield return selector(item);
            }
        }
    }
}

重點就只有那一行: yield return selector(item); 這就是將 item 投射為 TResult 的結果。

 

Select() 的多載

如 Where() 的多載需求一般,如果在使用 Select() 時,也希望取得該 TSource 的 item index 為何時,只有上面的方法是不夠的。例如我們除了想取得這一群人的姓名以外,還希望取得他們的序號來編號與呈現。我們期望 Select() 可以怎麼用呢?如下圖所示:

image

同樣的,來看一下這個多載的 Signature:

image

與 Where() 的多載相同, selector 多一個 int 的 parameter 可以使用,其意義就是 item 的 index 。

實作也與 Where() 的多載相同,把 index 傳給 selector 即可,如下所示:

        public static IEnumerable<TResult> Select<TSource, TResult>(
            this IEnumerable<TSource> source, Func<TSource, int, TResult> selector)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            if (selector == null)
            {
                throw new ArgumentNullException("selector");
            }

            return InternalSelect(source, selector);
        }

        private static IEnumerable<TResult> InternalSelect<TSource, TResult>(
            IEnumerable<TSource> source, Func<TSource, int, TResult> selector)
        {
            var index = 0;
            foreach (var item in source)
            {
                yield return selector(item, index);
                index++;
            }
        }

 

結論

Select() 的 Signature 一點也不可怕,如前面介紹 Func 的文章所說, Func<TSource, int, TResult> 就只是代表:

public delegate TResult Func<TSource, TResult>(TSource item, int index);

骨子裡的概念更是簡單:

將 source 展開,將每一個 element 交給 selector 進行投射作業,一一回傳投射完的結果。

當在 context 端使用到 source.Where().Select() 搭配延遲執行時,請參考前面介紹延遲執行的文章: [.NET]快快樂樂學LINQ系列前哨戰-延遲執行 (Deferred Execution)

 

Reference

  1. MSDN Select 說明
  2. Jon Skeet: Reimplementing "Select"

感謝同事 Shelly 與 King 協助設計 training 教材。

Sample Code 下載:SelectSample.zip


blog 與課程更新內容,請前往新站位置:http://tdd.best/