記事ディレクトリ
任せる
デリゲートは、.NET で遅延バインディング メカニズムを提供します。遅延バインディングとは、呼び出し元が、アルゴリズムの一部を実装するために作成したアルゴリズムに少なくとも 1 つのメソッドを提供することを意味します。
より簡単に言うと、デリゲートを呼び出すことで多数のメソッドを処理できます。デリゲートは関数のコンテナのようなものです。前の記事では、デリゲートを、料理を注文するときにウェイターが書く小さな注文に例えました。デリゲートがトリガーされると、注文がシェフに与えられ、シェフがそれを調理します。注文書を出してください。
委任の定義
デリゲートを定義するときは、delegate
キーワードを使用する必要があります。
delegate void MyFun();// 委托不可重载
delegate void MyFun<T>();// 带泛型的函数名MyFun<T>和MyFun不同
delegate int MyFun2(int i);
デリゲートはオーバーロード可能ではありません。関数の戻り値の定義を変更したり、パラメーターを追加したりしても、同じ関数名のデリゲートが存在する限り、コンパイルは通過しません。(ただし、ジェネリックを含む関数名は同じではありません。たとえば、MyFun、MyFun、MyFun<T,K> は同時に存在できます)
デリゲートのインスタンス化
デリゲートを使用したい場合は、まずインスタンス化する必要があります。インスタンス化には 2 つの方法があります。オブジェクトを新規作成し、最初の関数をそれに割り当てます。
int Input(int i)
{
Debug.Log(i);
return i;
}
void Start()
{
MyFun2 myFun = new MyFun2(Input);// new的时候需要定义第一个调用函数
MyFun myFun1 = null;// 委托初始化可为空,但是空委托触发时会报错
}
まず、デリゲートの初期化はメソッド内で実行する必要があり、定義中に初期化することはできません。次に、デリゲートの形式と関数の形式がまったく同じである必要があることに注意してください。デリゲートの戻り値にどのような型が指定されていても、デリゲートに割り当てられる関数も同じ型である必要があり、デリゲートが持つ入力パラメーターと同じ数の関数にも同じ入力パラメーターが必要です。
delegate int MyFun2(int i);
int Input(int i)
{
return i;
}
void Input1(int i)
{
}
int Input2(string i)
{
return 1;
}
MyFun2 myFun = new MyFun2(Input);
myFun += Input1; // 返回值不同,报错
myFun = new MyFun2(Input2); // 入参不同,报错
代理通話
デリゲートを呼び出すときは、デリゲートが空でないことを確認する必要があります。リフレクション メソッドと直接呼び出しメソッドを使用してデリゲートを呼び出すことができます。
myFun1();//委托为空执行会报错
myFun(100);
myFun.Invoke(100); //带有入参的委托需要在调用时给出入参
myFun1.Invoke(); //委托为空执行会报错
myFun1?.Invoke(); //使用?.Invoke(),当委托为空时不调用
マルチキャスト委任
デリゲートには複数のメソッドを含めてトリガーできます。このようなデリゲートはマルチキャスト デリゲートと呼ばれます。デリゲート内のメソッドについては、次のステートメントを使用するだけで簡単に増減できます。
myFun1 += Fun;
myFun1 -= Fun2;// 当减去方法的时候,若委托中没有对应函数,
//编译(即使委托为空)和执行都不会报错
myFun1 += SayHi;
マルチキャスト デリゲートでは、デリゲート内のイベントのトリガー順序は、ステートメントの実行時にデリゲートに追加したメソッドの順序で実行されます。メソッド A が最初に追加され、次にメソッド B が追加された場合、A が最初に実行され、メソッド B が追加されます。それからB。
なぜ委任を使用するのでしょうか?
以下の例を参照してください。
class Test
{
public MyFun fun;
public MyFun2 fun2;
int i=10;
public void TestFun(MyFun fun,MyFun2 fun2)
{
i= i*100;
fun2.Invoke(i);
fun.Invoke();
}
}
上記のクラスでは、関数を使用して 2 つのデリゲートを受け取り、関数内のパラメーターを処理してi
、2 つのデリゲートの呼び出し順序を定義します。最終的に、このクラスはインスタンス化後に必要に応じてこの関数メソッドをディスパッチできます。
Monster
クラスがインスタンス化された後に一連のメソッドを処理する必要がある場合、たとえばクラスがインスタンス化された場合小怪A
、i
そのダメージに対応して、fun1 と fun2 は攻撃後にトリガーされる一連の反応に対応します。このようにして、簡単にモブを攻撃する方法が完成する。
関数を呼び出したい場合、第一に、その関数をパラメータとして他の関数に渡すことができないこと、第二に、スケジューリング関数が多くの問題を考慮する必要があるかどうか、たとえば、関数のアクセス変更が可能かどうかなどです。スケジュールする必要がある関数がパブリックであるか、最初に他のクラスを導入する必要があるかどうか、この攻撃方法を変更したい場合はどうするかなど。デリゲートを使用する場合、これらの問題を考慮する必要はありません。デリゲートの形式と呼び出しメソッドが一貫していることを確認することだけが必要です。デリゲートをメソッドにスローして、それを直接呼び出します。問題全体は次のように抽象化されます。デリゲートをトリガーするタイミングのみを考慮してください。
一方、私たちの構造では、関数の代わりにデリゲートがトリガーされます。これは、デリゲートにメソッドを追加または削除するだけで、攻撃メソッドによってトリガーされる関数を自由に変更できることを意味します。
ここで紹介するデリゲートの使用例は、公式の Unity システムからのもので、公式に提供されている New InputSystem では、キーストロークごとにデリゲートがトリガーされます。これにより、キーに対応するメソッドをインターフェイスと同じくらい簡単に変更できます。以前はif
キーストロークを検出してからメソッドをスケジュールしていましたが、現在は委任を通じて委任されたマルチキャストにスケジューリング メソッドを直接追加できるようになりました。これにより、変更が容易になり、より柔軟になります。
公式委員会
自分でデリゲートを定義することもできますが、結局のところ、コードは人々が読むためのものです。誰かがあなたのコードを受け入れても、対応する型がクラスなのか、構造体なのか、それとも他のものなのかはわからないかもしれません。その人が参照を見たときにのみ、彼はそれが代表者であることを知っています。当局者は思慮深くいくつかの指名委員会を提供しました。
Action action = test.Fun; // void Action() 无参无返回委托
Action<string> action1 = NewString; // void Action<T>() 有参无返回泛型委托,最多接受16个泛型传入
action1 += Tstring; // 如果函数同样接收泛型,那么函数的泛型会自动接受Action委托声明时给出的对应泛型
Func<int> func = Input;// T Func<out T>(); 无参带返回值泛型委托,使用out修饰代表该委托是协变的,最后一个泛型决定返回值类型
Func<int,string,int> func1 = Input;// Result Func<T1,T2...Result>(T1 arg1, T2 arg2...) 有参带返回值泛型委托,最多接受16个泛型传入
公式の委任には、合計 2 つあります。Action
戻り値のない委任Func
と、戻り値のある委任です。これら 2 つのデリゲートには同じ名前のジェネリック定義があり、それぞれが入力パラメーターに対応する最大 16 個のジェネリックを受け入れることができます。
通常のデリゲートと同じように使用します。たとえば、次のようになります。
void Input(){
}
int Input()
{
return 1;
}
int Input<T1,T2>(T1 a,T2 b)
{
return 1;
}
void Tstring<T>(T i){
}
void NewString(string i){
}
Action action = Input; // Action无入参无返回值,对应delegate void Action()
Action<string> action1 = NewString;// Action有入参无返回值,对应void Action<T>(T t)
// 上句对应的NewString类型也要完全一致,也是无返回值,入参一个,类型为string
Func func = Input;// Func无入参有返回值,对应TResult Func<out TResult>(),out修饰协变
// 注意当使用Func委托的时候,至少需要定义一个泛型,这个泛型对应的不是入参而是返回值的类型
Func<int,string,int> func1 = Input;// 同理,右侧最后一个泛型int代表了返回值的类型
// Func定义的三个泛型,则需要委托的方法要有返回值,且有两个入参
上記は公式デリゲートの使用方法です 戻り値が不要な場合に使用しますAciton
戻り値が必要な場合に使用しますFunc
最後のジェネリック型も戻り値の型を表すように定義する必要があります
汎用メソッドと汎用デリゲート
int Input<T1,T2>(T1 a,T2 b)
{
return 1;
}
Func<int,string,int> func1 = Input;
上記のコードでは、関数はInput
2 つのジェネリックを定義しており、Func
デリゲートには 3 つのジェネリックがあります。実際、それらは一致しています。結局のところ、Func
最後のジェネリックが戻り値の型を表しています。
現在、次の定義があります。
T Input<T1,T2,T>(T1 a,T2 b)
{
T t = default(T);
return t;
}
Func<int,string,int> func1 = Input; // 报错
上記のコードは非常に合理的に見えますが、3 つのジェネリック型が入力の 3 つのジェネリック型に順番に割り当てられていますが、実際には機能しません。Func
最初の 2 つのジェネリックのみが関数に定義されるため、3 番目のジェネリックはコンパイラによって推論できません。
汎用デリゲートもタプルを受け入れます。デフォルトでは、呼び出されたときのタプルの最初の項目は.Item1
、2 番目の項目は.Item2
、というようになります。
Func<(int, int, int), (int, int, int)> doubleThem = ns => (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3);
var numbers = (2, 3, 4);
var doubledNumbers = doubleThem(numbers);
もちろん、次のようにタプル内の各項目の名前をカスタマイズすることもできます。
Func<(int n1, int n2, int n3), (int, int, int)> doubleThem = ns => (2 * ns.n1, 2 * ns.n2, 2 * ns.n3);
イベント
委任を学習したら、イベントも学習します。イベントと委任は基本的に同じです。
class Test
{
delegate void NewDel();
event NewDel MyFun; // 注意,定义事件时其访问性必须与委托一致
}
イベントを定義するときは、event
キーワードを使用してデリゲート名を変更し、イベント デリゲートの定義に名前を付けます。イベントとデリゲートの使用方法は基本的に同じなので、詳細は説明しません。唯一の違いは、クラスの外でイベントを割り当てたり呼び出すことができないことです。
class Test
{
public delegate void NewDel();
public NewDel del = null;
public event NewDel MyFun;
public Test()
{
del = NewFun;
MyFun = NewFun;
}
public void NewFun()
{
}
}
void Start()
{
Test t = new Test();
t.del();
t.del.Invoke();
t.MyFun(); // 报错
t.MyFun.Invoke(); // 报错
t.MyFun = null; // 报错
t.MyFun += Appli;
t.MyFun -= Appli;
}
void Appli()
{
}
クラスの外では直接操作はできずEvent
、加算と減算の関数のみ実行できます。また、イベントは関数の一時変数として使用することはできず、インテリジェンスはクラス、インターフェイス、および構造体のメンバーとして存在します。
なぜイベントがあるのか?
- 外部からの空のコミッションが任意に配置されるのを防ぐ
- 外部からのデリゲートへの任意の呼び出しを防止する
- イベントはデリゲートをカプセル化するのと同等であり、より安全になります
イベントとデリゲートの違い:
- イベントに外部から値を割り当てることはできず、関数の委任を通じて外部からのみ追加または減算できます。
- イベントは外部では実行できませんが、委任はどこでも実行できます
- イベントを関数の一時変数として使用することはできませんが、デリゲートは一時変数として使用できます。
余談 - デリゲートとオブザーバーのパターン
まず、オブザーバー パターンとは何かについて簡単に紹介します。オブザーバー パターンは、オブジェクト間の 1 対多の依存関係です。オブジェクトの状態が変化すると、そのオブジェクトに依存するすべてのオブジェクトが通知され、自動的に更新されます。
例えば、外国貿易会社が n 社あり、これらの会社はいずれも輸出入事業を行っていますが、人民元安になると、人民元安によりこれらの企業は輸出事業に傾き、逆に人民元高により輸出事業に傾きます。輸入ビジネスにもっと傾いている:
class Company
{
public void Update(bool 贬值了)
{
if (贬值了) {
Debug.Log("出口"); }
else {
Debug.Log("进口"); }
}
}
class CHY
{
private List<Company> Companies = new List<Company>();
bool state = false;
void 贬值()// 原谅我不懂贬值的英文,幸好C#可以起中文名,作为举例足够了
{
state = true;
Debug.Log("人民币贬值");
}
void 升值()
{
state = false;
Debug.Log("人民币升值");
}
bool setState()
{
// 判断升值还是贬值的代码
return state;
}
void getState()
{
foreach(var company in Companies)
{
company.Update(state);
}
}
}
上記のコードでは、人民元が上昇または下落した場合、getState()
オブザーバー (企業) に通知が送信され、オブザーバーは人民元の為替レートの変化を発見すると、対応する戦略計画を選択します。これは単純なオブザーバー パターンです。
オブザーバー パターンでは、これらの関連クラスに強い依存関係があることが明らかであり、監視対象のオブジェクトが変更されると、通知がすべてのオブザーバーにブロードキャストされ、オブザーバーはそれに応じて応答します。
では、委任とオブザーバー パターンの関係は何でしょうか? よく考えてみると、デリゲートとオブザーバーは本質的に似ており、処理モードは
1 つずつ開始、通知、処理を行います。
次に、上記のオブザーバー イベントをデリゲートに追加してみましょう。
Func<bool> ChangeState = setState;// 代码有点小问题,意思到了就行
ChangeState += Company1.Update;
ChangeState += Company2.Update;
......
ChangeState.Invoke(state);
ChangeState
イベントをトリガーすると、人民元のステータスが変更され、コミッションに所属する企業にも通知されました。実装されている機能はオブザーバーパターンと同じです。では、なぜそれを実装するために委任を使用するのでしょうか? なぜならデカップリング。
最初の例では、観察者と観察されるものの関係が非常に密接であるため、依存関係があり、観察されるものは観察者に通知する必要があります。カップリングが高すぎます。デリゲートを使用した後は、オブザーバーでメソッドを定義する必要はなくgetState()
、List<Company>
オブザーバーのステータス更新メソッドをデリゲートに追加するだけで済みます。2 つのクラス間の結合は大幅に減少します。委任を使用すると、観察される側は観察者の存在にまったく気づきません。これが真の観察者のパターンです。