对于Linq关键字和await,async异步关键字的扩展使用

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实现简单的全局协程托管,仅测试用:

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!");
    }
}

 

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之类的操作,还是会引入很多多线程的东西。因此不建议混用。