見出し画像

[Unity小ネタ]デリゲート、イベント、ラムダ式、非同期処理をまとめて考える

それぞれ独立したトピックスだが、まとめて理解したほうが色々と腑に落ちるんじゃないかと思う

デリゲート

C言語で言うところの関数ポインタ
関数の変数だ

delegate

delegate void SomeFunctionType(int a);

こう書くと、SomeFunctionType(int a)という型を定義したことになり、
例えば以下のような定義と同じ構造を持った関数があると

    private void Do(int val)
    {
        Debug.Log("Do!"+ val);
    }

このように関数の変数に関数を代入して使うことができる

    void Start()
    {
        SomeFunctionType A = Do;
        A(100);
    }

delegateは内部構造的には関数のリストになっているらしく、
複数の関数を+=を使って登録し同時に実行することもできる

    private void Do(int a)
    {
        Debug.Log("Do!"+ a);
    }
    private void Da(int a)
    {
        Debug.Log("Da!"+ a);
    }

    void Start()
    {
        SomeFunctionType A = Do;
        A += Da;
        A(100);
    }

-=を使って関数を削ることもできる

 A -= Da;

Action

delegateキーワードを使って関数型を定義するのが基本だが、
Actionという型を使うことのほうが多い
個別に型の定義するのではなく、どんな形の関数でもAction<>という型で定義するやりかただ

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class GameObject : MonoBehaviour
{
    private void Do(int a)
    {
        Debug.Log("Do!"+ a);
    }
    private void Da(int a)
    {
        Debug.Log("Da!"+ a);
    }

    void Start()
    {
        Action<int> A = Do;
        A += Da;
        A(100);
    }
}

Action<int>とdelegate SomeFunctionType(int a)は同じ意味の定義になる
Action<float,float>とかAction<string>とか引数にあわせどんな関数でもこれでいけるので結果的にdelegateを使う必要はない

ActionクラスはSystemに定義されているので頭に
using System;
を追加している

Func

Actionは戻り値が無い(void)関数を定義するときに使う、Funcは戻り値がある関数の時に使う
それ以外はActionもFuncも同じようなものだ

public class GameObject : MonoBehaviour
{
    private int Do(int a, int b)
    {
        return a+b;
    }

    void Start()
    {
        System.Func<int, int, int> A = Do;
        Debug.Log(A(100,200));
    }
}

Func<int, int, int>は、最初のintが戻り値で続く2つが引数の定義だ
つまり
delegate int Func(int a, int b);
を表現している

イベント

C#にはデリゲートを利用してイベントハンドラを設計するための仕組みがある

ある汎用型の人工知能クラスがあったとする
状況によって怒ったり喜んだりできるので、利用者はどの感情のときにどのような動作をさせるかを登録して使うことができるクラスだ

ここでいう感情がイベント、登録する動作がコールバック関数
登録された関数を条件に応じて呼び出す仕組みがイベントハンドラと呼ばれるものだ

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

public class SuperAI
{
    public event Action happyHandler;
    public event Action angryHandler;

    public void Update()
    {
        float a =UnityEngine.Random.value;
        if(a > 0.7)
            happyHandler?.Invoke();
        else if(a > 0.5)
            angryHandler?.Invoke();
    }

}

public class GameObject : MonoBehaviour
{
    SuperAI ai = new SuperAI();

    private void Do()
    {
        Debug.Log("嬉しいな");
    }
    private void Da()
    {
        Debug.Log("怒ったぞ");
    }

    void Start()
    {
        ai.happyHandler += Do;
        ai.angryHandler += Da;
    }

    void Update()
    {
        ai.Update();        
    }
}

public event Action happyHandler;
public event Action angryHandler;

この2行はデリゲートにeventというキーワードが付いただけだだが、
eventを付けることで
ai.happyHandler = Do;
のような直接代入の禁止
ai.happyHandler()
のような直接呼出しの禁止
また、この宣言自体がクラスの中でのみ定義が許されるようになる
といった制約がかかることになる

より安全なイベントハンドラを定義するための仕組みのようだ

ラムダ式

人のソースコードを呼んでいると=>という記号が出てくることがある
これはラムダ式と呼ばれる関数を簡易的に記述する表記法だ
関数名が必要ないので匿名関数と言われたりもする

int Add(int x, int y)
{
    return x + y;
}

この関数を

(int x, int y) => { return x + y; };

こんなふうに書くことができる
(引数) => {return 処理;};
という書式でちょっとした関数を記述できる機能だ

また、1行で終わる関数なら、{}もreturnも省略して

(x, y) => x + y;

こんな風に記述することもできる

デリゲートに代入してコールすることもできる

Func<int, int, int> func1 = (x, y) => x + y;
Debug.Log(func1(100,200));

この後説明する非同期処理などでは、引数にActionやFuncのdelegate型を持つ関数が存在する
そこにパラメータとして関数を渡す場合にラムダ式が必要になる
上のほうで書いたイベントハンドラを登録する処理も、ラムダ式を使ってスッキリさせることもできる

public class GameObject : MonoBehaviour
{
    SuperAI ai = new SuperAI();

    void Start()
    {
        ai.happyHandler += ()=>Debug.Log("嬉しいな");
        ai.angryHandler += ()=>Debug.Log("怒ったぞ");
    }

    void Update()
    {
        ai.Update();        
    }
}

非同期処理

ファイルのロード中にNow Loadingという文字をアニメーションさせたり
ネットワークゲームでキャラクターを動かしながらサーバーと通信したり
といった複数同時の処理のことだ
非同期といいつつ一方の処理の完了を他方が待つなど要所要所で同期をとる仕組みが備わっている

async / await

まず普通の処理(同期処理)

using UnityEngine;
using System.Threading.Tasks;   //Task.Delay()関数を使うために必要

public class GameObject : MonoBehaviour
{
    void Func1()
    {
        Debug.Log("Func1 Start.");
        Task.Delay(1000);
        Debug.Log("Func1 End.");
    }

    void Func2()
    {
        Debug.Log("Func2 Start.");
        Task.Delay(1000);
        Debug.Log("Func2 End.");
    }

    void Start()
    {
        Func1();
        Func2();
        Debug.Log("Start End.");
    }
}

処理の流れはこんな感じ

コンソールには

Func1 Start
Func1 End
Func2 Start
Func2 End
Start End

と出る

これをasync / awaitを使って非同期処理にかえてみる

using UnityEngine;
using System.Threading.Tasks;   //Task.Delay()関数を使うために必要

public class GameObject : MonoBehaviour
{
    async void Func1()
    {
        Debug.Log("Func1 Start.");
        await Task.Delay(1000);
        Debug.Log("Func1 End.");
    }

    async void Func2()
    {
        Debug.Log("Func2 Start.");
        await Task.Delay(1000);
        Debug.Log("Func2 End.");
    }

    void Start()
    {
        Func1();
        Func2();
        Debug.Log("Start End.");
    }
}

async / awaitを追加するとFunc1,Func2は非同期関数に生まれ変り、Start(), Func1(), Func2()がそれぞれ並列に動くようになる

コンソールの結果はこうなる

Func1 Start
Func2 Start
Start End
Func1 End
Func2 End

asyncのついた関数の中には必ず1つはawaitをいれなければいけないルールがある
awaitは非同期処理のなかで確実に完了するまでスレッドを終了させてはいけない処理の意味で
await Task Func();
という書式の関数でなければいけない
Task.Delay()は指定した時間だけスレッドを止める関数だ

Task.Run()

Task.Run()を使って関数を非同期に呼ぶこともできる

using UnityEngine;
using System.Threading.Tasks;   //Task.Run()関数を使うために必要

public class GameObject : MonoBehaviour
{
    void SomeTask()
    {
        for(int i=0; i< 100;i++)
            Debug.Log(i);
    }

    void Start()
    {
        Task.Run(() => {SomeTask();} );
        Debug.Log("Start End");
    }
}

Task.Run()の引数にはデリゲートを指定する必要があるので
Task.Run(()=>{SomeTask();});
とラムダ式で書かないといけない
Task.Run(SomeTask());
とは書けない

こう書いたほうがもっとスッキリする

using UnityEngine;
using System.Threading.Tasks;

public class GameObject : MonoBehaviour
{
    void Start()
    {
        Task.Run(() => {
            for(int i=0; i< 100;i++)
                Debug.Log(i);
        } );
        Debug.Log("Start End");
    }
}


この記事が気に入ったらサポートをしてみませんか?