自己动手重新实现LINQ to Objects: 9 - SelectMany
本文翻譯自Jon?Skeet的系列博文“Edulinq”。
本篇原文地址:
http://msmvps.com/blogs/jon_skeet/archive/2010/12/27/reimplementing-linq-to-objects-part-9-selectmany.aspx?
?
我們接下來要實現的這個操作符是LINQ中最重要的操作符。大多數(或者是全部?)其他的返回一個序列的操作符都可以通過調用SelectMany來實現,這是后話按下不表。現在我們首先來實現它吧。
?
SelectMany是什么?
?
SelectMany有四個重載,看起來一個比一個嚇人:
public?static?IEnumerable<TResult>?SelectMany<TSource,?TResult>(?
????this?IEnumerable<TSource>?source,?
????Func<TSource,?IEnumerable<TResult>>?selector)?
?
public?static?IEnumerable<TResult>?SelectMany<TSource,?TResult>(?
????this?IEnumerable<TSource>?source,?
????Func<TSource,?int,?IEnumerable<TResult>>?selector)?
?
public?static?IEnumerable<TResult>?SelectMany<TSource,?TCollection,?TResult>(?
????this?IEnumerable<TSource>?source,?
????Func<TSource,?IEnumerable<TCollection>>?collectionSelector,?
????Func<TSource,?TCollection,?TResult>?resultSelector)?
?
public?static?IEnumerable<TResult>?SelectMany<TSource,?TCollection,?TResult>(?
????this?IEnumerable<TSource>?source,?
????Func<TSource,?int,?IEnumerable<TCollection>>?collectionSelector,?
????Func<TSource,?TCollection,?TResult>?resultSelector)
其實還不算太壞,這些重載只是同一個操作的不同形式而已。
無論是哪個重載,都需要一個輸入序列。然后用一個委托來處理輸入序列中的每個元素以生成一個子序列,這個委托可能會接受一個代表元素index的參數。
再然后,我們或者把每個子序列中的元素直接返回,或者再用另一個委托來做處理,這個委托接受輸入序列中的元素并接受其對應的子序列中的元素。
以我的經驗來說,使用index兩個重載不太常用,而另外兩個重載(上面列出的第一個和第三個)則比較常用。還有,當C#編譯器處理一個含有多個from子句的查詢表達式的時候,它會把出第一個from之外的其他from子句轉譯為上面的第三個重載。
為了把上面的說法放入實例中理解,我們假設有這樣一個查詢表達式:
var?query?=?from?file?in?Directory.GetFiles("logs")?
????????????from?line?in?File.ReadLines(file)?
????????????select?Path.GetFileName(file)?+?":?"?+?line;
上面的查詢表達式會被轉譯為下面的“正常”調用:
var?query?=?Directory.GetFiles("logs")?
?????????????????????.SelectMany(file?=>?File.ReadLines(file),?
?????????????????????????????????(file,?line)?=>?Path.GetFileName(file)?+?":?"?+?line);
這個例子中,編譯器會把表達式中的select子句轉譯為投影操作;如果表達式后面還跟有where子句或其他子句,編譯器會把file和line包裝在一個匿名類型中傳遞給投影操作。這是查詢表達式轉譯中最令人難理解的一點,因為這涉及到了透明標識符(transparent?identifiers)。就現在來說,我們只分析上面給出的簡單例子。
上例中的SelectMany接受三個參數:
l?輸入序列,也就是一個字符串序列(Directory.GetFiles所返回的文件名)
l?一個初始投影操作,它把一個文件名轉化為該文件中包含的一行行的字符串
l?一個結束投影操作,它把一個文件名和一行文件內容轉化為一個由冒號分隔的字符串
表達式的最后結果會是一個字符串的序列,其中包含所有log文件的每一行,每一行會以文件名作為前綴。如果把結果打印出來,大概會是這樣的:
test1.log:?foo?
test1.log:?bar?
test1.log:?baz?
test2.log:?Second?log?file?
test2.log:?Another?line?from?the?second?log?file
要理解SelectMany可能會費點腦子,我當時理解它就費了點力,不過理解它是很重要的。
在講測試之前,還有幾點關于SelectMany的行為細節需要說明:
l?參數校驗是立即執行的,每個參數都不能是null
l?整個過程都是流式處理的。每次只會從輸入序列中讀取一個元素,然后生成一個子序列。然后每次只會返回子序列中的一個元素,返回子序列中的全部元素之后再去讀取輸入序列中的下一個元素,用它來生成下一個子序列,如此循環往復。
l?每個迭代器在使用完之后都會被關閉,正如你會預期的一樣。
?
我們要測試什么呢?
?
我有一點變懶了,我不想再寫參數為null的測試了。我給SelectMany的每一個重載都寫了一個測試。我發現我無法把這些測試寫得很清晰,不過還是拿出一個例子來,下面的代碼是針對SelectMany的最復雜的重載的測試:
[Test]?
public?void?FlattenWithProjectionAndIndex()?
{?
????int[]?numbers?=?{?3,?5,?20,?15?};?
????var?query?=?numbers.SelectMany((x,?index)?=>?(x?+?index).ToString().ToCharArray(),?
???????????????????????????????????(x,?c)?=>?x?+?":?"?+?c);?
????//?3?=>?"3:?3"?
????//?5?=>?"5:?6"?
????//?20?=>?"20:?2",?"20:?2"?
????//?15?=>?"15:?1",?"15:?8"?
????query.AssertSequenceEqual("3:?3",?"5:?6",?"20:?2",?"20:?2",?"15:?1",?"15:?8");?
}
?
給這個測試做一點解釋:
l?每一個數字都和它的序號相加?(3+0,?5+1,?20+2,?15+3)
l?相加的結果轉成字符串,然后轉成字符數組。(我們原本不需要調用ToCharArray的,因為String本身就實現了IEnumerable<char>,不過現在這樣寫比較清晰。)
l?然后把子序列中的每一個字符和原元素以“原元素:子序列字符”的形式組合在一起
注釋部分是每一個輸入元素對應的輸出結果,測試最后一句代碼給出了完整的輸出序列。
是不是一團亂麻?希望你看了上面逐步分解的解釋很清楚一點。好了,現在想辦法讓測試可以通過吧。
?
開始動手實現吧!
?
我們可以通過實現一個最復雜的重載并讓其他的重載都調用它來實現SelectMany,或者也可以寫一個沒有參數校驗的“Impl”方法,然后讓四個重載都調用它。比如說,最簡單重載可以這樣實現:
public?static?IEnumerable<TResult>?SelectMany<TSource,?TResult>(?
????this?IEnumerable<TSource>?source,?
????Func<TSource,?IEnumerable<TResult>>?selector)?
{?
????if?(source?==?null)?
????{?
????????throw?new?ArgumentNullException("source");?
????}?
????if?(selector?==?null)?
????{?
????????throw?new?ArgumentNullException("selector");?
????}?
????return?SelectManyImpl(source,?
??????????????????????????(value,?index)?=>?selector(value),?
??????????????????????????(originalElement,?subsequenceElement)?=>?subsequenceElement);?
}
不過我還是選擇為每一重載寫一個簽名相同的“SelectManyImpl”方法。我覺得這樣做可以讓以后單步調試時更簡單一些...而且這樣讓我們可以注意到不同重載之間的區別,代碼是這樣的:
//?Simplest?overload?
private?static?IEnumerable<TResult>?SelectManyImpl<TSource,?TResult>(?
????IEnumerable<TSource>?source,?
????Func<TSource,?IEnumerable<TResult>>?selector)?
{?
????foreach?(TSource?item?in?source)?
????{?
????????foreach?(TResult?result?in?selector(item))?
????????{?
????????????yield?return?result;?
????????}?
????}?
}?
?
//?Most?complicated?overload:?
//?-?Original?projection?takes?index?as?well?as?value?
//?-?There's?a?second?projection?for?each?original/subsequence?element?pair?
private?static?IEnumerable<TResult>?SelectManyImpl<TSource,?TCollection,?TResult>(?
????IEnumerable<TSource>?source,?
????Func<TSource,?int,?IEnumerable<TCollection>>?collectionSelector,?
????Func<TSource,?TCollection,?TResult>?resultSelector)?
{?
????int?index?=?0;?
????foreach?(TSource?item?in?source)?
????{?
????????foreach?(TCollection?collectionItem?in?collectionSelector(item,?index++))?
????????{?
????????????yield?return?resultSelector(item,?collectionItem);?
????????}?
????}?
}
這兩個方法之間的相似性很是明顯...不過我還是覺得保留著第一種形式很有用,如果我搞不清楚SelectMany的作用的話,通過第一種最簡單的重載就可以很容易的弄懂。以此為基礎再去理解余下的重載,跳躍性就不會那么大了。第一個重載在一定程度上起到了一個理解SelectMany的概念的墊腳石的作用。
有兩點需要指出:
如果C#中可以使用“yield?foreach?selector(item)”這種表達式的話,上面的第一個方法就可以實現的稍簡單一點。如果要在第二個方法中使用這種做法的話就會難一些,而且可能還要涉及到對Select的調用,這樣的話就有點得不償失了。
在第二個方法中,我沒有顯式的使用“checked”代碼塊,雖然說“index”是有可能溢出的。我沒有看過BCL的實現是什么樣的,但是我認為他們不會寫“checked”的。考慮到前后一致性,我或許應該在每一個處理index的方法中都是用“checked”代碼塊,或者給整個程序集開啟“checked”。
?
通過調用SelectMany來實現其他操作符
?
之前我提到過很多的LINQ操作符都可以通過調用SelectMany來實現。下面的代碼就是這一觀點的實例,我們通過調用SelectMany實現了Select,Where和Concat:
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?source.SelectMany(x?=>?Enumerable.Repeat(selector(x),?1));?
}?
?
public?static?IEnumerable<TSource>?Where<TSource>(?
????this?IEnumerable<TSource>?source,?
????Func<TSource,?bool>?predicate)?
{?
????if?(source?==?null)?
????{?
????????throw?new?ArgumentNullException("source");?
????}?
????if?(predicate?==?null)?
????{?
????????throw?new?ArgumentNullException("predicate");?
????}?
????return?source.SelectMany(x?=>?Enumerable.Repeat(x,?predicate(x)???1?:?0));?
}?
?
public?static?IEnumerable<TSource>?Concat<TSource>(?
????this?IEnumerable<TSource>?first,?
????IEnumerable<TSource>?second)?
{?
????if?(first?==?null)?
????{?
????????throw?new?ArgumentNullException("first");?
????}?
????if?(second?==?null)?
????{?
????????throw?new?ArgumentNullException("second");?
????}?
????return?new[]?{?first,?second?}.SelectMany(x?=>?x);?
}
Select和SelectMany使用Enumerable.Repeat來很方便的創建含有一個元素或不包含任何元素的序列。你也可以通過創建一個數組來代替使用Repeat的這種做法。Concat直接使用了一個數組:如果你理解了SelectMany的作用就是把多個序列組合為一個序列這一點的話,Concat這樣實現看起來就很自然了。我估計Empty和Repeat可以通過遞歸來實現,盡管這樣的話性能會很差。
現在,上面的代碼是放在條件編譯塊里面的。如果大家希望我多寫一些借助于SelectMany來實現的操作符的話,我可能會考慮把它單獨分離一個項目出來。不過我感覺以上的代碼已經足以顯示SelectMany的靈活性了,再利用SelectMany來實現更多的其他操作符也未必能更加充分的說明這一點。
在理論的意義上,SelectMany也很重要,因為它為LINQ提供了monadic的特性。我不想在這一話題上說的更多,你可以讀一讀Wes?Dyer的博客,或者直接搜索“bind?monad?SelectMany”就可以找到很多比我更聰明的人寫的文章。
?
結論
?
SelectMany是LINQ中的基礎之一,初看上去它很是令人生畏。但是一旦你理解了SelectMany的作用就是把多個序列組合起來這一點之后,它就很容易搞懂了。
下一次我們討論All和Any,這兩個操作符很適合放在一起來講解。
轉載于:https://www.cnblogs.com/cuipengfei/archive/2011/12/15/2289564.html
總結
以上是生活随笔為你收集整理的自己动手重新实现LINQ to Objects: 9 - SelectMany的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 孟姜女哭长城是什么歌呢?
- 下一篇: 日期NSDate的使用