最近在看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; } }View Code
最終使用代碼如下:
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;
而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#關鍵字自定義化的介紹就寫到這里,至于怎么去用就仁者見仁智者見智了
這種寫法最大的好處是不會引入System.Linq或是System.Threading等命名空間,
但如果要和多執行緒的異步混用或者用Task.WaitAll之類的操作,則避免不了;這種情況還需要甚用,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/378152.html
標籤:其他
