對於Linq查詢關鍵字及await,async非同步關鍵字的擴充套件使用

HONT發表於2021-12-09

最近在看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):

  1. 先呼叫t.GetAwaiter()方法,取得等待器a
  2. 呼叫a.IsCompleted取得布林型別b
  3. 如果b=true,則立即執行a.GetResult(),取得執行結果;
  4. 如果b=false,則看情況:
    1. 如果a沒實現ICriticalNotifyCompletion,則執行(a as INotifyCompletion).OnCompleted(action)
    2. 如果a實現了ICriticalNotifyCompletion,則執行(a as ICriticalNotifyCompletion).OnCompleted(action)
    3. 執行隨後暫停,OnCompleted完成後重新回到狀態機;

 

對於該介面的實現,這裡不考慮同步情況一律算作非同步,所以通過CoroutineRunner開啟一個協程式,

並在協程執行完成後呼叫mOnCompleted,通知c#的非同步可以往下執行了。

此處程式碼經過測試,全部是回撥函式實現的等待,並不會導致執行緒堵塞。

 

CoroutineRunner實現簡單的全域性協程託管,僅測試用:

對於Linq查詢關鍵字及await,async非同步關鍵字的擴充套件使用
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("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之類的操作,還是會引入很多多執行緒的東西。因此不建議混用。

相關文章