C#进阶:协程与事件

我们知道Unity中的协程提供了类似"多段代码并行执行"的功能,在我们定义一个协程函数的时候,需要遵守类似这样的语法:

复制代码
IEnumerator MyCoroutine(){
    //......
    yeild return null;
}

不难发现,其中的IEnumerator和yield return部分看起来很是奇怪,和一般的函数不太一样。如果返回值不为 IEnumerator 或者缺少yeild return null 语句都不会称为一个协程;

为了解答这个问题,我们需要了解这个IEnumerator的真面目。

枚举器接口**IEnumerator 接口**

这就是该接口的全部成员

复制代码
public interface IEnumerator
{
    bool MoveNext();          // 尝试移动到下一个元素,返回是否成功
    object Current { get; }   // 获取当前位置的元素
    void Reset();             // 重置到初始位置(-1),很少使用
}

IEnumerator迭代协议 ,协程是它的一个创造性应用------Unity 用它来实现"等待-继续"机制,而不是真正的数据遍历。

核心作用:让对象支持 foreach 遍历

任何实现 IEnumerator 的类都可以用 foreach 循环**(协程的本质是枚举器)**:

枚举器的概念:枚举器 通常指用于遍历集合(如数组、列表、字典等)中元素的对象。它提供了一种标准方式来逐个访问容器中的项

复制代码
// 编译器会将 foreach 翻译成 while 循环调用 MoveNext()
foreach(var item in collection) { ... }

// 等价于:
var enumerator = collection.GetEnumerator();
while(enumerator.MoveNext()) {
    var item = enumerator.Current;
    // ...
}

该接口与 Unity 协程的关系

Unity 的协程就是基于 IEnumerator 实现的:

复制代码
// 你的协程代码
IEnumerator MyCoroutine() {
    Debug.Log("开始");
    yield return new WaitForSeconds(1); // 返回一个 IEnumerator 对象
    Debug.Log("1秒后");
}

// Unity 引擎在背后做的事:
var enumerator = MyCoroutine();
while(enumerator.MoveNext()) {
    // 检查 Current 返回的 yield 对象
    // 如果是 WaitForSeconds,就计时,时间到了再调 MoveNext()
}

yield return 的本质

  • 每次 yield return X 时,MoveNext() 返回 trueCurrent 返回 X

  • Unity 根据 Current 的类型决定等待条件(WaitForSeconds, WaitForEndOfFrame 等)

  • 条件满足后,再次调用 MoveNext() 继续执行

协程的原理

Unity中的协程,利用了迭代器的机制,实现了"执行一系列操作"而非"遍历一个集合"的功能(相当于把该 IEnumerator 当作一个状态机来逐步执行。)

复制代码
IEnumerator MyCoroutine()
{
    //真正有用的语句
    yield return null;
    //真正有用的语句
    yield return new WaitForSeconds(5);
}
  1. 每一帧调用MoveNext(),这样可以执行协程直到遇到yield return

  2. 通过yield return返回的Current值,我们做一系列特定的操作 比如WaitForSeconds就是延迟下一次MoveNext()的执行。

事件

C#中的事件系统中有两个常用的组件:ActionFunc

可以把它们想象成存储了许多函数的List。当一个Action/Func执行时,其中所有的函数都会一起执行。(这是不是和之前玩家受击的需求很像?)

Action/Func是一个类型,我们可以定义类型为Action/Func的变量并用它来存放函数。

比如:

复制代码
public Action onPlayerInjure;

void Start(){
    onPlayerInjure += ScreenFlash;
}

上方的代码所做的事是:定义了 onPlayerInjure 这个 Action 类型变量,并在游戏开始运行时将 ScreenFlash 这个函数放进 onPlayerInjure 里。

说明:

  • Action 是一个无返回值的委托类型,可以用来存储无参数或带参数的方法。
  • Func 是一个有返回值的委托类型,通常用于需要返回值的场景。

Action:常用操作

  • Action 位于 namespace System 中(这意味着什么?)
  • 它能够存放的函数必须是无返回值的!(也就是 void 函数)
  • 有关的常用操作有这些:
    1. 向一个 Action 中添加一个函数:+=
      • 比如:onDamage += ScreenFlash;
      • 注意,+= 右侧的值为想要添加的函数名
    2. 从一个 Action 中删除一个函数:-=
      • 比如:onDamage -= UpdateHPBar;
    3. 调用一个 Action 中所有的函数:把它当作一个函数来用即可
      • 比如:onDamage();

代码使用案例:

cs 复制代码
public class L07_Events : MonoBehaviour
{
    public Action onPressF;

    void Start()
    {
        onPressF += PrintHello;
        onPressF += PrintNiHao;
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.F))
        {
            onPressF();
        }
    }

    void PrintHello()
    {
        print("Hello");
    }

    void PrintNiHao()
    {
        print("Ni Hao");
    }
}

和invoke的区别

直接调用 onPressF() 和显式调用 onPressF?.Invoke() 有什么区别?

功能上完全等价 ------onPressF();onPressF.Invoke(); 的语法糖(简写形式)。

安全性和空值处理 上有细微差别,推荐使用 onPressF?.Invoke()

1. onPressF();

  • 这是 C# 提供的便捷语法。

  • 编译器会自动将其转换为 onPressF.Invoke();

  • 但如果 onPressFnull,会抛出 NullReferenceException

    if (Input.GetKeyDown(KeyCode.F))
    {
    onPressF(); // 如果没人订阅(即 null),程序崩溃!
    }

在你的代码中,Start() 里已经添加了两个方法,所以运行时不会为 null。
但如果之后用 -= 全部移除了,或者某些路径没执行 Start(),就可能出错。


2. onPressF?.Invoke();

  • 使用 空条件运算符 ?. ,先检查是否为 null

  • 如果 onPressF == null,则 什么也不做,不调用,也不报错

    if (Input.GetKeyDown(KeyCode.F))
    {
    onPressF?.Invoke(); // 安全!即使没人订阅也不会崩溃
    }

Action:有参数的函数

  • Action 能够存放的函数必须是无返回值的 ,但是可以有参数!
  • 每个 Action 类型的变量中的函数的参数必须都是相同的。
    • 比如:
      • void A(int a)
      • void B(float b)
      • void C(float c)
    • 则 B 和 C 可以放在同一个 Action 里,而 A 不行(因为参数类型不同)。

正确使用有参函数Action:

  • 定义一个有参数的 Action:Action<参数1类型, 参数2类型, ......>
    • 比如:private Action<float> onDamage;
    • 这代表 onDamage 中的函数都是接受一个 float 类型的参数,且无返回值的函数。
  • 这时,使用它的时候就可以传入一个参数了:
    • onDamage(45.2f);

Func:有返回值的函数

  • Func 和 Action 是同一"家族"的,而它们唯一的区别就是有无返回值。
  • 在定义 Func 的时候,需要在所有参数之后加上返回值的类型:
cs 复制代码
private Func<int, float> func1;  // func1 中的函数接受 1 个 int 类型的变量,并返回 float
private Func<string> func2;      // func2 中的函数不接受变量,返回 string
private Func<int, int, int> func3;// func3 中的函数接受 2 个 int 类型的变量,并返回 int
private Func func4;              // func4 会报错,为什么?

但是一般Action用的多,而不怎么用Func类型

为什么需要这两个函数

  1. "一个存放函数的变量" 。我们可以在 Action/Func 中存放函数并动态修改其中的函数。这在许多时候都会带来很大的便利。
    1. 比如:一个四则运算程序中有加减乘除四个功能。使用 Action/Func 的话,我们只需调用一个 Action/Func 并动态修改其中真正的函数是加减乘除中的哪一个
  2. 促进代码的模块化。我们可以在多个其他位置访问一个事件并向其中添加函数。相反地,如果我们把事件写成一个(调用很多其他位置函数的)函数的话会很乱。
  3. "只在需要时做一件事"。使用 Action/Func 后,我们能确保与其相关的函数只在其出发时执行一次,这样避免了多余的浪费
相关推荐
jackletter1 小时前
DBUtil设计:c#中的DateTime和DateTimeOffset转sql时应该输出时区信息吗?
android·sql·c#
feifeigo1231 小时前
斜激波参数计算MATLAB程序
开发语言·matlab
小小前端--可笑可笑1 小时前
【Three.js + MediaPipe】视频粒子特效:实时运动检测与人物分割技术详解
开发语言·前端·javascript·音视频·粒子特效
奔跑的web.1 小时前
JavaScript 对象属性遍历Object.entries Object.keys:6 种常用方法详解与对比
开发语言·前端·javascript·vue.js
hoiii1872 小时前
使用C#实现文本转语音(TTS)及多音频合并
c#·音视频·语音识别
jiayong232 小时前
Word核心功能完全指南
c#·word·xhtml
古城小栈2 小时前
Rust 模式匹配 大合集
开发语言·后端·rust
e***98572 小时前
C++跨平台开发的5大核心挑战与突破
开发语言·c++
企业对冲系统官2 小时前
价格风险管理平台审批角色配置与权限矩阵设计
大数据·运维·开发语言·前端·网络·数据库·矩阵