首页 > 解决方案 > 根据规范监听事件并调用回调?

问题描述

我目前正在构建一个自定义任务管理器,我想知道是否可以告诉任务管理器侦听特定事件(OnSomething如下),然后在任务引发该事件时调用回调方法。但是,从精神上讲,我看不到如何侦听基类级别不存在的事件。例如,我有一个基类,其中包含有关任务的基本信息,称为CustomTask

public abstract class CustomTask {
    public bool IsRunning { get; private set; } = false;
    public void Start() {
        IsRunning = true;
        DoSomething();
        IsRunning = false;
    }
    protected abstract void DoSomething();
}

为了 SO 读者,我已经简化了定义,但你明白了它的要点。它包含基本的细节,一些启动和取消的方法,提供基本的状态管理(IsRunning这里简化)等。

然后我有从 派生的自定义任务CustomTask,在这种情况下,让我们关注一个名为 的示例任务CustomTaskA。它包含一个名为 的事件的定义OnSomething,某个地方的某个人可能想要监听该事件:

public sealed class CustomTaskA : CustomTask {
    protected override void DoSomething() => RaiseOnSomething(this, new EventArgs());
    public event EventHandler<EventArgs> OnSomething;
    private void RaiseOnSomething(object sender, EventArgs e) => OnSomething?.Invoke(sender, e);
}

现在,CustomTaskManager注册任务,通过 跟踪它们Guid,管理它们等等,但为了简单起见:

public sealed class CustomTaskManager {
    // Singleton setup.
    private static CustomTaskManager _instance = new CustomTaskManager();
    public static CustomTaskManager Instance {
        get {
            // Simplified for SO.
            if (_instance == null)
                _instance = new CustomTaskManager();

            return;
        }
    }

    // Collection of tasks.
    private Dictionary<Guid, CustomTask> _tasks = new Dictionary<Guid, CustomTask>();

    // Register and start a task.
    public bool TryRegisterAndStartTask(CustomTask task, out Guid taskId) {
        taskId = Guid.Empty;
        try {
            // Register task.
            taskId = Guid.NewGuid();
            _tasks.Add(taskId, task);

            // Listen for events.

            // Start task.
            task.Start();
        } catch (Exception e) {
            // Log exception.
        }

        return false;
    }
}

在注册启动任务时,我想告诉任务管理器我要监听OnSomething,如果OnSomething被调用,我希望任务管理器调用一个方法OnSomethingWasRaised。例如:

TaskManager.Instance.TryRegisterAndStartTask(task, out Guid taskId, task.OnSomething, OnSomethingWasRaised);

private static void OnSomethingWasRaised(object sender, EventArgs e) {
    Console.WriteLine("Woohoo!");
}

我知道指定和调用回调方法是完全可能的,并且通过反射来监听事件是合理的。


有没有办法(使用或不使用反射)监听派生对象上定义的指定事件,然后调用指定的回调方法?

注意:请原谅任何语法错误,因为我手动输入了片段以使其最小化。

标签: c#eventscallback

解决方案


像这样的(建议的)方法的问题:

TryRegisterAndStartTask(task, out Guid taskId, task.OnSomething, OnSomethingWasRaised);

是你不能将事件作为参数传递,或者将它存储在变量中,因为事件只是一组两个方法(添加和删除),就像属性是一组两个方法 get 和 set。

您当然可以将事件更改为“原始”委托:

public EventHandler<EventArgs> OnSomething;

这个你可以通过参考传递:

public bool TryRegisterAndStartTask(CustomTask task, ref EventHandler<EventArgs> del, EventHandler<EventArgs> sub, out Guid taskId) {
        taskId = Guid.Empty;
        // subscribe
        del += sub;
        ...
} 

CustomTaskManager.Instance.TryRegisterAndStartTask(task, ref task.OnSomething, OnSomethingWasRaised, out var taskId);

但这通常不是一个好主意,因为您正在失去事件的私有范围 - 对于事件,只能添加\删除委托,对于原始委托,任何人都可以做任何事情,比如调用或设置为 null。

如果常规事件仍然存在 - 这意味着反射是实现目标的唯一方法,甚至更糟 - 您必须通过字符串名称而不是实际引用来引用要订阅的事件,尽管您可以使用nameof(task.OnSomething). 然后,您将丢失订阅委托类型的编译时验证。假设您想订阅event Action Something但在那里传递Func<string>委托。它将使用反射方法编译得很好,并且仅在运行时失败。

尽管如此,如果你坚持会看起来像这样:

public bool TryRegisterAndStartTask(CustomTask task, string eventName, Delegate sub, out Guid taskId) {
    taskId = Guid.Empty;
    // subscribe
    var ev = task.GetType().GetEvent(eventName, BindingFlags.Public | BindingFlags.Instance);
    var addMethod = ev.GetAddMethod(); // this can be null or private by the way
    addMethod.Invoke(task, new [] {sub});
    ...
}

并像这样调用:

var task = new CustomTaskA();
EventHandler<EventArgs> handler = OnSomethingWasRaised;
CustomTaskManager.Instance.TryRegisterAndStartTask(task, nameof(task.OnSomething), handler, out var taskId);

在我看来,在你的场景中丑陋、不安全、不值得。


推荐阅读