【问题标题】:Constructor that takes any delegate as a parameter将任何委托作为参数的构造函数
【发布时间】:2023-03-16 12:49:01
【问题描述】:

这是简化的案例。我有一个存储一个委托的类,它将在完成时调用:

public class Animation
{
     public delegate void AnimationEnd();
     public event AnimationEnd OnEnd;
}

我有另一个实用程序类,我想订阅各种委托。在构造时,我希望自己注册到委托,但除此之外它不关心类型。问题是,我不知道如何在类型系统中表达它。这是我的伪C#

public class WaitForDelegate
{
    public delegateFired = false;

    // How to express the generic type here?
    public WaitForDelegate<F that's a delegate>(F trigger)
    {
        trigger += () => { delegateFired = true; };
    }
}

提前致谢!


感谢 Alberto Monteiro,我只使用 System.Action 作为事件的类型。我现在的问题是,如何将事件传递给构造函数以便它可以注册自己?这可能是一个非常愚蠢的问题。

public class Example
{
    Animation animation; // assume initialized

    public void example()
    {
        // Here I can't pass the delegate, and get an error like
        // "The event can only appear on the left hand side of += or -="
        WaitForDelegate waiter = new WaitForDelegate(animation.OnEnd);
    }
}

【问题讨论】:

    标签: c# .net delegates


    【解决方案1】:

    恐怕你不能按你的要求做。

    首先,您不能通过代表进行限制。最接近合法 C# 的代码是这样的:

    public class WaitForDelegate<F> where F : System.Delegate
    {
        public bool delegateFired = false;
    
        public WaitForDelegate(F trigger)
        {
            trigger += () => { delegateFired = true; };
        }
    }
    

    但它不会编译。

    但更大的问题是无论如何你都不能像这样传递代表。

    考虑这个简化的类:

    public class WaitForDelegate
    {
        public WaitForDelegate(Action trigger)
        {
            trigger += () => { Console.WriteLine("trigger"); };
        }
    }
    

    然后我尝试像这样使用它:

    Action bar = () => Console.WriteLine("bar");
    
    var wfd = new WaitForDelegate(bar);
    
    bar();
    

    唯一的输出是:

    bar
    

    trigger 这个词没有出现。这是因为委托是按值复制的,因此 trigger += () =&gt; { Console.WriteLine("trigger"); }; 行仅将处理程序附加到 trigger 而不是 bar

    使所有这些工作的方法是停止使用事件并使用 Microsoft 的反应式扩展 (NuGet "Rx-Main"),它允许您将事件转换为可以传递的基于 LINQ 的 IObservable&lt;T&gt; 实例.

    下面是我上面的示例代码的工作方式:

    public class WaitForDelegate
    {
        public WaitForDelegate(IObservable<Unit> trigger)
        {
            trigger.Subscribe(_ => { Console.WriteLine("trigger"); });
        }
    }
    

    你现在这样称呼它:

    Action bar = () => Console.WriteLine("bar");
    
    var wfd = new WaitForDelegate(Observable.FromEvent(h => bar += h, h => bar -= h));
    
    bar();
    

    这会产生输出:

    bar
    trigger
    

    请注意,Observable.FromEvent 调用包含在有权执行此操作的范围内附加和分离处理程序的代码。它允许通过对.Dispose() 的调用取消附加最终订阅调用。

    我已经使这个类变得非常简单,但更完整的版本应该是这样的:

    public class WaitForDelegate : IDisposable
    {
        private IDisposable _subscription;
    
        public WaitForDelegate(IObservable<Unit> trigger)
        {
            _subscription = trigger.Subscribe(_ => { Console.WriteLine("trigger"); });
        }
    
        public void Dispose()
        {
            _subscription.Dispose();
        }
    }
    

    如果您不想充分利用 Rx,另一种方法是这样做:

    public class WaitForDelegate : IDisposable
    {
        private Action _detach;
    
        public WaitForDelegate(Action<Action> add, Action<Action> remove)
        {
            Action handler = () => Console.WriteLine("trigger");
            _detach = () => remove(handler);
            add(handler);
        }
    
        public void Dispose()
        {
            if (_detach != null)
            {
                _detach();
                _detach = null;
            }
        }
    }
    

    你这样称呼它:

    Action bar = () => Console.WriteLine("bar");
    
    var wfd = new WaitForDelegate(h => bar += h, h => bar -= h);
    
    bar();
    

    这仍然是正确的输出。

    【讨论】:

    • 天哪。那是残酷的。有没有办法让一个类在不知道它的类型的情况下将自己注册到另一个类的事件中?我想这是不可能的,因为代表被复制了? ref 呢?
    • @AlexanderKondratskiy - 我已经为我的答案添加了一个可行的解决方案。
    • 感谢@Enigmativity!不幸的是,我在 Unity 中遇到了这个问题,不幸的是,Rx 不起作用。我很惊讶事件/代表等都是如此不透明的东西,而不是一流的价值观。
    • @AlexanderKondratskiy - 想象一下,如果他们是一等公民,那么任何地方的任何代码都可以开始分离事件。调试将是一场噩梦。为什么 Rx 不能与 Unity 一起使用?
    • @AlexanderKondratskiy:代表一流的价值观。但是在普通 C# 中没有办法形成对类成员的引用。但是,您可以将事件作为其组成部分 addremove 方法传递,这也是 FromEvent 调用所需要的。
    【解决方案2】:

    在 .NET 中已经有一个不接收任何参数的委托,它是 Action

    所以你的动画类可能是这样的:

    public class Animation
    {
         public event Action OnEnd;
    }
    

    但是你可以将事件作为参数传递,如果你尝试会收到这个编译错误

    事件只能出现在 += 或 -=" 的左侧

    所以让我们创建一个接口,并在那里声明事件

    public interface IAnimation
    {
        event Action OnEnd;
    }
    

    使用接口方法,您没有外部依赖项,您可以拥有许多实现它的类,这也是一个很好的做法,依赖于抽象而不是具体类型。有一个叫做 SOLID 的首字母缩写词解释了关于更好的 OO 代码的 5 个原则。

    然后你的动画类实现它

    Obs.: CallEnd 方法仅用于测试目的

    public class Animation : IAnimation
    {
        public event Action OnEnd;
    
        public void CallEnd()
        {
            OnEnd();
        }
    }
    

    现在您的 WaitForDelegate 将收到 IAnimation,因此该类可以处理任何实现 IAnimation 类的类

    public class WaitForDelegate<T> where T : IAnimation
    {
        public WaitForDelegate(T animation)
        {
            animation.OnEnd += () => { Console.WriteLine("trigger"); };
        }
    }
    

    然后我们可以测试我们用下面的代码做的代码

        public static void Main(string[] args)
        {
            var a = new Animation();
    
            var waitForDelegate = new WaitForDelegate<IAnimation>(a);
    
            a.CallEnd();
        }
    

    结果是

    触发

    这是 dotnetfiddle 上的工作版本

    https://dotnetfiddle.net/1mejBL

    重要提示

    如果你正在使用多线程,你必须小心避免空引用异常

    让我们再看一下我为测试添加的 CallEnd 方法

    public void CallEnd()
    {
        OnEnd();
    }
    

    OnEnd 事件可能没有值,然后如果您尝试调用它,您将收到 Null Reference Exception。

    因此,如果您使用的是 C# 5 或更低版本,请执行以下操作

    public void CallEnd()
    {
        var @event = OnEnd;
        if (@event != null)
            @event();
    }
    

    使用 C# 6 可能是这样的

    public void CallEnd()
        => OnEnd?.Invoke();
    

    更多解释,你可以有这个代码

    public void CallEnd()
    {
        if (OnEnd != null)
            OnEnd();
    }
    

    上面的这段代码可能会让你认为你可以免受空引用异常的影响,但是对于多线程解决方案,你不是。这是因为在执行if (OnEnd != null)OnEnd(); 之间可以将 OnEnd 事件设置为 null

    Jon Skeet 有一篇很好的文章,你可以看到 Clean event handler invocation with C# 6

    【讨论】:

    • 谢谢,这是有道理的。我有一个关于使用的问题。我将添加到我原来的问题
    • 这不是 OP 想要做的。根据您的回答,他想知道OnEnd 何时触发。
    • @Enigmativity 我刚刚改变了我的答案,我的答案还值得你投反对票吗?
    • 我已经删除了反对票,但我认为您的回答仍然不是最好的。 OP 正在寻找一种通用方法,但您提供了一种需要特定类/接口知识的方法。希望您不要介意我的反馈。
    • @Enigmativity 谢谢。我尊重你的意见,我同意 Eric Lipper/Jon Skeet 可以提供比我更好的答案,所以是的,我的答案不是最好的,但离坏的还很远,这是我的拙见。我想说的是,您的方法还需要有关 Rx 扩展的知识,恕我直言,比 OO 知识更难获得。需要明确的是,我并不是说您的解决方案不好,也值得一票!
    猜你喜欢
    • 1970-01-01
    • 2018-11-19
    • 1970-01-01
    • 2020-02-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多