Skip to content

ThrottleFirst

1. 概要

ThrottleFirst は、ソースシーケンスから最初に届いた値を即座に下流へ発行し、その後一定期間(または指定した条件が満たされるまで)新しい値を無視するオペレーターです。

ボタンの連打防止や、最初のイベントに即座に応答しつつ後続の重複イベントを抑制したい場合に適しています。R3 では時間ベース、サンプラー Observable ベース、非同期関数ベースの 3 種類のオーバーロードが用意されています。

2. シグネチャ

TimeSpan によるスロットリング

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

最初の値を即座に発行した後、timeSpan の間に届く値をすべて無視します。timeSpan が経過すると再び次の値を受け付けます。TimeProvider を指定するとタイマーの実装を差し替えられます。

csharp
// 500ms 間隔でスロットリング
source.ThrottleFirst(TimeSpan.FromMilliseconds(500))

ThrottleFirst(時間ベース)のマーブルダイアグラム

最初の値が発行されるとタイマーが開始し、タイマーが満了するまでの間に届いた値はすべて無視されます。タイマーが満了すると、次に届く値が再び即座に発行されます。


サンプラー Observable によるスロットリング

csharp
public static Observable<T> ThrottleFirst<T, TSample>(
    this Observable<T> source,
    Observable<TSample> sampler)

最初の値を即座に発行した後、sampler が次の値を発行するまでの間に届く値をすべて無視します。外部のイベント(タイマー、ボタンクリックなど)でスロットリング期間を制御できます。

csharp
// 外部タイマーでスロットリング期間を制御
source.ThrottleFirst(Observable.Interval(TimeSpan.FromSeconds(1)))

ThrottleFirst(サンプラー Observable)のマーブルダイアグラム

sampler が発火するたびにスロットリングの「ゲート」が開き、次に届く値が発行されます。ゲートが閉じている間の値はすべて無視されます。


非同期関数によるスロットリング

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

最初の値を即座に発行し、同時に sampler 非同期関数を開始します。関数が完了するまでの間に届く値はすべて無視されます。値ごとに異なるスロットリング期間を動的に設定できます。

csharp
// 値に応じてスロットリング期間を変える
source.ThrottleFirst(async (value, ct) =>
{
    var duration = value.Priority == Priority.High
        ? TimeSpan.FromMilliseconds(100)
        : TimeSpan.FromMilliseconds(500);
    await Task.Delay(duration, ct);
})

ThrottleFirst(非同期関数)のマーブルダイアグラム

非同期処理が実行されている間は値が無視され、処理が完了すると次の値を受け付けます。


overload の使い分け

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

3. サンプルコード

csharp
using R3;

// ボタン連打防止:最初のクリックのみ処理し、1 秒間は無視
buttonClickObservable
    .ThrottleFirst(TimeSpan.FromSeconds(1))
    .Subscribe(_ => ExecuteAction());

// サンプラー Observable を使った制御
var gate = new Subject<Unit>();
source
    .ThrottleFirst(gate)
    .Subscribe(x => Console.WriteLine($"値: {x}"));

// gate.OnNext(Unit.Default) を呼ぶたびにゲートが開く

// 非同期スロットリング:API コールの完了を待つ
source
    .ThrottleFirst(async (value, ct) =>
    {
        await apiClient.PostAsync(value, ct);
    })
    .Subscribe(x => Console.WriteLine($"送信済み: {x}"));

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

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

// results: [1] — 最初の値のみ即座に発行される
fakeTime.Advance(TimeSpan.FromSeconds(1));
// タイマー満了後、次の値を受け付ける状態に戻る

4. 補足

ThrottleLast / ThrottleFirstLast / Debounce との違い

オペレーター発行する値発行タイミング
ThrottleFirst最初の値値が来た直後
ThrottleLast最後の値期間の終了時
ThrottleFirstLast最初と最後の値即座 + 期間終了時
Debounce最後の値値が落ち着いた後

ThrottleFirst は即座の応答性を重視する場合に適しています。期間中の最新値も必要な場合は ThrottleFirstLast を検討してください。

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