Inspection

要知道我們並不能僅靠過濾冗餘的部份來取得所需的資料,有時候我們還需要從序列中取得相關或符合預期條件的部份,它是否符合特定條件?某個特定值是否存在?我想找到某個值!

上一章我們瞭解透過filter去縮減我們的來源可觀察序列,現在我們要看有關inspection的運算子,這些運算子大部份都會將來源序列縮減到單一值的序列,這些方法的回傳值雖然無法滿足我們對catamorphism的定義 - 非純量值(仍是IObservable<T>),但仍符合我們要將序列縮減至單一值的期待。

這系列的方法對inspection一個現有的序列很有幫助,每一種都會回傳包含一單一結果值的可觀察序列,原生的非同步方式再次證明它們的用處,且易於使用,請看下列介紹。

Any

我們先來看Any擴充函式的無參數覆載,如果來源序列沒有推送任何值就結束,它只回傳有一值為false的可觀察序列;而若來源序列的第一個值已被推送,則Any的可觀察序回傳true並結束。若第一個被推送的是錯誤,那它也會推送相同錯誤。

var subject = new Subject<int>();
subject.Subscribe(
    Console.WriteLine, 
    () => Console.WriteLine("Subject completed"));
var any = subject.Any();
any.Subscribe(
    b => Console.WriteLine("The subject has any values? {0}", b));
subject.OnNext(1);
subject.OnCompleted();

輸出:

1
The subject has any values? True
subject completed

如果我們把OnNext(1)拿掉,輸出如下:

subject completed
The subject has any values? False

Any只關心來源序列的第一個值,若是推送的是一個錯誤,它直接送出錯誤,否則就推送true。

var subject = new Subject<int>();
subject.Subscribe(Console.WriteLine,
    ex => Console.WriteLine("subject OnError : {0}", ex),
    () => Console.WriteLine("Subject completed"));
var any = subject.Any();
any.Subscribe(
    b => Console.WriteLine("The subject has any values? {0}", b),
    ex => Console.WriteLine(".Any() OnError : {0}", ex),
    () => Console.WriteLine(".Any() completed"));
subject.OnError(new Exception());

輸出:

subject OnError : System.Exception: Fail
.Any() OnError : System.Exception: Fail

Any另有一個需代入一個判斷式的覆載,就像Where一樣。

subject.Any(i => i > 2);
//Functionally equivalent to 
subject.Where(i => i > 2).Any();

試著自己實作上述兩種Any擴充方法當做練習,或許答案不是那麼明顯,但我們之前學到的函式已夠你實現類似功能。

用Observable.Create來建立Any擴充函式:

public static IObservable<bool> MyAny<T>(
this IObservable<T> source)
{
    return Observable.Create<bool>(
        o =>
        {
            var hasValues = false;
            return source
                .Take(1)
                .Subscribe(
                    _ => hasValues = true,
                    o.OnError,
                    () =>
                    {
                        o.OnNext(hasValues);
                        o.OnCompleted();
                    });
        });
}
public static IObservable<bool> MyAny<T>(
this IObservable<T> source, 
Func<T, bool> predicate)
{
    return source
        .Where(predicate)
        .MyAny();
}

All

擴充函式All()跟Any一樣,除了所有的值都需要符合判斷式,若是任一值的判斷結果為false,輸出序列會馬上結束,如果來源序列為空,All函式會傳回true,如同Any函式,錯誤也會一樣被送出。

var subject = new Subject<int>();
subject.Subscribe(
    Console.WriteLine, () => Console.WriteLine("Subject completed"));
var all = subject.All(i => i < 5);
all.Subscribe(
    b => Console.WriteLine("All values less than 5? {0}", b));
subject.OnNext(1);
subject.OnNext(2);
subject.OnNext(6);
subject.OnNext(2);
subject.OnNext(1);
subject.OnCompleted();

Output:

1
2
6
All values less than 5? False
all completed
2
1
subject completed

早期有用Rx的人可能會注意到IsEmpty擴充函式不見了,因為現在你可以用All()函式實作相同功能。

//IsEmpty() is deprecated now.
//var isEmpty = subject.IsEmpty();
var isEmpty = subject.All(_ => false);

Contains

Contains擴充函式可以很明顯的以Any函式來覆載之。Contains函式的行為就像Any,然而它的目標使用的是IComparable而不是判斷式,且是設計為尋找特定值而不是依據條件判斷結果而定。I believe that these are not overloads of Any for consistency with IEnumerable.

var subject = new Subject<int>();
subject.Subscribe(
    Console.WriteLine, 
    () => Console.WriteLine("Subject completed"));
var contains = subject.Contains(2);
contains.Subscribe(
    b => Console.WriteLine("Contains the value 2? {0}", b),
    () => Console.WriteLine("contains completed"));
subject.OnNext(1);
subject.OnNext(2);
subject.OnNext(3);
subject.OnCompleted();

Output:

1
2
Contains the value 2? True
contains completed
3
Subject completed

Contains函式另有一個可讓你自行指定IEqualityComparer<T>型態參數的覆載,在你需要時會很有用。

DefaultIfEmpty

DefaultIfEmpty函式在來源序列是空的時候會回傳預設值或是Default(T),Default(T)的值若是結構則為0,類別則為null,如果來源不為空,則所有值會直接傳出。

這個範例中來源序列產生值,所以DefaultIfEmpty的結果就是來源的值。

var subject = new Subject<int>();
subject.Subscribe(
    Console.WriteLine,
    () => Console.WriteLine("Subject completed"));
var defaultIfEmpty = subject.DefaultIfEmpty();
defaultIfEmpty.Subscribe(
    b => Console.WriteLine("defaultIfEmpty value: {0}", b),
    () => Console.WriteLine("defaultIfEmpty completed"));
subject.OnNext(1);
subject.OnNext(2);
subject.OnNext(3);
subject.OnCompleted();

Output:

1
defaultIfEmpty value: 1
2
defaultIfEmpty value: 2
3
defaultIfEmpty value: 3
Subject completed
defaultIfEmpty completed

如果來源為空,我們可以使用型別的預設值(如在int則為0)或提供我們自己的值,如範例中的42:

var subject = new Subject<int>();
subject.Subscribe(
    Console.WriteLine,
    () => Console.WriteLine("Subject completed"));
var defaultIfEmpty = subject.DefaultIfEmpty();
defaultIfEmpty.Subscribe(
    b => Console.WriteLine("defaultIfEmpty value: {0}", b),
    () => Console.WriteLine("defaultIfEmpty completed"));
var default42IfEmpty = subject.DefaultIfEmpty(42);
default42IfEmpty.Subscribe(
    b => Console.WriteLine("default42IfEmpty value: {0}", b),
    () => Console.WriteLine("default42IfEmpty completed"));
subject.OnCompleted();

Output:

Subject completed
defaultIfEmpty value: 0
defaultIfEmpty completed
default42IfEmpty value: 42
default42IfEmpty completed

ElementAt

ElementAt函式讓我們可以用索引挑選值,如同IEnumerable<T>是以0開始。

var subject = new Subject<int>();
subject.Subscribe(
    Console.WriteLine,
    () => Console.WriteLine("Subject completed"));
var elementAt1 = subject.ElementAt(1);
elementAt1.Subscribe(
    b => Console.WriteLine("elementAt1 value: {0}", b),
    () => Console.WriteLine("elementAt1 completed"));
subject.OnNext(1);
subject.OnNext(2);
subject.OnNext(3);
subject.OnCompleted();

Output

1
2
elementAt1 value: 2
elementAt1 completed
3
subject completed

由於我們無法確認可觀察序列到底會有多長,所以可以假定這個函式可能會造成問題。如果來源序列只有5個值,然後你想跟它要第六個值,ArgumentOutOfRangeException會在來源序列結束後被推送,因此我們有三個選擇:

  • 優雅的處理OnError
  • 使用Skip(5).Take(1);這會跳過前5個值,並只取第六個值,如果序列的元素少於六個,我們只會得到一個空序列,而不會發生錯誤。
  • 使用ElementAtOrDefault

ElementAtOrDefault擴充函式在這種索引值大於範圍的狀況下,會推送Default(T)值,而目前沒有提供你自設預設值的選項。

SequenceEqual

Finally SequenceEqual extension method is perhaps a stretch to put in a chapter that starts off talking about catamorphism and fold, but it does serve well for the theme of inspection. 這個函式讓我們可以比較兩個可觀察序列。當任一序列產生值時,此值會和另一序列的快取值相互比較,確保兩個序列是值相同,長度也相同。這也表示若是來源序列一產生不一樣的值時,結果序列會為false,而在兩個來源序皆相同且結束後,結果序列會為true。

var subject1 = new Subject<int>();
subject1.Subscribe(
    i=>Console.WriteLine("subject1.OnNext({0})", i),
    () => Console.WriteLine("subject1 completed"));
var subject2 = new Subject<int>();
subject2.Subscribe(
    i=>Console.WriteLine("subject2.OnNext({0})", i),
    () => Console.WriteLine("subject2 completed"));
var areEqual = subject1.SequenceEqual(subject2);
areEqual.Subscribe(
    i => Console.WriteLine("areEqual.OnNext({0})", i),
    () => Console.WriteLine("areEqual completed"));
subject1.OnNext(1);
subject1.OnNext(2);
subject2.OnNext(1);
subject2.OnNext(2);
subject2.OnNext(3);
subject1.OnNext(3);
subject1.OnCompleted();
subject2.OnCompleted();

Output:

subject1.OnNext(1)
subject1.OnNext(2)
subject2.OnNext(1)
subject2.OnNext(2)
subject2.OnNext(3)
subject1.OnNext(3)
subject1 completed
subject2 completed
areEqual.OnNext(True)
areEqual completed

本章討論了一組可讓我們檢查可觀察序列的函式,且一般都會回傳一單值的序列。我們將繼續研究可減少我們的序列的函式,直到我們碰到不好理解的functional fold功能。