Home Csharp委托和事件
Post
Cancel

Csharp委托和事件

高阶函数

  1. 接收委托类型(函数指针)参数的函数称为高阶函数, 高阶函数不仅可以接收函数参数, 也可以返回新的函数.

Delegate

  1. 委托即是在没有函数指针的C#托管代码中实现回调函数的功能。所有函数指针功能都以委托的方式来完成。委托可以视为一个更高级的函数指针。
  2. 委托是一种特殊的类,本质上是一种函数类型,可以定义在类的外面。
  3. 如果委托的声明嵌入到类中,就称为了嵌套类型。如果仅在某个包容类中使用这个委托,就要考虑嵌套委托
  4. 委托定义了在代码中处理事件的模式。这几乎完全消除了写轮询例程的必要。
  5. 委托不是一种类似于 float, List 等预定义好的类型,委托是我们定义的类类型,使用 delegate 关键词定义一个委托相当于定义一个继承自 System.MulticastDelegate
  6. 要使用定义好的委托需要继续定义这个委托类型的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
// 1. 声明委托类型
public delegate void Processor(string param);

// 2. 定义一个方法给委托使用
void PrintString(string x) {}

// 3. 创建委托实例
Processor proc = PrintString;

// 4. 使用委托实例里的三个函数调用委托
proc.Invoke("hello world");	//直接调用
proc.BeginInvoke("hello world");    // 异步开始调用
proc.EndInvoke("hello world");      // 结束异步调用

预定义委托

  1. 几乎不需要自己声明委托,使用合适的预定义委托,除非能够大大提高代码的可读性
Predicate
  1. 谓词指:获取一个实参并返回 bool 值的委托表达式。
  2. 通常用谓词筛选或识别集合中的数据项。即向谓词传递一个数据项,它返回true或false指出该项是否符合条件。
  3. 接受一个T类型的对象p, 返回针对这个对象的某个条件是否成立(true/false)
1
2
public delegate bool Predicate<in T>(T obj) {}
Predicate<T, bool> predicate;   // 谓词,返回bool
Action
  1. 行动只执行一些逻辑,不返回值。
1
2
public delegate void Action<in T1, in T2, ...>(T1 args, T2 args, ...)
Action<int, string...> action;	// 事件,返回void
Func
  1. 方法的最后一个参数为返回值。
1
2
public delegate TResult Func<in T, out TResult>(T arg)
Func<int,bool> func;  // 函数,最后一个参数类型为返回类型

委托继承链

image-20230205195341105

  1. 所有委托总是派生自 System.MulticastDelegate 类,MulticastDelegate 又派生自 System.Delegate
  2. Delegate 是一个抽象基类,它引用静态方法或引用类实例及该类的实例方法。只有系统和编译器可以显式地从 Delegate 类派生出委托类型。
  3. MulticastDelegate:是一个继承于Delegate的类,其拥有一个带有链表格式的委托列表,该列表称为调用列表,在调用多路广播委托时,将按照调用列表中的委托出现的顺序来同步调用这些委托。平常我们声明一个delegate的类型,都是继承于MulticastDelegate类的(注意:不能显式地从此类进行派生。这点与Delegate类是一样的,只有系统和编译器也可以显示地进行派生)。

多播委托

  1. 多播委托不止可以引用一个方法,它可以引用一系列的方法,这些方法将按照注册顺序依次调用。(不是同时,它们全部在一个执行线程上依次调用)
  2. 注册的方法依次执行,但执行顺序是不能保证的,不一定按照先来后到的顺序调用。编程时不能依赖特定的调用顺序。
  3. 通过多播委托,单个事件的通知可以发布给多个订阅者。

方法组

  1. 方法组是一组方法的名称(可能只是一个)。
  2. 把方法名作为实参传递给委托形参时,从方法组向委托类型的转换会自动创建新的委托对象;所以,传参时不需要再单独定义委托对象。
1
2
3
4
5
6
7
8
9
10
11
12
// 声明委托类型
public delegate bool Comparer(int first, int second);
// 声明高阶函数
public void BubbleSort(int[] items, Comparer compare)
{}
// 比较方法(谓词)
public bool GreaterThan(int first, int second)
{}
// 传统调用方法,手动创建委托对象
BubbleSort(items, new Comparer(GreaterThan));
// 使用方法组调用,自动创建委托对象
BubbleSort(items, GreaterThan);

本地函数

  1. 可以创建嵌套方法,并将方法名用作委托。

Lambda

  1. Lambda 表达式名字的来源是 函数的λ演算系统。
  2. Lambda 表达式是在委托的基础上构建起来的,它提供了比委托更优雅和简洁的语法。
  3. Lambda 表达式的形参列表中的类型可以省略,利用了编译器的类型推断。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 无参数 语句lambda
getUserInput = () => {
    return "xxx";
}
// 单个参数 语句lambda, 参数圆括号可以省略
processes.GetProcesses().Where(
    process => { return process.WorkingSet > 100 }
);
// 多参数 语句lambda
BubbleSort(items, 
   (first, second) => {
        return first < second;
    }
);
// 表达式lambda,只包含要返回的表达式,没有语句块
BubbleSort(items, (first, second) => first < second);

外部变量

  1. 在Lambda表达式外部声明的局部变量(包括包容方法的参数)称为该Lambda的外部变量。如果Lambda表达式主体使用一个外部变量,那么就说该变量被该Lambda表达式捕获。
  2. 编译器把被捕捉的局部变量作为生成的lambda表达式辅助类(闭包)的实例字段实现,从而延长了其生存期。所有使用局部变量的地方都改为使用那个字段。从而被捕获的外部变量的生存期超过了它的作用域,至少延长到了和委托对象一样长。
  3. 被Lambda捕捉到的是变量,而不是创建委托实例时该变量的值。
  4. 在Lambda外部对变量的更改在Lambda内部是可见的,反之亦然。

闭包 Closure

  1. 闭包:闭包是为Lambda生成的一个 C#类,其中包含一个表达式及对该表达式进行求值所需要的变量。

循环变量

  1. foreach认为每一次循环变量都应该是“新的变量”,所以每次创建委托,捕捉到的都是不同的变量,不共享同一个变量。
  2. 但for循环在语句头中声明的任何循环变量在被捕捉时,都被看成同一个外部变量。
  3. 以下两种写法等价:
1
2
3
4
5
6
7
8
9
10
11
var items = new string[] { "Moe", "Larry", "Curly"};
var actions = new List<Action>();
foreach(string item in items)
{
    actions.Add( () => Debug.Log(item); );
}
for(int i = 0; i < items.Count; i++) 
{
    int index = i;
    actions.Add( () => Debug.Log(items[index]));
}

表达式树

不太熟,用到的时候再看资料。

  1. 表达式树是对象,允许传递编译器对Lambda表达式的分析。
  2. Where()方法获得的表达式树可转换成SQL查询并传给数据库。

image-20230205195402251


Event

观察者模式

[[观察者模式]]

事件

  1. C# delegate 关键词用于声明一个委托类型(函数类型)。
  2. C# event 关键词可以把一个委托实例封装为事件
  3. 事件是对委托实例的一种特殊封装 ,实现了发布者,订阅者模式。通过[[多播委托]],单个事件的通知可以发布给多个订阅者。

事件对委托的封装

对订阅的封装
  1. 不允许使用 = 赋值操作符,以避免不小心清空整个委托链,取消了其他订阅者。
  2. 外部类只能使用 += 或 -= 向发布者添加或删除对事件的订阅。
对发布的封装
  1. 事件相比于委托,能够确保只有包容类才能发布事件通知。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MouseManager : MonoBehaviour {
	public event Action<string> OnMouseClicked;
    if (Physics.Raycast(ray, out hitInfo))
    {
        // 使用空条件操作符调用委托
        OnMouseClicked?.Invoke(hitInfo.point);
    }
}
    
MouseManager.Instance.OnMouseClicked += MoveToTarget;

public void MoveToTarget(Vector3 target)
{
    agent.destination = target;
}

UnityEvent

UnityEvent

  1. Unity事件是 [[Delegate]] 的简单封装。
  2. 事件支持多次添加同一个订阅函数,添加几次就会调用几次。这种设计虽然灵活,但有时也会造成不小心订阅了很多次的情况。
  3. 使用 Serializable 可以在Editor中 AddListener。
  4. 开发时应当尽量使用UnityEvent,因为这样不仅符合惯例,而且可以获得更多便利。

无参数事件

1
2
3
4
5
6
7
using UnityEngine.Events;
public UnityEvent JumpEvent;

JumpEvent.AddListener(FuncName);
JumpEvent.RemoveListener(FuncName);

JumpEvent?.Invoke();

自定义参数事件

  1. 泛型版本的 UnityEvent 都是抽象类型, 要使用的话, 需要自己声明一个类继承之, 然后再实例化该类才可以使用.
1
2
3
4
5
6
7
8
9
10
11
12
// 
// 泛型参数类型就是Invoke函数时传入的参数
[Serializable]
public class EventVector3 : UnityEvent<Vector3> {}
// 定义变量
public EventVector3 OnMouseClicked;		
// 注册回调函数
OnMouseClicked.AddListener(FuncName);
// 移除回调函数
OnMouseClicked.RemoveListener(FuncName);
// 触发回调函数
OnMouseClicked?.Invoke(Vector3.zero);

UnityAction

  1. UnityAction仅仅是预定义好的委托类型。
  2. 类似:[[Action]]
1
2
3
4
5
6
7
8
9
namespace UnityEngine.Events
{
    public delegate void UnityAction();
    public delegate void UnityAction<T0>(T0 arg0);
    public delegate void UnityAction<T0, T1>(T0 arg0, T1 arg1);
    public delegate void UnityAction<T0, T1, T2>(T0 arg0, T1 arg1, T2 arg2);
    public delegate void UnityAction<T0, T1, T2, T3>(T0 arg0, T1 arg1, T2 arg2, T3 
arg3);
}
This post is licensed under CC BY 4.0 by the author.
Contents