對於Linq查詢關鍵字及await,async非同步關鍵字的擴展使用
最近在看neuecc大佬寫的一些庫://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(//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#對該介面的調用流程,參考知乎(//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; } }
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;
而unirx庫拿這些關鍵字做了一些非查詢的自定義操作:
// composing asynchronous sequence with LINQ query expressions var query = from google in ObservableWWW.Get("//google.com/") from bing in ObservableWWW.Get("//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地址://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之類的操作,還是會引入很多多執行緒的東西。因此不建議混用。