Skip to content

Debounce

1. 概要

Debounce は、ソースシーケンスから値を受け取るたびにタイマーをリセットし、指定した時間が経過しても新しい値が届かなかった場合にのみ、最後に受け取った値を下流に発行するオペレーターです。

高頻度で連続する値の中から「落ち着いた後の最終値」だけを取り出したい場合に適しています。典型的なユースケースは、テキスト入力フィールドのオートコンプリートや検索候補の取得です。

2. シグネチャ

TimeSpan によるデバウンス

csharp
public static Observable<T> Debounce<T>(
    this Observable<T> source,
    TimeSpan timeSpan)

値を受け取るたびに timeSpan のタイマーを開始(リセット)し、タイマーが満了したら最後の値を発行します。

csharp
source.Debounce(TimeSpan.FromMilliseconds(300))

TimeSpan + TimeProvider によるデバウンス

csharp
public static Observable<T> Debounce<T>(
    this Observable<T> source,
    TimeSpan timeSpan,
    TimeProvider timeProvider)

TimeProvider を指定してタイマーの実装を差し替えられます。テスト時に FakeTimeProvider を使用して時間を手動制御できます。

csharp
source.Debounce(TimeSpan.FromMilliseconds(300), fakeTimeProvider)

非同期セレクターによるデバウンス

csharp
public static Observable<T> Debounce<T>(
    this Observable<T> source,
    Func<T, CancellationToken, ValueTask> throttleDurationSelector,
    bool configureAwait = true)

値ごとに異なるデバウンス期間を非同期関数で指定できます。関数が完了する前に新しい値が届くと、前の非同期処理はキャンセルされ、新しい値で再度関数が呼び出されます。

csharp
// 値に応じてデバウンス時間を変える
source.Debounce(async (value, ct) =>
{
    var delay = value > 100
        ? TimeSpan.FromMilliseconds(500)
        : TimeSpan.FromMilliseconds(200);
    await Task.Delay(delay, ct);
})

overload の使い分け

overload使う場面
TimeSpan固定のデバウンス時間で十分な場合(最も一般的)
TimeSpan, TimeProviderユニットテストで時間を制御したい場合
Func<T, CancellationToken, ValueTask>値に応じてデバウンス時間を動的に変えたい場合

3. マーブルダイアグラム

Debounce のマーブルダイアグラム

ソースが値を発行するたびにタイマーがリセットされます。タイマーが満了するまで新しい値が届かなかった場合にのみ、最後の値が下流に発行されます。連続して値が届いている間は何も発行されません。

4. サンプルコード

csharp
using R3;

// テキスト入力のオートコンプリート
searchTextObservable
    .Debounce(TimeSpan.FromMilliseconds(300))
    .SubscribeAwait(async (query, ct) =>
    {
        var results = await SearchAsync(query, ct);
        UpdateSuggestions(results);
    });

// 値に応じた動的デバウンス
sensorDataObservable
    .Debounce(async (value, ct) =>
    {
        // 急激な変化には短いデバウンス、緩やかな変化には長いデバウンスを適用
        var delay = Math.Abs(value.Delta) > threshold
            ? TimeSpan.FromMilliseconds(100)
            : TimeSpan.FromMilliseconds(500);
        await Task.Delay(delay, ct);
    })
    .Subscribe(value => ProcessStableValue(value));

// FakeTimeProvider を使ったテスト
var fakeTime = new FakeTimeProvider();
var list = new List<int>();

Observable.Create<int>(observer =>
{
    observer.OnNext(1);
    observer.OnNext(2);
    observer.OnNext(3);
    return Disposable.Empty;
})
.Debounce(TimeSpan.FromSeconds(1), fakeTime)
.Subscribe(x => list.Add(x));

fakeTime.Advance(TimeSpan.FromSeconds(1));
// list: [3] — 最後の値のみ発行される

5. 補足

ThrottleFirst / ThrottleLast / ThrottleFirstLast との違い

オペレーター動作発行タイミング
Debounce値が来なくなるまで待つ値が落ち着いた後
ThrottleFirst最初の値を即座に発行し、一定期間無視最初の値が来た直後
ThrottleLast一定期間の最後の値を発行期間の終了時
ThrottleFirstLast最初の値 + 期間終了時の最後の値即座 + 期間終了時

Debounce は値が連続して発行される状態が落ち着くのを待つ動作であり、ThrottleFirst / ThrottleLast は一定間隔でサンプリングする動作です。入力が途切れない場合、Debounce は値を発行しない可能性がありますが、ThrottleLast は定期的に発行します。

フレームベースの版は DebounceFrame を参照してください。