委托和事件

UnityEngine C#

先说结论

委托和事件最容易混在一起,是因为事件本身就是基于委托做的。

可以先这样记:

  • 委托是一个类型安全的方法引用,可以保存一个或多个方法
  • 多播委托用 += 增加方法,用 -= 移除方法,调用时会按注册顺序执行
  • 事件是对委托字段的一层限制,外部只能订阅和取消订阅,不能直接赋值、清空或者主动触发

也就是说,委托解决的是“什么样的方法可以被保存和调用”,事件解决的是“谁有资格触发这个通知”。

委托

委托使用 delegate 关键字声明。

下面这个委托表示:只要是无参数、无返回值的方法,都可以被保存进来。

public delegate void GameOverHandler();

比如游戏结束时,通知所有玩家处理结算逻辑:

using UnityEngine;

public delegate void GameOverHandler();

public class GameWithDelegate
{
    public GameOverHandler OnGameOver;

    public void GameOver()
    {
        Debug.Log("game over");
        OnGameOver?.Invoke();
    }
}

public class DelegatePlayer
{
    private readonly GameWithDelegate _game;
    private readonly int _id;

    public DelegatePlayer(GameWithDelegate game, int id)
    {
        _game = game;
        _id = id;
        _game.OnGameOver += HandleGameOver;
    }

    public void Dispose()
    {
        _game.OnGameOver -= HandleGameOver;
    }

    private void HandleGameOver()
    {
        Debug.Log($"player {_id} handle game over");
    }
}

使用时可以注册多个玩家:

public class DelegateExample : MonoBehaviour
{
    private void Start()
    {
        GameWithDelegate game = new GameWithDelegate();

        DelegatePlayer player1 = new DelegatePlayer(game, 1);
        DelegatePlayer player2 = new DelegatePlayer(game, 2);

        game.GameOver();

        player1.Dispose();
        player2.Dispose();
    }
}

输出结果大概是:

game over
player 1 handle game over
player 2 handle game over

这个例子能跑,但它有一个问题:OnGameOver 是公开的委托字段,外部代码权限太大。

game.OnGameOver = null;        // 可以清空所有订阅
game.OnGameOver = SomeMethod;  // 可以覆盖之前的订阅
game.OnGameOver?.Invoke();     // 可以在外部主动触发

这就是公开委托字段不适合做“事件通知”的原因。游戏结束应该只能由 GameWithDelegate 自己触发,而不是任何拿到 game 对象的代码都能触发。

事件

事件使用 event 关键字声明,但它仍然需要一个委托类型。

把上面的例子改成事件,只需要改 Game 这一层:

using UnityEngine;

public delegate void GameOverHandler();

public class GameWithEvent
{
    public event GameOverHandler OnGameOver;

    public void GameOver()
    {
        Debug.Log("game over");
        OnGameOver?.Invoke();
    }
}

玩家订阅事件的方式不变:

public class EventPlayer
{
    private readonly GameWithEvent _game;
    private readonly int _id;

    public EventPlayer(GameWithEvent game, int id)
    {
        _game = game;
        _id = id;
        _game.OnGameOver += HandleGameOver;
    }

    public void Dispose()
    {
        _game.OnGameOver -= HandleGameOver;
    }

    private void HandleGameOver()
    {
        Debug.Log($"player {_id} handle game over");
    }
}

使用方式也差不多:

public class EventExample : MonoBehaviour
{
    private void Start()
    {
        GameWithEvent game = new GameWithEvent();

        EventPlayer player1 = new EventPlayer(game, 1);
        EventPlayer player2 = new EventPlayer(game, 2);

        game.GameOver();

        player1.Dispose();
        player2.Dispose();
    }
}

区别在于,外部代码现在只能 += 和 -=:

game.OnGameOver += SomeMethod;
game.OnGameOver -= SomeMethod;

下面这些写法都会编译失败:

game.OnGameOver = null;
game.OnGameOver = SomeMethod;
game.OnGameOver?.Invoke();

事件的意义就在这里:订阅者可以说“游戏结束时通知我”,但不能反过来控制整个通知列表,更不能替 GameWithEvent 假装游戏结束。

对比

对比项 委托字段 事件
声明方式 public GameOverHandler OnGameOver; public event GameOverHandler OnGameOver;
外部订阅 可以 += 可以 +=
外部取消订阅 可以 -= 可以 -=
外部直接赋值 可以 不可以
外部清空 可以 不可以
外部触发 可以 不可以
适合场景 回调参数、策略传入、临时方法组合 对外发布通知

Unity 里怎么用

Unity 里更常见的写法是:在 OnEnable 里订阅,在 OnDisable 里取消订阅。

using UnityEngine;

public class GameController : MonoBehaviour
{
    public event GameOverHandler OnGameOver;

    public void GameOver()
    {
        OnGameOver?.Invoke();
    }
}

public class PlayerView : MonoBehaviour
{
    [SerializeField] private GameController gameController;

    private void OnEnable()
    {
        gameController.OnGameOver += HandleGameOver;
    }

    private void OnDisable()
    {
        gameController.OnGameOver -= HandleGameOver;
    }

    private void HandleGameOver()
    {
        Debug.Log("show game over ui");
    }
}

如果只是把一个方法当参数传进去,比如排序规则、按钮回调、异步完成回调,用委托就够了。

如果是一个对象向外发布通知,比如游戏结束、血量变化、网络断开,优先用事件。这样外部只能监听,不能越权修改发布者内部的通知列表。

评论