Select
及SelectMany
的差別在前一章的說明後應該有個初步的了解了,知道了應用的方式後我們接著來看看它是怎麼做到的吧。
SelectMany
總共有四個多載的方法,兩個是只有一個selector
,另外兩個是有兩個selector
(collectionSelector
及resultSelector
)/* 下面兩個方法只有一個selector */
public static IEnumerable<TResult> SelectMany<TSource, TResult>( // 4
this IEnumerable<TSource> source,
Func<TSource, IEnumerable<TResult>> selector);
public static IEnumerable<TResult> SelectMany<TSource, TResult>( // 1
this IEnumerable<TSource> source,
Func<TSource, int, IEnumerable<TResult>> selector); // 多一個int參數
/* 下面兩個方法有兩個selector */
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>( // 2
this IEnumerable<TSource> source,
Func<TSource, IEnumerable<TCollection>> collectionSelector,
Func<TSource, TCollection, TResult> resultSelector);
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>( // 3
this IEnumerable<TSource> source,
Func<TSource, int, IEnumerable<TCollection>> collectionSelector, // 多一個int參數
Func<TSource, TCollection, TResult> resultSelector);
selector
但有int
傳入參數的方法:public static IEnumerable<TResult> SelectMany<TSource, TResult>(
this IEnumerable<TSource> source, Func<TSource, int, IEnumerable<TResult>> selector)
{
if (source == null)
{
throw Error.ArgumentNull(nameof(source));
}
if (selector == null)
{
throw Error.ArgumentNull(nameof(selector));
}
return SelectManyIterator(source, selector);
}
source
及selector
是否為空,為空的話丟ArgumentNull
例外SelectManyIterator
到這裡跟Select
幾乎一模一樣,唯一有差別的就只有最後的回傳值,SelectMany
是傳回SelectManyIterator
,相信特別之處就是在這裡,我們來看看SelectMany
的定義:
private static IEnumerable<TResult> SelectManyIterator<TSource, TResult>(
IEnumerable<TSource> source, Func<TSource, int, IEnumerable<TResult>> selector)
{
int index = -1;
foreach (TSource element in source)
{
checked
{
index++;
}
foreach (TResult subElement in selector(element, index))
{
yield return subElement;
}
}
}
yield
區塊,會轉為Iterator Pattern
,回傳的資料是IEnumerable
的集合yield return
傳回每一個元素的資料index
較前面的元素多加1
selector
執行後取得每個元素的子集合資料,再用foreach
巡覽整個子集合
我們可以看到跟SelectIterator
還是幾乎一模一樣,差別在於第二個foreach
,還記得我們前面講SelectMany的應用時比較了跟Select
的差別之處就是SelectMany
不用多一個迴圈去處理子集合的資料,從原始碼中觀察就更加明顯了,原來SelectMany
已經幫我們把第二個迴圈要做的事情給做掉了。
selector
(collectionSelector及resultSelector)但CollectionSelector
沒有int
參數的方法:public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(
this IEnumerable<TSource> source,
Func<TSource, IEnumerable<TCollection>> collectionSelector,
Func<TSource, TCollection, TResult> resultSelector)
{
if (source == null)
{
throw Error.ArgumentNull(nameof(source));
}
if (collectionSelector == null)
{
throw Error.ArgumentNull(nameof(collectionSelector));
}
if (resultSelector == null)
{
throw Error.ArgumentNull(nameof(resultSelector));
}
return SelectManyIterator(source, collectionSelector, resultSelector);
}
source
及selector
是否為空,空的話丟出ArgumentNull
的例外SelectManyIterator
這個方法跟剛剛介紹的第一個SelectMany
的方法差在多了一個if
判斷resultSelector
是否為空,然後回傳的方法SelectManyIterator
有三個參數,想當然,這裡不會是重點所在,我們接著來看看這個有三個參數的SelectManyIterator
:
private static IEnumerable<TResult> SelectManyIterator<TSource, TCollection, TResult>(
IEnumerable<TSource> source,
Func<TSource, IEnumerable<TCollection>> collectionSelector,
Func<TSource, TCollection, TResult> resultSelector)
{
foreach (TSource element in source)
{
foreach (TCollection subElement in collectionSelector(element))
{
yield return resultSelector(element, subElement);
}
}
}
resultSelector
執行後的結果跟第一個介紹的方法差別只差在子集合的元素要回傳前再去執行了resultSelector
,這樣的目的就是可以輸出子集合跟原集合合併的資料。
我們可以看到它跟第一個方法的結構是完全一樣的,但是有resultSelector
的幫助讓我們可以更省力的拿到自己想要的檔案。
int
的collectionSelector
的方法,因為方法的實作跟第二個完全一樣,我們就直接來看SelectManyIterator
的實作:private static IEnumerable<TResult> SelectManyIterator<TSource, TCollection, TResult>(
IEnumerable<TSource> source,
Func<TSource, int, IEnumerable<TCollection>> collectionSelector,
Func<TSource, TCollection, TResult> resultSelector)
{
int index = -1;
foreach (TSource element in source)
{
checked
{
index++;
}
foreach (TCollection subElement in collectionSelector(element, index))
{
yield return resultSelector(element, subElement);
}
}
}
學一套就會了全部,這個方法中完全沒有新的東西,只是把第一跟第二個方法合併起來而已。
index
來給予每個selector
位置的資訊(第一個方法)resultSelector
執行結果(第二個方法)public static IEnumerable<TResult> SelectMany<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, IEnumerable<TResult>> selector)
{
if (source == null)
{
throw Error.ArgumentNull(nameof(source));
}
if (selector == null)
{
throw Error.ArgumentNull(nameof(selector));
}
return new SelectManySingleSelectorIterator<TSource, TResult>(source, selector);
}
跟前面的方法一樣,判斷參數是否為空,如果都是合法的就傳給Iterator
做事,眼尖的人應該有發現到這次叫用的Iterator
跟前面方法叫用的並不相同,看來是有什麼秘密藏在這裡喔,我們來看看吧:
private sealed class SelectManySingleSelectorIterator<TSource, TResult> :
Iterator<TResult>,
IIListProvider<TResult>
這是一個實作了Iterator
的Class
,看到Iterator
的Class
自然就會想看看它的MoveNext
,以下是它的實作:
public override bool MoveNext()
{
switch (_state)
{
case 1:
// Retrieve the source enumerator.
_sourceEnumerator = _source.GetEnumerator();
_state = 2;
goto case 2;
case 2:
// Take the next element from the source enumerator.
if (!_sourceEnumerator.MoveNext())
{
break;
}
TSource element = _sourceEnumerator.Current;
// Project it into a sub-collection and get its enumerator.
_subEnumerator = _selector(element).GetEnumerator();
_state = 3;
goto case 3;
case 3:
// Take the next element from the sub-collection and yield.
if (!_subEnumerator.MoveNext())
{
_subEnumerator.Dispose();
_subEnumerator = null;
_state = 2;
goto case 2;
}
_current = _subEnumerator.Current;
return true;
}
Dispose();
return false;
}
_state
在GetEnumerator()
時會設為1(請參考第11章-Select的原碼探險)_state
為1時取得集合的Enumerator
_state
為2時對集合執行_selector
取得目標資料,到這裡為止就是Select
的MoveNext()
所做的事,但SelectMany
將目標資料的Enumerator
取得放進_subEnumerator
並且進入第三狀態(_state=3
)_state
為3時將子集合的Enumerator
(_subEnumerator
)做巡覽放進_current
裡面,如果巡覽終止則將狀態調回2(_state=2
)實際上SelectMany
比Select
多了一層的MoveNext()
來取得子集合的元素資料,達到扁平化的目的。
[Theory]
[MemberData(nameof(ParameterizedTestsData))]
public void ParameterizedTests(IEnumerable<int> source, Func<int, IEnumerable<int>>selector)
{
var expected = source.Select(i => selector(i)).Aggregate((l, r) => l.Concat(r));
var actual = source.SelectMany(selector);
Assert.Equal(expected, actual);
Assert.Equal(expected.Count(), actual.Count()); // SelectMany may employ an optimized Count implementation.
Assert.Equal(expected.ToArray(), actual.ToArray());
Assert.Equal(expected.ToList(), actual.ToList());
}
public static IEnumerable<object[]> ParameterizedTestsData()
{
for (int i = 1; i <= 20; i++)
{
Func<int, IEnumerable<int>> selector = n => Enumerable.Range(i, n);
yield return new object[] { Enumerable.Range(1, i), selector };
}
}
Aggregate()
會將每個目前巡覽的結果向後一個元素丟,以上述程式碼為例Aggregate((l, r) => l.Concat(r))
:
l
: 前個元素執行Aggregate
後的值r
: 目前的元素值所以Aggregate((l, r) => l.Concat(r))
是把所有元素合為一個IEnumerable
,而在這個測試案例我們可以發現到SelectMany()
可以轉為Select().Aggregate((l, r) => l.Concat(r))
。
這是一個驗證Dispose
執行的測試,在剛剛觀察程式碼後我們知道在巡覽(MoveNext()
)過程中會產生兩層Enumerator
(Source及Sub),這個測試就是要確定Enumerator
都有在應該Dispose
時Dispose
,由於案例較長,我們節錄重要的部分:
using (e) // Enumerator
{
while (e.MoveNext())
{
int item = e.Current;
Assert.Equal(subState[subIndex], item); // Verify Current.
Assert.Equal(index / subLength, subIndex);
// 第一層的Source巡覽結束後才會Dispose
Assert.False(sourceDisposed); // Not yet.
// This represents whehter the sub-collection we're iterating thru right now
// has been disposed. Also not yet.
Assert.False(subCollectionDisposed[subIndex]); // 目前的Sub因還在執行巡覽所以不會Dispose
// However, all of the sub-collections before us should have been disposed.
// Their indices should also be maxed out.
Assert.All(subState.Take(subIndex), s => Assert.Equal(subLength + 1, s));
// 此Source中的其他已巡覽完的Sub會Dispose
Assert.All(subCollectionDisposed.Take(subIndex), t => Assert.True(t));
index++;
}
}
// 巡覽結束Source會Dispose
Assert.True(sourceDisposed);
Assert.Equal(sourceLength, subIndex);
Assert.All(subState, s => Assert.Equal(subLength + 1, s));
Assert.All(subCollectionDisposed, t => Assert.True(t));
Source
在全部巡覽完後Dispose
Sub
都應該Dispose
我們都知道IEnumerable
只會知道目前的元素資料,它是屬於一種延遲執行的巡覽方式,而陣列跟Enumerable
的狀況不同,它一開始就知道所有的元素值了,那如果把它們兩個同時放到一個IEnumerable
要如何處理呢? 例如像下面這樣:
// Marker at the end
new IEnumerable<int>[]
{
new TestEnumerable<int>(new int[] { 0 }),
new TestEnumerable<int>(new int[] { 1 }),
new TestEnumerable<int>(new int[] { 2 }),
new int[] { 3 },
}
當然我們還是可以把陣列當作IEnumerable
去做處理,這在一般的處理中是可以的(因為都必須要Call MoveNext()
),但在ToArray()
中你的目標本來就是要轉為Array了,你卻要把陣列轉成IEnumerable
再轉成Array怎麼樣都划不來,因此SelectMany
的ToArray()
用了一個Marker
來表示集合中的陣列,我們先來看程式碼:
public TResult[] ToArray()
{
var builder = new SparseArrayBuilder<TResult>(initialize: true);
var deferredCopies = new ArrayBuilder<IEnumerable<TResult>>();
foreach (TSource element in _source)
{
IEnumerable<TResult> enumerable = _selector(element);
/*
* 是陣列(或是非延遲的集合)嗎?
* Yes: 將其的位置(Index)及數量(Count)加到builder.Markers中,然後傳回true
* No: 加到builder中,然後回傳false
*/
if (builder.ReserveOrAdd(enumerable))
{
// 陣列內容加到deferredCopies中
deferredCopies.Add(enumerable);
}
}
// 將builder中的資料做ToArray的動作(因為已經排除了陣列的資料,所以沒有做多餘的轉換)
TResult[] array = builder.ToArray();
ArrayBuilder<Marker> markers = builder.Markers; // 取得陣列位置(Index)及數量(Count)資訊
for (int i = 0; i < markers.Count; i++)
{
Marker marker = markers[i];
IEnumerable<TResult> enumerable = deferredCopies[i]; // 取得陣列內容
EnumerableHelpers.Copy(enumerable, array, marker.Index, marker.Count); // 複製到剛剛builder轉出來的陣列中
}
return array;
}
從上述的程式碼說明可以知道ToArray()
的運作細節,這個測試案例就是在測這個部分,我是因為看到這個案例才知道它的實作方式這麼的特別,所以才在測試案例這個單元做說明,如果想要深入了解的可以仔細看看這個測試案例。
SelectMany
是在Select
的基礎上多做事情,整體的邏輯更為複雜,希望透過本文的介紹可以讓大家更加了解這個方法的原理。