最近在看neuecc大佬寫的一些庫:https://neuecc.medium.com/,其中對await,async以及linq查詢關鍵字實現了神奇的擴充套件,
使其不需要引用對應名稱空間,不需要多執行緒就可以做一些自定義操作。因此進行學習,並在Unity3D下進行測試。
1.await,async關鍵字的自定義化擴充套件
只需要實現GetAwaiter公共方法即可,通過擴充套件方法實現也可以:
public static CoroutineAwaiter<WaitForSeconds> GetAwaiter(this WaitForSeconds instruction) { CoroutineAwaiter<WaitForSeconds> awaiter = new CoroutineAwaiter<WaitForSeconds>(instruction); return awaiter; }
該擴充套件方法可以實現Unity中的協程WaitForSeconds的非同步封裝。
這裡看到會返回一個型別,實際上c#編譯器關注返回的型別有沒有實現INotifyCompletion介面
或ICriticalNotifyCompletion介面,這裡以INotifyCompletion介面為例。
注意:此處程式碼參考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 }
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
完成後重新回到狀態機;
對於該介面的實現,這裡不考慮同步情況一律算作非同步,所以通過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主執行緒上, 並通過協程控制非同步邏輯執行。
2.Linq關鍵字的自定義化擴充套件
我們知道Linq可以寫出類似Sql風格的關鍵字:
int[] arr = new[] {1, 2, 3}; var r = from item in arr where item > 0 orderby item descending select item;
而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#關鍵字自定義化的介紹就寫到這裡,至於怎麼去用就仁者見仁智者見智了
這種寫法最大的好處是不會引入System.Linq或是System.Threading等名稱空間,
但如果要和多執行緒的非同步混用或者用Task.WaitAll之類的操作,還是會引入很多多執行緒的東西。因此不建議混用。