自己動手重新實現LINQ to Objects: 10 - Any和All

本文翻譯自Jon Skeet的系列博文“Edulinq”。

本篇原文地址:

http://msmvps.com/blogs/jon_skeet/archive/2010/12/28/reimplementing-linq-to-objects-part-10-any-and-all.aspx 

 

今天我們介紹兩個操作符:AnyAll

 

AnyAll做什麼?

Any有兩個重載,而All只有一個:

public static bool Any<TSource>( 

    this IEnumerable<TSource> source) 

 

public static bool Any<TSource>( 

    this IEnumerable<TSource> source, 

    Func<TSource, bool> predicate) 

 

public static bool All<TSource>( 

    this IEnumerable<TSource> source, 

    Func<TSource, bool> predicate)

這兩個方法所做的事情完全可以顧名思義:

l 不接受謂詞的Any用於判斷輸入序列中是否存在任何元素

l 接受謂詞的Any用於判斷輸入序列中是否存在能夠通過謂詞檢驗的元素

l All用於判斷輸入序列中的元素是否全部都能通過謂詞的檢驗

 

這兩個操作符都是立即執行的,它們在得出最終結果之前不會返回。

很重要的一點,All必須要迭代整個輸入序列才能返回true,但是它只要遇到一個不能通過謂詞檢驗的元素就會返回false;而Any只要找到一個可以通過謂詞檢驗的元素就會返回true,但是必須要迭代整個輸入序列才能返回false。這就引出了一個很簡單的LINQ性能小竅門,下面這種用法幾乎在所有情況下都是不可取的:

// Don't use this 

if (query.Count() != 0)

上面的用法會迭代整個輸入序列,如果你只想知道序列中是否含有元素的話,這樣來做:

// Use this instead 

if (query.Any())

如果這是一個較大的LINQ to SQL查詢中的一部分,那麼這兩種做法的區別可能不大,但是對於LINQ to Objects來說,區別很大。

 

我們需要測試什麼?

 

我今晚感覺不錯,我甚至把參數校驗都做了...雖然說參數校驗在這個立即執行的特例下並不困難。

除此之外,我還測試了以下一些場景:

l Any作用於空序列應該返回false,而All則應該返回true。(因爲無論謂詞是什麼樣的,沒有任何一個元素會通不過檢驗。)

l 一個序列,只要它含有元素,不接受謂詞的Any就應該返回true

l 如果所有元素都不能通過謂詞,那麼AnyAll都應該返回false

l 如果部分元素能夠通過謂詞,Any將會返回trueAll會返回false

l 如果所有元素都能夠通過謂詞,那麼All會返回true

以上測試都很簡潔明瞭,我就不給出代碼了。還有最後一個測試很有趣:我們要證明Any會在找到第一個符合條件的元素之後立即返回,證明的手段是通過把Any作用在一個被完整迭代時會拋出異常的查詢結果上。最簡單的方式就是創建一個包含有0的整數序列,然後對其做Select操作,Select中會把每一個元素除以某個常數。以下的測試用例中,序列中會導致異常的元素之前存在一個能夠通過謂詞的元素:

[Test] 

public void SequenceIsNotEvaluatedAfterFirstMatch() 

{ 

    int[] src = { 10, 2, 0, 3 }; 

    var query = src.Select(x => 10 / x); 

    // This will finish at the second element (x = 2, so 10/x = 5) 

    // It won't evaluate 10/0, which would throw an exception 

    Assert.IsTrue(query.Any(y => y > 2)); 

}

對於All,也有一個類似的測試用例,其中會導致異常的元素前面存在一個不能通過謂詞檢驗的元素。

現在所有測試都有了,下面開始有趣的部分了:

 

來動手實現吧!

 

有一點需要提醒,我們可以基於接受謂詞的Any或者基於All來實現另外兩個方法。比如說,如果已經實現了All的話,那麼Any就可以這樣實現:

public static bool Any<TSource>( 

    this IEnumerable<TSource> source) 

{ 

    return source.Any(x => true); 

} 

 

public static bool Any<TSource>( 

    this IEnumerable<TSource> source, 

    Func<TSource, bool> predicate) 

{ 

    if (predicate == null) 

    { 

        throw new ArgumentNullException("predicate"); 

    } 

    return !source.All(x => !predicate(x)); 

}

基於接受謂詞的的Any來實現不接受謂詞的Any是最簡單的,我們使用了一個對任何元素都會返回true的謂詞,這就意味着只要輸出序列中含有元素就會返回true,這正是我們想要的行爲。

上面調用All時的兩次否操作會讓你費點腦筋,不過這其實就是德摩根定律LINQ中的表現形式:我們對謂詞做否操作,來檢驗是否所有的元素都不能通過謂詞,得到結果後,再次做否操作並返回。由於否操作的原因,這種實現方式仍然會在合適的情況下提前返回。

雖然以上的方式可行,但是我更喜歡給每個方法一個單獨的實現,這樣做簡單明瞭:

public static bool Any<TSource>( 

    this IEnumerable<TSource> source) 

{ 

    if (source == null) 

    { 

        throw new ArgumentNullException("source"); 

    } 

             

    using (IEnumerator<TSource> iterator = source.GetEnumerator()) 

    { 

        return iterator.MoveNext(); 

    } 

} 

 

public static bool Any<TSource>( 

    this IEnumerable<TSource> source, 

    Func<TSource, bool> predicate) 

{ 

    if (source == null) 

    { 

        throw new ArgumentNullException("source"); 

    } 

    if (predicate == null) 

    { 

        throw new ArgumentNullException("predicate"); 

    } 

 

    foreach (TSource item in source) 

    { 

        if (predicate(item)) 

        { 

            return true; 

        } 

    } 

    return false; 

} 

 

 

public static bool All<TSource>( 

    this IEnumerable<TSource> source, 

    Func<TSource, bool> predicate) 

{ 

    if (source == null) 

    { 

        throw new ArgumentNullException("source"); 

    } 

    if (predicate == null) 

    { 

        throw new ArgumentNullException("predicate"); 

    } 

 

    foreach (TSource item in source) 

    { 

        if (!predicate(item)) 

        { 

            return false; 

        } 

    } 

    return true; 

}

這樣的實現方式很明顯的凸顯了“提前返回”這一特性。而且,這樣做也可以使得堆棧跟蹤記錄更易讀。對於一個下游開發者來說,如果調試Any時在堆棧跟蹤記錄中看到了調用All的記錄會顯得很奇怪;調用All時看到了Any也會很奇怪。

有一點很有趣,不接受謂詞的Any中我們沒用到foreach。而是用了迭代器第一次調用MoveNext方法時的返回值來表示序列中是否存在元素。讀這個方法可以很明顯的(至少對我來說很明顯)看出我們根本不關心第一個元素的值是什麼,因爲我們根本就沒有去訪問它。

 

結論

 

儘量使用Any而不是Count的建議或許是這篇文章中最重要的一點,餘下的部分都比較簡單,不過能基於一個操作符來實現另一個操作符總是很有趣的。

下一篇講什麼呢?或許是SingleSingleOrDefaultFirstFirstOrDefaultLast或者LastOrDefault。也或許我會把它們都放到一篇文章中來闡釋它們的相似同時也強調它們之間的差別。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章