今回の記事ではデリゲートとイベントについて解説していきます。
前回は辞書の使い方について解説しましたね。
デリゲートとイベントを使うことでメソッドを値として使用することができます。メソッドを値にして利点があるのかと思われますが、アプリでできることが広がるので重要なC#の機能になります。
今回の内容は当サイトのUnity C#入門講座の中でも高難易度な箇所になります。もし一回でわからなくても挫折しないようにしましょう。
わかるところまで理解して、次へ進み、また適宜戻ってくるのもよいでしょう。
今回の講座を理解すればUnityのイベントの実装の仕組みがわかるようになります。
デリゲートとイベントとは
デリゲートおよびイベントは参照型になります。それぞれメソッドを値として扱うための型になります。
デリゲートについて
デリゲート(Delegate)とはメソッドを表すものになります。そのため定義の書き方はメソッドと似ています。
デリゲートの定義の書き方:delegate <戻り値> <デリゲート名> (<引数>, …)
定義したデリゲートは型のように扱いますが、sealedキーワードが自動的に指定されているためそれ以上派生できません。
デリゲートには以下の演算子が用意されており、これらの演算子でメソッドの管理ができます。
- +演算子:2つのデリゲートを一つにまとめる。
- -演算子:右辺値側のメソッドがあったらそれを削除する。
また、デリゲートはメソッドのように引数を渡す事で設定されたメソッドを全て実行することができます。また、Invokeメソッドを使用しても同じ結果が得られます。
- メソッド形式:
<デリゲート型の変数>(<引数>, …);
- Invokeメソッド:
<デリゲート型の変数>.Invoke(<引数>, …);
ただし、デリゲートに何もメソッドが設定されていない場合にメソッドを実行しようとすると例外が発生します。なので、基本的には?演算子とInvokeメソッドを組み合わせてメソッドを呼び出すのがオススメです。
- オススメなメソッドの呼び出し方:
<デリゲート型の変数>?.Invoke(<引数>, …);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class A { public int Method1(int a) => a + 10; public int Method2(int a) => a - 10; public int Method3(int a) => a * 10; } delegate int Delegate1(int a); var inst = new A(); Delegate1 del1 = inst.Method1; Delegate1 del2 = inst.Method2; Delegate1 del3 = del1 + del2; int r1 = del1(10); // <- inst.Method1(10)が呼び出される。r1 == 20 int r2 = del2.Invoke(20); // <- inst.Method2(20)が呼び出される。r2 == 10 //inst.Method1(30), inst.Method2(30)が順に呼び出される。 // r3 には最後に呼び出されたメソッドの戻り値が呼び出される。 int r3 = del3?.Invoke(30); // <- r3 == 20 del3 -= inst.Method1; // <- del3からinst.Method1が削除される。 |
アロー演算子(=>)の使い方がわからなくなってる人はメソッドの記事を復習してみてね。
GetInvocationListメソッドについて
デリゲート型にはGetInvocationListメソッドというメンバメソッドが定義されています。このメソッドを使用することでデリゲート型に設定されているメソッドのリストを取得することができます。
実際にメソッドの情報にアクセスするにはリフレクションの知識が必要になってきます。これはデリゲートの高度な使い方になります。
1 2 3 4 5 6 7 8 9 10 |
using System.Reflection; delegate void Delegate1(int); Delegate1 del = (a) => { a+=10; } del += (b) => { b -= 10; }; foreach(System.Delegate method in del.GetInvocationList()) { //methodInfoからメソッドの情報を取得していく。 } |
無名関数とラムダ関数ついて
デリゲート型にはクラスのメンバメソッド以外にも無名関数やラムダ関数も設定することができます。
無名関数とラムダ関数は似たものでメソッド内でメソッドの定義を書くことができるC#の機能になります。
- 無名関数の書き方:
delegate(引数, …) { <処理>… };
- ラムダ関数の書き方:
(<引数>, …) => { <処理>… };
。(引数が一つの時は丸かっこは省略できます。)
これらのメソッドの設定方法は同じですが、削除する際にどこのメソッドなのか判別することができないのでそのままでは削除できません。
削除したい場合は一度デリゲート型の変数に設定した後にその変数を使用することで削除できます。また、GetInvocationInfoメソッドとリフレクションを使用し削除したいメソッドを特定することでもできますが、こちらの方法は少々難しいやり方になります。
1 2 3 4 5 6 7 8 9 |
delegate int Delgate1(int a); Delegate1 del1 = delegate(int a) { return a + 10 }; //無名関数 Delegate1 del2 = (int a) => { return a - 10; } // ラムダ関数 Delegate1 del3 = del1 + del2; //削除する時はデリゲート型の変数を使う del3 -= del1; // <- 無名関数を削除 del3 -= del2; // <- ラムダ関数を削除 |
戻り値およびref、outキーワードがついた引数の扱いについて
デリゲートでも設定したメソッドの戻り値を取得できたり、引数についたrefやoutキーワードなどでメソッドから値を設定できます。
その場合の注意点として、戻り値および引数に設定される値はデリゲートの中で最後に呼び出されたメソッド一つだけになりますので注意してください。
1 2 3 4 5 6 7 8 |
delegate void Delegate1(ref int a); Delegate1 del = (ref int a) => { a += 10; }; del += (ref int a) => { a -= 10; }; int n = 100; del(ref n); //<- nには最後に設定されたメソッドのものが反映される。 bool b = (n == 90); // <- b == true |
System.Delegate型について
少しマニアックな解説になりますが全てのデリゲート型はSystem.Delegate
型から派生した型になります。
System.Delegate
型は一つのメソッドしか表さないので、複数個のメソッドを扱いたい時はSystem.MulticastDelegate
というSystem.Delegate
型から派生したクラスが実際には使われています。
C#がこれらの型を裏側で上手い事使ってデリゲートを実現しているので、豆知識程度に覚えておくといいでしょう。
イベントについて
イベント(Event)はクラスのメンバの一種で指定したデリゲート型を簡単に扱うためのものです。
イベントを定義したクラスにはデリゲートと同じく複数のメソッドを設定することができ、そのクラス内部でのみ設定したメソッドを呼び出すことができます。(内部的にはデリゲートのInvoke()メソッドを使用しています。)
以下の演算子およびメソッドがあります。
- メソッドの追加:+=, add()
- メソッドの削除:-=, remove()
これらの演算(add, remove)は定義しなくてもイベントを使用できますが、デフォルトの挙動をプロパティのget,setのようにオーバーライドできます。その際はsetメソッドと同じくvalue変数に設定される値が格納されています。
(プロパティ、set、getの使い方やvalueについての解説はこちら>>)
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 |
delegate void Delegate1(int a); class A { //イベントの定義 public event Delegate1 OnEvent1; //メソッドを拡張したイベントの定義 Delegate1 _onEvent2; public event Delegate1 OnEvent2 { add { _onEvent2 += value; } remove { _onEvent2 -= value; } } //イベントは定義したクラスからしか実行できない。 void RunEvent1(int a) { OnEvent1?.Invoke(a); } void RunEvent2(int a) { _onEvent2?.Invoke(a); } } var inst = new A(); //イベントにメソッドを追加・削除する Delegate1 del1 = (int a) => { a += 10 }; inst.OnEvent1 += del1; inst.OnEvent1 += (int a) => { a -= 10 }; inst.OnEvent1 -= del1; inst.OnEvent2 += del1; inst.RunEvent1(10); // <- A.OnEvent1に設定されたメソッドを実行する inst.RunEvent2(30); // <- A.OnEvent2に設定されたメソッドを実行する |
EventHandler<T>型とEventArgs型について
.Netと呼ばれるMicrosoftが提供するフレームワークにおいてイベントはEventHandler<T>型とEventArgs型を利用しています。
これらの型はFormアプリケーションなどWindows環境での標準的なイベントの使い方として利用されています。
これらの2つの型を使用することで新しくデリゲート型を定義する必要なくイベントを使用することができるので、利用できる場合は活用してみましょう!
- EventHandler<T>型:テンプレートなデリゲート型でTにはEventArgsを継承した型を指定する。
- EventArgs型:EventHandler<T>型を使用するときのイベント発生時のパラメータを表す型。イベントのパラメータはこの型を継承した型になる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
using System; class EventParam : EventArgs { public int Value { get; } public EventParam(int value) { Value = value; } } class A { public event EventHandler<EventParam> ChangedValue; public void InvokeChangedValue(EventParam param) { ChangedValue?.Invoke(this, param); } } var inst = new A(); inst.ChangedValue += (self, e) => var a = e.Value + 10; //ChangedValueイベントを実行する。引数には今回のパラメータを渡す。 inst.InvokeChangedValue(new EventParam(100)); |
Unityが提供しているデリゲート・イベント型について
Unityでは上で説明したC#のデリゲート・イベントも使用できますが、Unity専用のデリゲート・イベントも存在しています。
UnityActionについて
UnityにはUnityEngine.Events.UnityAction
というUnity専用のデリゲート型があります。
UnityActionはUnityの仕様にあったものなので、デリゲート型をUnityで使用する時はこちらを使用するといいでしょう。
またメソッドの引数に合わせたバリエーションも存在していますので、用途に合わせて使い分けてください。これらのバリエーションではテンプレートを利用してメソッドの引数を指定します。戻り値はvoid型のものしかありませんので注意してください。
- UnityAction<T>
- UnityAction<T1, T2>
- UnityAction<T1, T2, T3>
などなど。
後、UnityActionでは対応した戻り値・引数を持つメソッドをUnityActionに自動的に変換(※)してくれるようになっています。
(※これについてはC#の暗黙の型変換と呼ばれる機能を利用しています。詳しい解説は長くなるのでここでは省略します。)
UnityEventクラスについて
UnityEngine.Events.UnityEvent
クラスはUnityAction用のイベントみたいなものになります。
UnityAction
と同じくテンプレート版のものも用意されています。
スクリプト上からメソッドを追加・削除する時は次のメソッドを使用してください。
- AddListenerメソッド:指定したメソッドを追加する。
- RemoveListenerメソッド:指定したメソッドを削除する。
- RemoveAllListenersメソッド:登録されているメソッドを全て削除する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
using UnityEngine; using UnityEngine.Events; public class Sample : MonoBehaviour { public UnityEvent Event; void Start() { Event.AddListener(() => { ... }); // <- メソッドの追加 } public void FireAction() { Event?.Invoke(); // <- メソッドの実行 } } |
UnityEventクラスを使用する利点として、Inspector上からメソッドの設定ができます。そのためUnityではイベント型よりこちらを使用するのをオススメします。
Inspector上から設定する時はシーンに存在するGameObjectを指定しそれにアタッチされているコンポーネントのpublicなメンバが指定できます。(メソッドだけではなく、フィールドも設定できます。)
またUnityEventクラスの便利な点として、Inspector上では異なるメソッドの戻り値・引数を持つものでも引数が一つの数値型、および文字列のものでも設定することが可能です。その際はInspectorからそのメソッドに渡す値を入力してください。
【実践】Unityでイベントとデリゲートを使ってみよう!
それでは実際にデリゲートとイベントをUnityで使ってみましょう!
Unityには先に紹介したUnityAction
とUnityEvent
が提供されているので今回のサンプルではそちらを使用します。
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 |
using UnityEngine; using UnityEngine.Events; public class Sample : MonoBehaviour { public enum ActionType { X, Y, Z } public ActionType Type; public UnityEvent Action; public void MoveAction(float n) { //ActionTypeに応じて位置を変更する var pos = Vector3.zero; switch(Type) { case ActionType.X: pos.x = n; break; case ActionType.Y: pos.y = n; break; case ActionType.Z: pos.z = n; break; } transform.position = pos; } private void OnMouseUpAsButton() { //クリックしたらActionに設定されているメソッドを実行する。 Action?.Invoke(); } } |
サンプルコードではSampleコンポーネントを使用してイベントを設定したり、実行したりします。
Sampleコンポーネントの使い方は以下のものになります。
- このコンポーネントをアタッチされたGameObjectがクリックされた時に実行したいメソッドをActionフィールドに設定。
- 他のイベントに設定できるメソッドとしてMoveActionメソッドを用意している。
GameObjectをクリックした時の処理はUnityの機能の一つであるOnMouseUpAsButtonメソッドを利用しています。このメソッドを使用する際はColliderと呼ばれる物理エンジンの当たり判定を表すコンポーネントをアタッチしておく必要がありますので注意してください(※)。
Actionフィールドに設定できるものはUnityが提供しているコンポーネントにも色々ありますので、それらもSampleコンポーネントのActionフィールドに設定してみるのもいいでしょう!
(※物理エンジンについての解説は省略しますが、基本的に<XXX>Colliderと名付けられたコンポーネントがアタッチされていればOnMouseUpAsButtonメソッドでクリックできるようになります。)
まとめ
今回の記事ではデリゲートとイベントについて解説してきました。簡単にまとめると以下のようになります。
- デリゲートはメソッドを表す参照型。
- デリゲート型は引数と戻り値、およびデリゲート名で定義される。
- デリゲート型は同じ引数、戻り値を持つメソッドを変数として複数個格納することができる。
- デリゲート型はメソッドのように使用するか、Invoke()メソッドで格納されたメソッドを実行することができる。
- デリゲート型に何もメソッドが設定されていない時にメソッドを実行すると例外が発生する。
- なので、基本的には?演算子とInvokeメソッドを組み合わせて使うのがオススメ。
- デリゲート型のGetInvocationInfoメンバメソッドを使用することで設定されているメソッドの情報が取得できる。(ただし、リフレクションの知識がいる。)
- 複数のメソッドが設定されているときにメソッドを呼び出すと最後に実行されるメソッドの戻り値が返される。
- これと同じくref、outキーワード指定された引数がある場合も最後のメソッドのものが設定される。
- イベントはクラスのメンバの一種で、指定したデリゲート型を使いやすくしたもの。
- イベントはプロパティと似たもので、add、removeというメソッドを定義することができる。
- イベントは定義されたクラスからしか設定されたメソッドを実行することができない。
- Unityを使用するときはUnityActionおよびUnityEventを使用すると便利。
それでは次の記事に行ってみましょう!
コメント