将任何委托作为参数的构造方法

时间:2016-01-10 01:59:57

标签: c# .net delegates

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

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);
    }
}

2 个答案:

答案 0 :(得分:4)

我担心你不能做你所要求的。

首先,你不能被代表约束。与合法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 += () => { Console.WriteLine("trigger"); };仅将处理程序附加到trigger而不是bar

您可以完成所有这些工作的方法是停止使用事件并使用Microsoft的Reactive Extensions(NuGet“Rx-Main”),它允许您将事件转换为可以获取的基于LINQ的IObservable<T>实例传了过来。

以上是我上面的示例代码的工作原理:

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();

那仍然是正确的输出。

答案 1 :(得分:2)

在.NET中,已经有一个代理人没有收到任何参数,它是Action

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

public class Animation
{
     public event Action OnEnd;
}

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

  

该事件只能出现在+ =或 - =&#34;

的左侧

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

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();
}

上面的代码可能会让您认为您可以安全地使用Null Reference Exception,但是对于多线程解决方案,您不是。这是因为OnEnd事件可以在执行if (OnEnd != null)OnEnd();之间设置为空

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