[.NET]快快樂樂學LINQ系列-Select() 簡介
前言
上一篇介紹了第一個 LINQ to Objects 的 API: Where() ,這一篇則是介紹另外一個很常用的 API: Select() 。
需求
假設我們希望從一群人裡,篩選出來他們所有的姓名。該怎麼設計呢?
context 端的程式碼,如下所示:
沒有 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:
第一個參數與上一篇介紹 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() 可以怎麼用呢?如下圖所示:
同樣的,來看一下這個多載的 Signature:
與 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
感謝同事 Shelly 與 King 協助設計 training 教材。
Sample Code 下載:SelectSample.zip
blog 與課程更新內容,請前往新站位置:http://tdd.best/