平常我們的程式都是由上到下按照順序執行,只要沒放錯位置,後面的功能都有辦法讀到前面方法輸出的結果。
但是如果碰到非同步 Async、協程 Coroutine 之類的會大幅改變程式的執行順序時,原本作法就不管用。在這篇文章中要教你如何用「Action」來解決這個問題,而不是用一個迴圈來頻繁檢查前面的作業是否已完成,是個能讓程式更靈活、更易讀的寫法。
Action 是 System 內的功能,它有分成無帶參數、有帶參數兩種用法。它在調用常會使用 Lambda 運算式,初次使用的開發者們可能會被這個外星文嚇跑,但其實這個東西並沒有你想得那麼複雜,其實它的邏輯很簡單!
基礎語法一
Action 在其他同樣是 C# 語言的框架中都能用,我們這邊就以 Unity 來說明。
以往我們對參數的認知都是「死」的,比方說我輸入整數 1,這個參數在方法中就只能做運算或再丟給其他方法使用,它不會為這個方法帶來新的東西。
那我們用 Action 來將方法變成參數,輸入到這個 RunJob 中如何?
1public void RunJob(System.Action onComplete)
2{
3 Debug.Log("作業中...");
4 onComplete();
5}
你可以發現我們輸入的 onComplete 參數居然可以使用調用方法的寫法來呼叫!這代表我們能在這個方法中的任意位置調用 onComplete,能依照你給的東西來改變程式最終的結果。
那麼我們要怎麼把方法當參數輸入進去呢?
一、再寫一個方法
目前我們是不額外帶參數的 Action,所以我們可以再寫一個無參數的方法 PrintTips,並再調用 RunJob 時將 PrintTips 直接當參數帶入,僅名稱就好,不可加上括弧。
有括弧就是方法調用了,語法錯誤 ❌⛔
1private void Start()
2{
3 RunJob(PrintTips);
4}
5public void PrintTips()
6{
7 Debug.Log("初次登入時請記得修改密碼!");
8 Debug.Log("不能忘喔!");
9}
二、Lambda
Lambda 的規則說起來麻煩,就不解釋了。我們這邊聚焦在要怎麼直接上手用它。請參考以下:
1RunJob(() => {
2 Debug.Log("初次登入時請記得修改密碼!");
3 Debug.Log("不能忘喔!");
4});
首先因為現在沒有讓 Action 也帶入參數,所以要用 () 空括弧來代表 => 後的方法內容,大括弧中的內容就是你的自由發揮空間。
如果裡面只有一行程式碼,還可以省略掉大括弧與結尾分號:
1RunJob(() => Debug.Log("初次登入時請記得修改密碼!"));
兩種方式的輸出結果都一樣:
實作練習一
請使用一個無帶參數的 Action 並搭配方法,在開始時印出「開始任務…」後過三秒,再經由輸入的 Action 中打印「任務完成!」字串。
參考解答
1using System.Collections;
2using UnityEngine;
3public class ActionTutorial : MonoBehaviour
4{
5 private void Start()
6 {
7 RunJob(() => {
8 Debug.Log("任務完成!");
9 });
10 }
11 public void RunJob(System.Action onComplete)
12 {
13 Debug.Log("開始任務...");
14 StartCoroutine(JobTask(onComplete));
15 }
16 private IEnumerator JobTask(System.Action onComplete)
17 {
18 yield return new WaitForSecondsRealtime(3);
19 onComplete();
20 }
21}
基礎語法二
前面我們講過無帶參數的用法了,現在來介紹有帶參數要怎麼寫。
在寫方法的時候,在定義的 Action 類別後加上 <類別>。例如你想它帶一個整數,那就是 System.Action<int>
,如果要帶布林值,那就寫 System.Action<bool>
。
最後在調用 Action 時帶入你定義的型別資料,就可以成功帶入囉!
1public void RunJob(System.Action<int> onComplete)
2{
3 Debug.Log("開始隨機取號...");
4 onComplete(Random.Range(0, 1000));
5}
如果想要帶入更多筆資料,除了用 Struct、Class 之外,也可以這樣做:
1public void RunJob(System.Action<int, string> onComplete)
2{
3 Debug.Log("開始隨機取號...");
4 onComplete(Random.Range(0, 1000), "成功的男人背後都有一條脊椎");
5}
用逗點分隔就能加參數,很有趣吧?(最多可以加 16 個)
那要怎麼接收帶入的參數呢?
一、用另一個方法
假設 RunJob 的 Action 有帶入一個整數,那就在輸入的方法中加入一個整數參數即可。
1private void Start()
2{
3 RunJob(PrintTips);
4}
5public void PrintTips(int value)
6{
7 Debug.Log("你抽到的號碼是" + value);
8 Debug.Log("恭喜你囉!");
9}
多參數的寫法如下,以此類推。
1public void PrintTips(int value, string data)
2{
3 Debug.Log("你抽到的號碼是" + value);
4 Debug.Log("恭喜你囉!");
5 Debug.Log(data);
6}
二、Lambda
如果你僅帶入一個值,那可以用 xyz、abc 或你隨便亂取的名稱來做為暫存用變數,最後就能在定義的區塊隨意調用它。
1RunJob(x => {
2 Debug.Log("你抽到的號碼是" + x);
3 Debug.Log("恭喜你囉!");
4});
多參數的寫法如下,需用括弧包住多筆參數,以此類推。
1RunJob((x, y) => {
2 Debug.Log("你抽到的號碼是" + x);
3 Debug.Log("恭喜你囉!");
4 Debug.Log(y);
5});
兩種方法的輸出結果都一樣:
實作練習二
請用有帶參數的 Action 並搭配方法,輸出一個範圍 -1000~1000 隨機取值的 XY 二維座標,並判斷這個座標是否在地底(看 Y 是否小於 0)。
參考解答
1using UnityEngine;
2public class ActionTutorial : MonoBehaviour
3{
4 private void Start()
5 {
6 RunJob((a, b) => {
7 Debug.Log("座標: " + a);
8 Debug.Log("是否在地底: " + b);
9 });
10 }
11 public void RunJob(System.Action<Vector2, bool> onComplete)
12 {
13 int xPos = Random.Range(-1000, 1000);
14 int yPos = Random.Range(-1000, 1000);
15 bool isUnderGround = yPos < 0;
16 onComplete(new Vector2(xPos, yPos), isUnderGround);
17 }
18}