本文翻譯自Jon Skeet的系列博文“Edulinq”。
本篇原文地址:
距離上次寫完本系列博文的第一篇和第二篇已經有一段日子了,希望接下來的進度會快一些。
現在我給本項目在Google Code上建立了源碼管理,現在就無需每篇博文包含一個zip文件了。創建項目時,我給它取了個顯而易見的名字,叫做Edulinq。我修改了代碼中的命名空間,而且現在這一系列博文的tag也修改爲了Edulinq了。好了,閒話少敘...我們來開始重新實現LINQ吧,這次要實現Select操作符。
Select操作符是什麼?
和Where類似,Select也有兩個重載:
public static IEnumerable<TResult> Select<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector)
public static IEnumerable<TResult> Select<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, int, TResult> selector)
其第二個重載讓投影操作可以訪問到序列元素的index。
先說簡單的東西:Select方法把一個序列投影成爲另一個序列:“selector”這個作爲參數的委託會被依次應用到輸入序列中的每一個元素上,並每次yield返回一個輸出元素。Select的行爲和Where很類似(實在是太類似了,以至於下面一段文字都是從上一篇文章中複製過來的,只是稍加修改):
l Select不會對輸入序列做任何修改。
l Select是延遲執行的 - 在你開始讀取輸出序列中的元素之前,Select不會去輸入序列中取元素。
l 不過也有一點不是延遲執行的,它會立即檢查參數是否爲null。
l 它以流式處理結果:它每次只處理一個結果元素。
l 你每在輸出序列上迭代一次,Select方法就會在輸入序列上迭代一次,這二者是嚴格對應的。
l 每次yield返回結果值的時候,“selector”這個委託就會被調用一次。
l 如果輸出序列的迭代器被Dispose掉的話,對應的輸入序列的迭代器也會被Dispose掉。
我們要測試什麼?
對Select的測試和對Where的測試也是很類似的,之前我們是針對Where的過濾功能來做測試,現在我們是針對Select的投影功能來做測試。
有幾個測試比較有趣。首先,你會發現Select方法是泛型的,而且有兩個泛型參數,分別是TSource和TResult。雖然這兩個參數的含義不言自明,不過還是得寫一個單元測試來測一下TSource和TResult分別爲不同類型的情況,比如說把int轉換成string的情況。
[Test]
public void SimpleProjectionToDifferentType()
{
int[] source = { 1, 5, 2 };
var result = source.Select(x => x.ToString());
result.AssertSequenceEqual("1", "5", "2");
}
然後我們看另一個測試,這個測試給我們展示了使用LINQ有可能會遇到的奇怪的副作用。其實我們本可以在Where的單元測試中做這個例子的,不過針對Select做起來更清晰一些:
[Test]
public void SideEffectsInProjection()
{
int[] source = new int[3]; // Actual values won't be relevant
int count = 0;
var query = source.Select(x => count++);
query.AssertSequenceEqual(0, 1, 2);
query.AssertSequenceEqual(3, 4, 5);
count = 10;
query.AssertSequenceEqual(10, 11, 12);
}
請注意我們只調用了Select一次,但是對Select方法返回值的多次迭代結果都不同,這是因爲“count”這個變量的值被保留住了並在每一次的投影過程中都會被修改。希望您不要寫出這種代碼。
再然後,我們可以寫一些同時包含“select”和“where”的查詢表達式:
[Test]
public void WhereAndSelect()
{
int[] source = { 1, 3, 4, 2, 8, 1 };
var result = from x in source
where x < 4
select x * 2;
result.AssertSequenceEqual(2, 6, 4, 2);
}
如果你用過LINQ to Objects的話,那麼上面這些東西對你來說應該是很熟悉很親切的,沒有什麼令人驚訝的。
來動手實現吧!
我們實現Select的方式和實現Where的方式差不多。我只是把Where的實現的代碼複製過來,稍加修改,這二者真的就是如此的相似。詳細說來就是:
l 我們利用迭代器代碼塊來輕鬆實現序列的返回。
l 要用到迭代器代碼塊就意味着必須要把參數校驗的代碼和核心實現代碼分離開。(我寫完上一篇博文之後瞭解到VB11中將會有匿名迭代器,匿名迭代器可以解決這個問題。哎。羨慕VB用戶的感覺怪怪的,但是我會學着接受現實的。)
l 我們在迭代器代碼塊中使用foreach,這樣就可以保證在輸出序列的迭代器被Dispose時或者輸入序列的元素被迭代完時,輸入序列的迭代器可以被妥當的Dispose掉。
由於Select的實現和Where的實現實在是太類似了,下面我直接給出代碼。Select方法的重載(含有index的那一個)的實現代碼就不展示了,因爲它和下面的代碼差別實在太小了。
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 SelectImpl(source, selector);
}
private static IEnumerable<TResult> SelectImpl<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector)
{
foreach (TSource item in source)
{
yield return selector(item);
}
}
很簡單,對吧?真正用來實現功能的代碼還沒有參數校驗的代碼長呢。
結論
雖然說我不想讓我的讀者感到無聊(你們中的有些人可能會感到驚訝),但是我還是得承認本篇文章頗有些無趣。我重複的強調“和Where很類似”,強調了那麼多次,搞得都有點乏味了,不過這樣才足以說明實現Select並沒有你可能想象的那麼複雜。
下次(我希望就在幾天之內)我會寫點不一樣的東西。我還不確定下次要寫哪個方法,待選的方法還有很多...