【转】Unity中的协同程序-使用Promise进行封装(二) 译者:王磊(未来的未来)    审校:崔国军(飞扬971) 在上一篇文章中,我们的注意力主要是放在Unity的协同程序的内部机制以及深入讨论了下它们是如何工作的。我们涉及了IEnumerator接口和迭代器模块来了解了下引擎是如何实现协同程序功能的。我们还列出了几个在实现中会遇到的问题以及在需要编写更复杂的协同程序的时候,可能会偶然发现的缺点。 在今天这篇文章中,我们将向你展示Promise到底是一个什么样子的概念以及它们最初起源自什么地方。

原文:http://gad.qq.com/program/translateview/7170970

 

在上一篇文章中,我们的注意力主要是放在Unity的协同程序的内部机制以及深入讨论了下它们是如何工作的。我们涉及了IEnumerator接口和迭代器模块来了解了下引擎是如何实现协同程序功能的。我们还列出了几个在实现中会遇到的问题以及在需要编写更复杂的协同程序的时候,可能会偶然发现的缺点。

 

在今天这篇文章中,我们将向你展示Promise到底是一个什么样子的概念以及它们最初起源自什么地方。

 

回调函数

Javascript API中的大多数异步操作在完成工作的时候会返回一个回调函数作为结果(用c#术语来表示的话:就是一个Action的委托)。你可以显式地为那些异步操作提供一个函数,但遵循这些异步代码的规则使用匿名函数实现回调会容易许多。理论上它似乎是我们的协同程序的一个像样的升级版本。让我们试着举出一个如何在C #程序中做这个事情的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
StartCoroutine(RequestData( (error, message) =>
{
    if (error)
    {
        Debug.Log(string.Format("We got an error: {0}", message));
    }
    else
    {
        StartCoroutine(RequestSomeOtherData(message, (error2, message2) =>
        {
            if (error2)
            {
                Debug.Log(string.Format("We got an error: {0}", message2));
            }
            else
            {
                Debug.Log(string.Format("Success: {0}", message2));
            }
        }));
    }
}));
  
// ...
  
IEnumerator RequestData(Action<bool, string=""> onComplete)
{
    // Do some real work
    yield return new WaitForEndOfFrame();
    //--
  
    // No errors
    onComplete(false, "Data 1 working");
}
  
IEnumerator RequestSomeOtherData(string dataArgument, Action<bool, string=""> onComplete)
{
    // Do some real work with dataArgument
    yield return new WaitForEndOfFrame();
    //--
  
    // No errors
    onComplete(false, "Data 2 woohoo");
}</bool,></bool,>

这并不太坏! 正如你所看到的那样,这个程序的效果相当好。它给了我们一个可能,就是在返回一个值的同时报告错误和对这个错误进行反应。但是,这个问题在于你堆栈协同程序的时候会有更多水平的代码缩进。此外,错误检查必须在每一个地方都要进行一次。最终,这个更复杂的函数通过一个回调函数结束了。那到底是什么?好吧,让我们复制协同程序并把这四个坏男孩放入堆栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
StartCoroutine(RequestData( (error, message) =>
{
    if (error)
    {
        Debug.Log(string.Format("We got an error: {0}", message));
    }
    else
    {
        StartCoroutine(RequestSomeOtherData(message, (error2, message2) =>
        {
            if (error2)
            {
                Debug.Log(string.Format("We got an error: {0}", message2));
            }
            else
            {
                StartCoroutine(RequestSomeOtherData(message, (error3, message3) =>
                {
                    if (error3)
                    {
                        Debug.Log(string.Format("We got an error: {0}", message3));
                    }
                    else
                    {
                        StartCoroutine(RequestSomeOtherData(message, (error4, message4) =>
                        {
                            if (error4)
                            {
                                Debug.Log(string.Format("We got an error: {0}", message4));
                            }
                            else
                            {
                                Debug.Log(string.Format("Success: {0}", message4));
                            }
                        }));
                    }
                }));
            }
        }));
    }
}));

看下这段程序里面的金字塔形状以及在程序结尾的所有的} }));。呵呵!你还需要得到一些真正有创意的名字,因为它们必须是唯一的。关于错误处理,如果你把错误处理移动到另一个函数的话,你可以稍微整理一下代码。但它仍然看起来不怎么吸引人,这里我们只有四个协同程序放入了堆栈。当编写更复杂的函数或者为你的后端服务编写api的时候,这一数字可以有极大的增加。那么,解决方案是什么?

Promise

Promise在ES6中本地化实现之前,就已经通过许多第三方的Javascript库得到了推广。有趣的是,这个想法本身实际上已经在80年代和70年代进行过研究和介绍。我们今天所知道的Promise是芭芭拉 李斯柯夫和柳吧谢拉在1988年首次引入的,但类似的想法来自于很多年前的函数式编程语言。

一个符合Promise和A +规范的c#库几年前被正式推出,这对于管理异步操作来说是一个很大的游戏规则的改变。让我们深入代码来了解下。

一个Promise就是一个你从异步函数立即返回的对象,所以这是一个调用者可以等待决议(或者错误)的操作。所以从本质上说,它代表了当Promise创建的时候对于一个不确定知道的值的代理。一旦你完成你的行动以后,这个Promise可以决议或者被拒绝。这允许异步方法返回值并像同步方法那样运行:除了得到的不是最终的结果以外,它们返回的是一个在未来某一个时间点有有一个确定值得承诺。然而,这么做最好的部分在于用一个简单的接口进行简单的叠加。下面是我们如何重新实现我们的第一个例子并且不用纯粹的Action委托来实现我们的Promise:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using RSG;
//...
 
RequestData()
.Then(result =>
{
    return RequestSomeOtherData(result);
})
.Then(result =>
{
    Debug.Log(string.Format("Success: {0}", result));
})
.Catch(error => { // Exception error
    Debug.Log(string.Format("We got an error: {0}", error.Message));
});
  
//...
  
IPromise RequestData()
{
    var promise = new Promise();
    StartCoroutine(_RequestData(promise));
    return promise;
}
  
IEnumerator _RequestData(Promise promise)
{
    // Do some real work
    yield return new WaitForEndOfFrame();
    //--
  
    // No errors
    promise.Resolve("Data 1 working");
}
  
  
IPromise RequestSomeOtherData(string dataArgument)
{
    var promise = new Promise();
    StartCoroutine(_RequestSomeOtherData(dataArgument, promise));
    return promise;
}
  
IEnumerator _RequestSomeOtherData(string dataArgument, Promise promise)
{
    // Do some real work with dataArgument
    yield return new WaitForEndOfFrame();
    //--
  
    // No errors
    promise.Resolve("Data 2 woohoo");
}

这种方法对于调用者来说更棒! 你需要隐藏和封装协同程序的行为从而获得Promise这种方法的所有优势。通过.Then()函数,你可以很容易地用一个逻辑顺序来将新的Promise放入堆栈并且处理不同的结果类型。此外,通过.Catch()函数,你可以捕获所有发生在你的Promise里面的异常。如果任何上述Promise被拒绝的话,那么整个Promise的链会立即终止,然后代码将跳转到最近的catch语句。不仅如此,catch函数实际使用的是异常类型,所以你可以很容易地抛出自己的异常类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class MyException : Exception
{
    private readonly int foo;
  
    public int Foo
    {
        get { return foo; }
    }
  
    public MyException(int foo)
    {
        this.foo = foo;
    }
  
    public MyException(int foo, string message)
        : base(message)
    {
        this.foo = foo;
    }
  
    public MyException(int foo, string message, Exception inner)
        : base(message, inner)
    {
        this.foo = foo;
    }
}
  
IEnumerator _RequestData(Promise promise)
{
    // Do some real work
    yield return new WaitForEndOfFrame();
    //--
  
    // Error!
    promise.Reject(new MyException(100, "Error error!"));
}

只要你从异常类继续,就可以编写自己的异常类来模仿try。。。catch行为。像下面这样进行处理:

1
2
3
4
5
6
7
.Catch(error => { // Exception error
    if (error is MyException)
    {
        var myError = error as MyException;
        Debug.Log(string.Format("We got an error: {0} {1}", myError.Message, myError.Foo));
    }
});

总结

正如你可以看到的那样,Promise提供了一个伟大的机制来处理你的异步操作。你可以很容易地把错误从你的堆栈中的任何Promise里面抛出,如果你使用这个机制并与Unity合作的话,你可以在处理返回值的同时隐藏协同程序的实现。你在保留协同程序所有强大的方面的同时,还大大提高了你的复杂api和服务的接口。这个库提供了很多方面的内容,我们强烈建议你看下相关的文档来看看这个库还提供了哪些内容。

在这个系列的最后一部分之中,我们将向你展示一个真实的例子,通过使用Unity的协同程序以及Promise来编写一个REST API接口来得到尽可能最干净的代码。

我们会在这个系列的第三篇文章里面继续这个话题。