R3:適用于 .NET 的新一代響應式擴展庫
R3 是 dotnet/reactive(.NET 官方響應式擴展)與 UniRx(適用于 Unity 的響應式擴展)的新一代替代方案,支持多種平臺,包括 Unity、Godot、Avalonia、WPF、WinForms、WinUI3、Stride、LogicLooper、MAUI、MonoGame、Blazor 和 Uno。
我擁有超過 10 年的 Rx 使用經驗,不僅為游戲引擎實現過自定義 Rx 運行時(UniRx),還為游戲引擎開發過異步運行時(UniTask)。基于這些經驗,我認為有必要為 .NET 實現一款全新的響應式擴展庫,它既要能體現現代 C# 的特性,又要回歸 Rx 的核心價值。
核心設計理念
R3 的設計源于對傳統 Rx 局限性的反思,核心理念如下:
- 在 OnError 處中斷管道是錯誤設計:傳統 Rx 中,管道遇到異常會觸發 OnError 并自動取消訂閱,這在事件處理場景中過于嚴苛,且難以通過 Retry 等操作符優雅恢復。
- IScheduler 是性能瓶頸的根源:傳統 Rx 的 IScheduler 抽象層導致性能損耗,且內部實現復雜(如通過 PeriodicTimer 和 IStopwatch 規避問題)。
- 基于幀的操作是游戲引擎的關鍵需求:傳統 Rx 缺乏幀循環適配能力,而游戲引擎(如 Unity、Godot)中,基于幀的事件處理(如每幀更新、延遲 N 幀執行)至關重要。
- 單一異步操作應完全交給 async/await:Rx 應專注于事件流處理,而非替代 async/await 處理單個異步任務(如網絡請求、文件讀寫)。
- 不實現同步 API:同步操作無需 Rx 抽象,避免冗余設計。
- 查詢語法(Query Syntax)僅適用于 SQL:C# 的 LINQ 查詢語法(如?
from x in xs select x
)在 Rx 中使用體驗不佳,應優先使用方法鏈語法。 - 訂閱列表是防止內存泄漏的必要手段:需提供類似 “并行調試器” 的訂閱跟蹤能力,解決 GUI 或游戲等長生命周期應用的內存泄漏問題。
- 背壓(Backpressure)交給 IAsyncEnumerable 和 Channels:Rx 無需重復實現背壓機制,可復用 .NET 原生組件。
- 分布式處理與查詢有更優方案:GraphQL、Kubernetes、Orleans、Akka.NET、gRPC、MagicOnion 等工具更適合分布式場景,Rx 應專注于內存內消息處理(LINQ to Events)。
與傳統 Rx 的核心差異
為解決 dotnet/reactive 的不足,R3 對核心接口進行了重構。近年來,Kotlin Flow、Swift Combine 等面向現代語言特性的 Rx 類框架已成為標準,而 C# 也已演進至 C# 12,因此 R3 旨在打造一款與最新 C# 特性對齊的響應式擴展庫。
1. 性能優化
性能提升是 R3 重構的核心目標之一,以下是關鍵優化點:
- 移除 IScheduler 帶來的性能提升:傳統 Rx 的 IScheduler 導致大量冗余計算,R3 改用 .NET 8 引入的 TimeProvider,性能顯著提升(如下圖,
Observable.Range(1, 10000).Subscribe()
?的執行效率對比)。 - Subject 訂閱 / 取消的內存優化:傳統 Rx 的 Subject 使用 ImmutableArray 存儲訂閱者,每次添加 / 刪除訂閱都會分配新數組,導致內存頻繁波動。R3 采用自定義數據結構,避免 ImmutableArray 帶來的性能損耗(如下圖,
10000 次 subject.Subscribe() + 10000 次 subscription.Dispose()
?的內存分配對比)。
2. 核心接口重構
R3 的表面 API 與傳統 Rx 保持一致(便于遷移),但內部接口完全重構,不再依賴?IObservable<T>/IObserver<T>
。
2.1 新接口定義
傳統 Rx 的?IObservable<T>/IObserver<T>
?雖在理論上優雅(與?IEnumerable<T>
?對偶),但實際使用中存在諸多限制。R3 改用抽象類實現,確保行為可控:
csharp
using System;namespace R3
{/// <summary>/// 可觀察對象抽象類,替代傳統 IObservable<T>/// </summary>/// <typeparam name="T">事件流中數據的類型</typeparam>public abstract class Observable<T>{/// <summary>/// 訂閱可觀察對象,返回可取消訂閱的 IDisposable/// </summary>/// <param name="observer">觀察者對象,用于接收事件</param>/// <returns>用于取消訂閱的 IDisposable 實例</returns>public abstract IDisposable Subscribe(Observer<T> observer);}/// <summary>/// 觀察者抽象類,替代傳統 IObserver<T>,并實現 IDisposable 用于資源釋放/// </summary>/// <typeparam name="T">接收數據的類型</typeparam>public abstract class Observer<T> : IDisposable{/// <summary>/// 接收正常事件數據/// </summary>/// <param name="value">事件數據</param>public abstract void OnNext(T value);/// <summary>/// 接收異常事件(不會中斷訂閱)/// 區別于傳統 Rx 的 OnError,此處異常不會自動取消訂閱,需手動處理/// </summary>/// <param name="error">異常對象</param>public abstract void OnErrorResume(Exception error);/// <summary>/// 接收事件流完成通知(成功/失敗)/// 合并傳統 Rx 的 OnCompleted 和 OnError,用 Result 表示完成狀態/// </summary>/// <param name="result">完成結果,包含成功/失敗狀態</param>public abstract void OnCompleted(Result result);/// <summary>/// 釋放觀察者占用的資源/// </summary>public abstract void Dispose();}/// <summary>/// 事件流完成結果,區分成功/失敗狀態/// </summary>public readonly struct Result{/// <summary>/// 是否為失敗狀態/// </summary>public bool IsFailure { get; }/// <summary>/// 失敗時的異常對象(成功時為 null)/// </summary>public Exception? Exception { get; }/// <summary>/// 創建成功狀態的 Result/// </summary>/// <returns>成功狀態的 Result 實例</returns>public static Result Success() => new Result(false, null);/// <summary>/// 創建失敗狀態的 Result/// </summary>/// <param name="exception">失敗對應的異常</param>/// <returns>失敗狀態的 Result 實例</returns>public static Result Failure(Exception exception) => new Result(true, exception);private Result(bool isFailure, Exception? exception){IsFailure = isFailure;Exception = exception;}}
}
2.2 關鍵差異點
特性 | 傳統 Rx | R3 |
---|---|---|
異常處理 | OnError 觸發后自動取消訂閱 | OnErrorResume 觸發后不取消訂閱,需手動控制 |
完成通知 | OnCompleted(成功)+ OnError(失敗) | OnCompleted (Result) 合并兩種狀態 |
接口類型 | 接口(IObservable/IObserver) | 抽象類(Observable/Observer) |
訂閱跟蹤 | 無原生支持 | 內置訂閱列表,支持泄漏檢測 |
3. 訂閱管理與內存泄漏防護
訂閱泄漏是長生命周期應用(如 GUI、游戲)的常見問題。R3 通過以下設計解決該問題:
- 觀察者與訂閱綁定:訂閱時,Observer 會自動與目標 Observable 關聯,并作為 Subscription 實例(避免傳統 Rx 中額外的 IDisposable 分配)。
- 全鏈路訂閱跟蹤:Observer 從上游到下游形成可靠的鏈路,確保 OnCompleted/Dispose 時能釋放所有資源。
- ObservableTracker 工具:可啟用訂閱跟蹤功能,查看所有活躍訂閱的狀態(如創建時間、調用棧),便于定位泄漏。
csharp
using R3;
using System;// 啟用訂閱跟蹤(默認關閉)
ObservableTracker.EnableTracking = true;
// 啟用調用棧捕獲(性能損耗較高,調試時使用)
ObservableTracker.EnableStackTrace = true;// 創建一個示例訂閱
using var subscription = Observable.Interval(TimeSpan.FromSeconds(1)).Where(x => true).Take(10000).Subscribe();// 遍歷所有活躍訂閱,打印狀態
ObservableTracker.ForEachActiveTask(trackingState =>
{Console.WriteLine($"跟蹤ID: {trackingState.TrackingId}");Console.WriteLine($"類型: {trackingState.FormattedType}");Console.WriteLine($"創建時間: {trackingState.AddTime}");Console.WriteLine($"調用棧: {trackingState.StackTrace}");Console.WriteLine("------------------------");
});
核心功能特性
1. TimeProvider 替代 IScheduler
傳統 Rx 的 IScheduler 存在性能問題且設計復雜,R3 改用 .NET 8 引入的?TimeProvider?作為時間抽象,支持以下能力:
- 異步時間操作:通過?
TimeProvider.CreateTimer()
、TimeProvider.GetTimestamp()
?實現高效的時間控制。 - 平臺適配:默認使用?
TimeProvider.System
(基于線程池),同時提供平臺專用實現(如 WPF 的?DispatcherTimeProvider
、Unity 的?UpdateTimeProvider
)。 - 測試友好:支持?
FakeTimeProvider
(來自?Microsoft.Extensions.TimeProvider.Testing
),便于單元測試中模擬時間流逝。
1.1 時間相關操作符示例
csharp
using R3;
using System;// 1. 每隔 1 秒發送一個事件(使用默認 TimeProvider)
var interval = Observable.Interval(TimeSpan.FromSeconds(1));// 2. 延遲 2 秒后發送事件(指定自定義 TimeProvider,如 WPF 的 DispatcherTimeProvider)
var delay = Observable.Return(42).Delay(TimeSpan.FromSeconds(2), new DispatcherTimeProvider());// 3. 防抖操作(300ms 內無新事件則發送最后一個事件)
var debounce = Observable.FromEvent<EventArgs>(addHandler: h => button.Click += (s, e) => h(e),removeHandler: h => button.Click -= (s, e) => h(e)).Debounce(TimeSpan.FromMilliseconds(300), TimeProvider.System);
2. 基于幀的操作(FrameProvider)
游戲引擎和 GUI 應用依賴幀循環,R3 引入?FrameProvider?抽象層,提供與幀相關的操作符,支持以下場景:
- 每幀執行:如?
EveryUpdate()
?每幀發送一個事件。 - 延遲 N 幀執行:如?
DelayFrame(5)
?延遲 5 幀后發送事件。 - 幀防抖 / 節流:如?
DebounceFrame(10)
?10 幀內無新事件則發送。
2.1 幀操作符示例
csharp
using R3;
using UnityEngine; // 以 Unity 為例,其他平臺類似// 1. 每幀發送事件(使用 Unity 的 Update 幀循環)
Observable.EveryUpdate(UnityFrameProvider.Update).Subscribe(_ =>{// 每幀更新角色位置player.transform.Translate(Vector3.forward * Time.deltaTime);});// 2. 延遲 3 幀后執行(例如:玩家死亡后 3 幀顯示游戲結束界面)
player.OnDeathAsObservable().DelayFrame(3, UnityFrameProvider.Update).Subscribe(_ =>{UIManager.ShowGameOver();});// 3. 幀防抖(避免按鈕快速點擊觸發多次事件)
button.OnClickAsObservable().DebounceFrame(2, UnityFrameProvider.Update).Subscribe(_ =>{// 處理按鈕點擊(2 幀內多次點擊僅觸發一次)SubmitRequest();});// 4. 監聽屬性變化(每幀檢查屬性值,無 INotifyPropertyChanged 也可使用)
Observable.EveryValueChanged(this, x => x.PlayerHealth, UnityFrameProvider.Update).Subscribe(health =>{// 更新生命值顯示healthText.Text = $"HP: {health}";});
3. Subject 與 ReactiveProperty
R3 提供 5 種 Subject 類型,滿足不同事件分發場景,且默認在 Dispose 時觸發 OnCompleted,確保訂閱自動釋放。
Subject 類型 | 用途說明 |
---|---|
Subject<T> | 基礎事件分發器,無狀態,僅分發訂閱后的事件。 |
BehaviorSubject<T> | 持有最新值,新訂閱者會立即收到當前值。 |
ReactiveProperty<T> | 繼承 BehaviorSubject,支持去重(相同值不觸發通知),適合數據綁定。 |
ReplaySubject<T> | 緩存指定數量 / 時間范圍內的事件,新訂閱者會收到歷史事件。 |
ReplayFrameSubject<T> | 基于幀緩存事件(如緩存最近 10 幀的事件),適合游戲幀同步場景。 |
3.1 ReactiveProperty 示例(數據綁定與驗證)
csharp
using R3;
using System;
using System.ComponentModel.DataAnnotations;// 1. 響應式模型定義(以游戲角色為例)
public class Enemy
{// 生命值(ReactiveProperty 自動去重,值變化時觸發通知)public ReactiveProperty<long> CurrentHp { get; }// 是否死亡(由 CurrentHp 推導,自動同步狀態)public ReadOnlyReactiveProperty<bool> IsDead { get; }public Enemy(int initialHp){CurrentHp = new ReactiveProperty<long>(initialHp);// 從 CurrentHp 派生 IsDead(當生命值 ≤ 0 時為 true)IsDead = CurrentHp.Select(hp => hp <= 0).ToReadOnlyReactiveProperty();}
}// 2. 帶驗證的 ReactiveProperty(如限制數值范圍)
public sealed class ClampedReactiveProperty<T> : ReactiveProperty<T> where T : IComparable<T>
{private readonly T _min;private readonly T _max;private static readonly IComparer<T> _comparer = Comparer<T>.Default;// 構造函數:初始化時限制值在 [min, max] 范圍內public ClampedReactiveProperty(T initialValue, T min, T max): base(initialValue, EqualityComparer<T>.Default, callOnValueChangeInBaseConstructor: false){_min = min;_max = max;// 手動修正初始值(確保符合范圍)OnValueChanging(ref GetValueRef());}// 重寫值變更前的邏輯,強制限制范圍protected override void OnValueChanging(ref T value){if (_comparer.Compare(value, _min) < 0){value = _min; // 小于最小值時強制設為最小值}else if (_comparer.Compare(value, _max) > 0){value = _max; // 大于最大值時強制設為最大值}}
}// 3. XAML 數據綁定(以 WPF 為例)
public class PlayerViewModel
{// 可寫屬性(用于綁定輸入控件,如 Slider)public ReactiveProperty<int> Level { get; } = new ReactiveProperty<int>(1);// 只讀屬性(用于綁定顯示控件,如 TextBlock)public ReadOnlyReactiveProperty<string> LevelText { get; }public PlayerViewModel(){// 派生 LevelText(格式化為 "等級:X")LevelText = Level.Select(level => $"等級:{level}").ToReadOnlyReactiveProperty();}
}
3.2 Subject 自動釋放示例
R3 的 Subject 在 Dispose 時會自動觸發 OnCompleted,確保所有訂閱者收到完成通知并釋放資源:
csharp
using R3;
using System;var subject = new Subject<string>();// 訂閱 Subject
var subscription = subject.Subscribe(onNext: msg => Console.WriteLine($"收到消息:{msg}"),onCompleted: result => Console.WriteLine(result.IsFailure ? $"失敗:{result.Exception.Message}" : "成功完成")
);// 發送消息
subject.OnNext("Hello R3");// 釋放 Subject(會觸發 OnCompleted)
subject.Dispose();// 此時訂閱已自動取消,后續發送消息不會被接收
subject.OnNext("This message is ignored");
4. 靈活的 Disposable 管理
R3 提供多種 Disposable 組合方式,滿足不同場景的性能與靈活性需求,性能從高到低排序如下:
- `Disposable.Combine(d1, d2,..., d8)
:適用于已知數量(≤8個)的訂閱,內部使用字段存儲,性能最優。 2.?
Disposable.CreateBuilder():適用于動態數量但構建時已知的訂閱,Builder 為值類型,無內存分配。 3.?
Disposable.Combine(params IDisposable[]):適用于動態數量的訂閱,內部使用數組存儲。 4.?
DisposableBag:適用于動態添加的訂閱,輕量級值類型,非線程安全。 5.?
CompositeDisposable`:支持動態添加 / 移除,線程安全,但性能最低。
4.1 各 Disposable 用法示例
csharp
using R3;
using System;
using System.Reactive.Disposables;// 1. Disposable.Combine(已知3個訂閱,性能最優)
public class Example1
{private IDisposable _disposables;public void Initialize(){var d1 = Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Update 1"));var d2 = Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Update 2"));var d3 = Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Update 3"));// 組合3個訂閱,Dispose時會釋放所有_disposables = Disposable.Combine(d1, d2, d3);}public void Cleanup(){_disposables?.Dispose();}
}// 2. Disposable.CreateBuilder(動態添加但構建時確定數量)
public class Example2
{private IDisposable _disposables;public void Initialize(){var builder = Disposable.CreateBuilder();// 動態添加訂閱(數量在構建時確定)Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Builder 1")).AddTo(ref builder);Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Builder 2")).AddTo(ref builder);Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Builder 3")).AddTo(ref builder);// 構建組合訂閱_disposables = builder.Build();}public void Cleanup(){_disposables?.Dispose();}
}// 3. DisposableBag(動態添加,非線程安全)
public class Example3
{// DisposableBag 是值類型,無需 new,也不要拷貝private DisposableBag _disposableBag;public void Initialize(){// 初始添加訂閱Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Bag 1")).AddTo(ref _disposableBag);Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Bag 2")).AddTo(ref _disposableBag);}// 動態添加(如按鈕點擊時)public void OnButtonClick(){Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Dynamic Bag")).AddTo(ref _disposableBag);}public void Cleanup(){_disposableBag.Dispose();}
}// 4. CompositeDisposable(支持移除,線程安全)
public class Example4
{private CompositeDisposable _compositeDisposable = new CompositeDisposable();private IDisposable _dynamicSubscription;public void Initialize(){// 添加固定訂閱_compositeDisposable.Add(Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Composite 1")));_compositeDisposable.Add(Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Composite 2")));// 保存動態訂閱,后續可移除_dynamicSubscription = Observable.EveryUpdate().Subscribe(_ => Console.WriteLine("Dynamic Composite"));_compositeDisposable.Add(_dynamicSubscription);}// 移除動態訂閱public void RemoveDynamicSubscription(){_compositeDisposable.Remove(_dynamicSubscription);}public void Cleanup(){_compositeDisposable.Dispose();}
}
5. 與 async/await 深度集成
R3 針對 async/await 做了專門優化,支持異步操作符(如?SelectAwait
、WhereAwait
),并提供靈活的異步執行策略(如串行、并行、丟棄、切換)。
5.1 異步操作符示例(避免按鈕重復點擊)
csharp
using R3;
using System;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;// 按鈕點擊后發起網絡請求,使用 AwaitOperation.Drop 避免重復請求
public class AsyncExample : MonoBehaviour
{public Button RequestButton;public Text ResultText;private void Start(){RequestButton.OnClickAsObservable()// 異步轉換:發起網絡請求.SelectAwait(async (_, ct) =>{// 使用 UnityWebRequest 發起請求,并支持取消using var request = UnityWebRequest.Get("https://api.example.com/data");var operation = request.SendWebRequest();// 綁定取消令牌(當訂閱取消或完成時取消請求)using (ct.Register(() => operation.Abort())){await operation;}if (request.result != UnityWebRequest.Result.Success){throw new Exception($"請求失敗:{request.error}");}return request.downloadHandler.text;}, // 執行策略:當異步操作未完成時,丟棄新的點擊事件awaitOperation: AwaitOperation.Drop,// 訂閱完成時取消正在進行的異步操作cancelOnCompleted: true)// 訂閱結果并更新UI.Subscribe(result => ResultText.text = $"結果:{result}",error => ResultText.text = $"錯誤:{error.Message}")// 將訂閱綁定到當前 MonoBehaviour,銷毀時自動釋放.AddTo(this);}
}
5.2 異步執行策略(AwaitOperation)
策略 | 行為說明 |
---|---|
Sequential | 所有事件排隊,下一個事件等待前一個異步操作完成。 |
Drop | 異步操作執行期間,丟棄新的事件(如避免重復點擊)。 |
Switch | 新事件到來時,取消前一個未完成的異步操作,立即執行新操作(如搜索聯想)。 |
Parallel | 所有事件立即執行異步操作,不限制并發數。 |
SequentialParallel | 異步操作并行執行,但結果按事件順序傳遞給下一個操作符。 |
ThrottleFirstLast | 異步操作執行期間,僅保留第一個和最后一個事件(如批量處理)。 |
6. 單元測試支持
R3 提供完善的單元測試工具,支持時間 / 幀模擬,結合?LiveList
?可輕松驗證事件流結果。
6.1 時間模擬測試(使用 FakeTimeProvider)
csharp
using R3;
using Microsoft.Extensions.TimeProvider.Testing;
using Xunit;
using Shouldly;public class TimeProviderTests
{[Fact]public void Timer_Should_Trigger_After_Delay(){// 1. 創建虛假時間提供器var fakeTime = new FakeTimeProvider();// 2. 創建事件流:5秒后發送一個事件var timer = Observable.Timer(TimeSpan.FromSeconds(5), fakeTime);// 3. 將事件流轉換為 LiveList,便于斷言var results = timer.ToLiveList();// 斷言:提前4秒,事件未觸發fakeTime.Advance(TimeSpan.FromSeconds(4));results.AssertIsNotCompleted();results.AssertEmpty();// 斷言:再提前1秒(總計5秒),事件觸發fakeTime.Advance(TimeSpan.FromSeconds(1));results.AssertIsCompleted();results.AssertEqual([Unit.Default]);}
}
6.2 幀模擬測試(使用 FakeFrameProvider)
csharp
using R3;
using System;
using Xunit;
using Shouldly;public class FrameProviderTests
{[Fact]public void EveryUpdate_Should_Trigger_Per_Frame(){// 1. 創建虛假幀提供器var fakeFrameProvider = new FakeFrameProvider();var cts = new CancellationTokenSource();// 2. 創建事件流:每幀發送當前幀計數var everyUpdate = Observable.EveryUpdate(fakeFrameProvider, cts.Token).Select(_ => fakeFrameProvider.GetFrameCount());var results = everyUpdate.ToLiveList();// 斷言:初始狀態無事件results.AssertEmpty();// 斷言:推進1幀,收到幀計數0fakeFrameProvider.Advance();results.AssertEqual([0]);// 斷言:推進3幀,收到幀計數1、2、3fakeFrameProvider.Advance(3);results.AssertEqual([0, 1, 2, 3]);// 斷言:取消令牌,事件流完成cts.Cancel();results.AssertIsCompleted();// 斷言:繼續推進幀,無新事件fakeFrameProvider.Advance();results.AssertEqual([0, 1, 2, 3]);}
}// 測試輔助擴展方法
public static class LiveListExtensions
{public static void AssertEqual<T>(this LiveList<T> list, params T[] expected){list.ShouldBe(expected);}public static void AssertEmpty<T>(this LiveList<T> list){list.Count.ShouldBe(0);}public static void AssertIsCompleted<T>(this LiveList<T> list){list.IsCompleted.ShouldBeTrue();}public static void AssertIsNotCompleted<T>(this LiveList<T> list){list.IsCompleted.ShouldBeFalse();}
}
平臺支持
R3 支持多種 .NET 平臺,核心庫無需額外配置即可使用,平臺專用擴展需安裝對應 NuGet 包,并替換?TimeProvider
/FrameProvider
?以適配平臺特性。
1. 平臺支持列表
平臺 | NuGet 包名稱 | 核心適配點 |
---|---|---|
WPF | R3Extensions.WPF | 提供?WpfDispatcherTimeProvider (UI 線程時間)、WpfRenderingFrameProvider (渲染幀)。 |
Avalonia | R3Extensions.Avalonia | 提供?AvaloniaDispatcherTimeProvider 、AvaloniaRenderingFrameProvider (基于渲染事件)。 |
Uno | R3Extensions.Uno | 提供?UnoDispatcherTimeProvider 、UnoRenderingFrameProvider (跨平臺 UI 適配)。 |
MAUI | R3Extensions.Maui | 提供?MauiDispatcherTimeProvider 、MauiTickerFrameProvider (基于 Ticker 幀循環)。 |
WinForms | R3Extensions.WinForms | 提供?WinFormsTimeProvider 、WinFormsFrameProvider (基于消息循環)。 |
WinUI3 | R3Extensions.WinUI3 | 提供?WinUI3DispatcherTimeProvider 、WinUI3RenderingFrameProvider 。 |
Unity | R3 + R3.Unity | 提供?UnityTimeProvider (支持 Update/FixedUpdate 等生命周期)、UnityFrameProvider 。 |
Godot | R3 + R3.Godot | 提供?GodotTimeProvider (Process/PhysicsProcess)、GodotFrameProvider 。 |
Stride | R3Extensions.Stride | 提供?StrideTimeProvider 、StrideFrameProvider (游戲引擎幀循環)。 |
MonoGame | R3Extensions.MonoGame | 提供?MonoGameTimeProvider 、MonoGameFrameProvider (基于 Game.Update)。 |
LogicLooper | R3Extensions.LogicLooper | 提供?LogicLooperTimeProvider 、LogicLooperFrameProvider (循環邏輯適配)。 |
Blazor | R3Extensions.Blazor | 適配 Blazor 同步上下文,避免 UI 線程阻塞。 |
2. 平臺初始化示例(以 Unity 為例)
Unity 平臺需安裝兩個包:核心?R3
?和 Unity 專用擴展?R3.Unity
,步驟如下:
- 通過 NuGetForUnity 安裝?
R3
(搜索 “R3” 并安裝)。 - 引用 Git 地址安裝?
R3.Unity
:https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity
。 - (可選)指定版本:
https://github.com/Cysharp/R3.git?path=src/R3.Unity/Assets/R3.Unity#1.0.0
。
2.1 Unity 平臺核心用法
csharp
using R3;
using R3.Unity;
using UnityEngine;
using UnityEngine.UI;public class UnityExample : MonoBehaviour
{public Button AttackButton;public Text HpText;public Slider HpSlider;// 響應式屬性:角色生命值(初始1000)private ReactiveProperty<long> _currentHp = new ReactiveProperty<long>(1000);// 派生屬性:是否死亡(生命值 ≤ 0)private ReadOnlyReactiveProperty<bool> _isDead;private void Awake(){// 初始化派生屬性_isDead = _currentHp.Select(hp => hp <= 0).ToReadOnlyReactiveProperty();}private void Start(){// 1. 綁定生命值到 UI(自動同步更新)_currentHp.Subscribe(hp =>{HpText.text = $"HP: {hp}";HpSlider.value = hp;}).AddTo(this);// 2. 綁定死亡狀態到按鈕(死亡后禁用攻擊按鈕)_isDead.Subscribe(isDead =>{AttackButton.interactable = !isDead;if (isDead){HpText.text = "已死亡";}}).AddTo(this);// 3. 綁定按鈕點擊事件(每點擊一次減少100生命值)AttackButton.OnClickAsObservable().Subscribe(_ => _currentHp.Value -= 100).AddTo(this);// 4. 每幀檢測生命值變化(無 INotifyPropertyChanged 也可使用)Observable.EveryValueChanged(this, x => x.transform.position, UnityFrameProvider.Update).Subscribe(pos => Debug.Log($"當前位置: {pos}")).AddTo(this);// 5. 固定幀執行(如物理邏輯)Observable.EveryUpdate(UnityFrameProvider.FixedUpdate).Subscribe(_ =>{// 物理相關邏輯(如碰撞檢測)}).AddTo(this);}
}
與傳統 Rx 的兼容性
R3 提供與?IObservable<T>
(傳統 Rx)的雙向轉換,便于逐步遷移現有項目。
1. 轉換方法
轉換方向 | 方法 | 說明 |
---|---|---|
IObservable<T> → R3.Observable<T> | ToObservable() (擴展方法) | 將傳統 Rx 的可觀察對象轉換為 R3 的 Observable<T>。 |
R3.Observable<T> → IObservable<T> | AsSystemObservable() (擴展方法) | 將 R3 的 Observable<T> 轉換為傳統 Rx 的 IObservable<T>。 |
1.1 轉換示例
csharp
using R3;
using System.Reactive.Linq;
using System.Reactive.Subjects;public class CompatibilityExample
{public void ConvertFromSystemRx(){// 1. 創建傳統 Rx 的 Subjectvar systemSubject = new Subject<int>();// 2. 轉換為 R3 的 Observable<int>var r3Observable = systemSubject.ToObservable();// 3. 使用 R3 的操作符處理r3Observable.Where(x => x % 2 == 0).Delay(TimeSpan.FromSeconds(1)).Subscribe(x => Console.WriteLine($"R3 處理結果:{x}"));// 4. 發送事件(傳統 Rx 側發送,R3 側接收)systemSubject.OnNext(1); // 被過濾(奇數)systemSubject.OnNext(2); // 被 R3 接收并延遲1秒輸出}public void ConvertToSystemRx(){// 1. 創建 R3 的 Observablevar r3Observable = Observable.Interval(TimeSpan.FromSeconds(1));// 2. 轉換為傳統 Rx 的 IObservable<Unit>var systemObservable = r3Observable.AsSystemObservable();// 3. 使用傳統 Rx 的操作符處理systemObservable.Take(5).Subscribe(_ => Console.WriteLine("傳統 Rx 接收事件"),() => Console.WriteLine("傳統 Rx 事件流完成"));}
}
許可證
R3 基于?MIT 許可證