最近在看neuecc大佬寫的一些庫:https://neuecc.medium.com/,其中對await
,async
以及Linq查詢關鍵字做了一些神奇的擴展,
使其可以拿來做些自定義操作,並且不需要引用System.Linq
之類的對應命名空間。
關於這些功能的實現,對此進行了學習並在Unity3D下進行測試。
1.await,async關鍵字的自定義擴展
對於await
關鍵字的自定義擴展,只需要實現GetAwaiter
公共方法即可,通過擴展方法實現也可以:
public static CoroutineAwaiter<WaitForSeconds> GetAwaiter(this WaitForSeconds instruction) { CoroutineAwaiter<WaitForSeconds> awaiter = new CoroutineAwaiter<WaitForSeconds>(instruction); return awaiter; }
遇到await
關鍵字時,實際會去執行GetAwaiter
部分的內容。
而如上的擴展方法是通過await
實現Unity中的協程WaitForSeconds
的異步封裝。
上面的方法還會看到一個返回類型,c#編譯器會關注返回的類型是否實現INotifyCompletion
接口
(或實現ICriticalNotifyCompletion
接口)
注:此處代碼參考Unity3dAsyncAwaitUtil(https://github.com/modesttree/Unity3dAsyncAwaitUtil)
對於返回類型,CoroutineAwaiter<WaitForSeconds>
其實現如下:
public class CoroutineAwaiter<T> : INotifyCompletion where T : YieldInstruction { private T mValue; private Action mOnCompleted; public bool IsCompleted => false; public CoroutineAwaiter(T value) { mValue = value; } public T GetResult() => default; private IEnumerator CoroutineExec() { yield return mValue; mOnCompleted(); } #region INotifyCompletion void INotifyCompletion.OnCompleted(Action onCompleted) { mOnCompleted = onCompleted; CoroutineRunner.Instance.StartCoroutine(CoroutineExec()); } #endregion }
那么返回的INotifyCompletion
接口對象,c#會做如下操作,參考知乎(https://zhuanlan.zhihu.com/p/121792448):
- 先調用
t.GetAwaiter()
方法,取得等待器a
;- 調用
a.IsCompleted
取得布爾類型b
;- 如果
b=true
,則立即執行a.GetResult()
,取得運行結果;- 如果
b=false
,則看情況:
- 如果
a
沒實現ICriticalNotifyCompletion
,則執行(a as INotifyCompletion).OnCompleted(action)
- 如果
a
實現了ICriticalNotifyCompletion
,則執行(a as ICriticalNotifyCompletion).OnCompleted(action)
- 執行隨后暫停,
OnCompleted
完成后重新回到狀態機;
對於該接口的實現,為了方便舉例;這里不考慮同步情況而是都算作異步處理
private IEnumerator CoroutineExec() { yield return mValue; mOnCompleted(); } #region INotifyCompletion void INotifyCompletion.OnCompleted(Action onCompleted) { mOnCompleted = onCompleted; CoroutineRunner.Instance.StartCoroutine(CoroutineExec()); } #endregion
所以OnCompleted
中,通過CoroutineRunner
開啟一個協程,並在協程執行完后調用mOnCompleted
,通知c#的異步可以繼續往下執行了。
此處代碼經過測試,全部是主線程回調函數實現的等待,並不會導致線程堵塞或是開在新線程上去執行。
CoroutineRunner
實現簡單的全局協程托管,該類僅測試用:

using UnityEngine; public class CoroutineRunner : MonoBehaviour { private static CoroutineRunner sInstance; public static CoroutineRunner Instance => sInstance; private void Awake() { sInstance = this; } }
最終使用代碼如下:
public class Test1 : MonoBehaviour { public void Start() { _ = WaitForSecondsExecTest(); //繞過警告提示 } async Task WaitForSecondsExecTest() { Debug.Log("Waiting 1 second..."); await new WaitForSeconds(1f); Debug.Log("Done!"); } }
這段代碼運行在unity主線程上, 並通過協程控制異步邏輯執行。
因為迭代器代碼並不直接暴露,因此對try catch異常捕獲較為友好。
2.Linq關鍵字的自定義擴展
我們知道Linq可以寫出類似SQL風格的語句:
int[] arr = new[] {1, 2, 3}; var r = from item in arr where item > 0 orderby item descending select item;
而c#開源庫UniRx拿這些關鍵字做了一些非集合查詢的自定義操作:
// composing asynchronous sequence with LINQ query expressions var query = from google in ObservableWWW.Get("http://google.com/") from bing in ObservableWWW.Get("http://bing.com/") from unknown in ObservableWWW.Get(google + bing) select new { google, bing, unknown }; var cancel = query.Subscribe(x => Debug.Log(x)); // Call Dispose is cancel. cancel.Dispose();
(該段代碼位於Sample01_ObservableWWW.cs中, UniRx地址:https://github.com/neuecc/UniRx)
這么神奇,那么是怎么實現的呢?
研究了下它的代碼,發現實現這樣的操作和GetAwaiter
類似,只需包含與默認名稱一致的公共方法即可。
但是后來又發現,類型還必須包含一個泛型,C#編譯器才可以成功識別:
public class Test : MonoBehaviour { public class Result<T>//此處需有一個泛型才行 { public int Select<TOut>(Func<T, TOut> selector) { return 12; } } private void Start() { Result<int> r = new Result<int>(); var rInt = from item in r select new {item}; Debug.Log("rInt: " + rInt); //return 12. } }
這樣就實現了select
關鍵字的自定義化操作,而對於where
、skip
等操作類似,不再舉例。
最后c#關鍵字自定義化的介紹就寫到這里,至於怎么去用就仁者見仁智者見智了。