事件在.NET 4.0委托逆变和C#4.0(Event and delegate contravar

2019-06-25 19:07发布

在调查这个问题,我好奇的新的协方差/逆变功能如何在C#4.0会影响它。

在Beta 1中,C#似乎与CLR不同意。 早在C#3.0中,如果你有:

public event EventHandler<ClickEventArgs> Click;

...然后你在其他地方有:

button.Click += new EventHandler<EventArgs>(button_Click);

...编译器会BARF,因为他们是不兼容的委托类型。 但在C#4.0中,它编译罚款,因为在CLR 4.0类型参数现在被标记为in ,所以它是逆变的,所以编译器假定多播委托+=会工作。

下面是我的测试:

public class ClickEventArgs : EventArgs { }

public class Button
{
    public event EventHandler<ClickEventArgs> Click;

    public void MouseDown()
    {
        Click(this, new ClickEventArgs());
    }
}

class Program
{    
    static void Main(string[] args)
    {
        Button button = new Button();

        button.Click += new EventHandler<ClickEventArgs>(button_Click);
        button.Click += new EventHandler<EventArgs>(button_Click);

        button.MouseDown();
    }

    static void button_Click(object s, EventArgs e)
    {
        Console.WriteLine("Button was clicked");
    }
}

但是,尽管它编译,它在运行时不工作( ArgumentException :代表必须是同一类型的)。

这没关系,如果你只添加两个委托类型的任何一个。 但两种不同类型的在多播的组合导致当第二个被添加的异常。

我想这是在Beta 1中(编译器的行为看起来希望右)CLR中的错误。

更新发布候选:

上面的代码不再编译。 它必须是的逆变TEventArgsEventHandler<TEventArgs>委托类型已经被回滚,所以现在该委托具有相同的定义,在.NET 3.5。

也就是说,我看了一定有公测:

public delegate void EventHandler<in TEventArgs>(object sender, TEventArgs e);

现在又回到:

public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);

Action<T>委托参数T仍然逆变:

public delegate void Action<in T>(T obj);

这同样适用于Func<T>T是协变。

这一妥协使得有很大的意义,只要我们假定组播代表的主要用途是在事件的上下文中。 我个人发现,我从来没有使用多路广播委托,除非事件。

所以我想C#编码标准,现在可以采用新的规则:不形成从通过协方差/逆变相关多次委托类型多路广播委托。 如果你不知道这意味着什么,只是避免使用Action事件是在安全方面。

当然,这个结论对影响原来的问题,这从一个成长 ...

Answer 1:

很有意思。 你并不需要用事件来看到这种情况出现,而事实上我觉得它更容易使用简单的代表。

考虑Func<string>Func<object> 在C#4.0中,你可以隐式转换一个Func<string>Func<object>因为你总是可以使用一个字符串作为参考对象引用。 但是,当你试图把它们混合起来的东西出问题。 下面是一个简短但完整的节目展示了两种不同的方式的问题:

using System;

class Program
{    
    static void Main(string[] args)
    {
        Func<string> stringFactory = () => "hello";
        Func<object> objectFactory = () => new object();

        Func<object> multi1 = stringFactory;
        multi1 += objectFactory;

        Func<object> multi2 = objectFactory;
        multi2 += stringFactory;
    }    
}

编译没有问题,但无论是的Combine呼吁(由+ =语法糖是隐藏的)抛出异常。 (注释出第一个看到第二个。)

这肯定是有问题,但我不完全确定的解决方案应该是什么。 这有可能是在执行时委托代码需要制定出最合适的类型使用基于所涉及的委托类型。 这是一个有点讨厌。 这将是相当不错的一个通用Delegate.Combine电话,但你不能真正表达相关类型以有意义的方式。

有一两件事值得一提的是,协变转换是引用转换-在上面, multi1stringFactory指向同一个对象:它是一样的写作

Func<object> multi1 = new Func<object>(stringFactory);

(在这一点上,下面一行将与没有例外执行)在执行时,所述BCL确实必须处理一个Func<string>和一个Func<object>被组合; 它没有其他的信息去。

这是肮脏的,我认真希望它被固定在某种方式。 我要提醒的Mads和Eric这个问题,所以我们可以得到一些更明智的评论。



Answer 2:

我只是在我的应用程序来解决这个问题。 我做了以下内容:

// variant delegate with variant event args
MyEventHandler<<in T>(object sender, IMyEventArgs<T> a)

// class implementing variant interface
class FiresEvents<T> : IFiresEvents<T>
{
    // list instead of event
    private readonly List<MyEventHandler<T>> happened = new List<MyEventHandler<T>>();

    // custom event implementation
    public event MyEventHandler<T> Happened
    {
        add
        {
            happened.Add(value);
        }
        remove
        {
            happened.Remove(value);
        }
    }

    public void Foo()
    {
        happened.ForEach(x => x.Invoke(this, new MyEventArgs<T>(t));
    }
}

我不知道是否有定期多播事件相关的差异。 至于我用它,它的工作原理...

顺便说一句: 我从来不喜欢在C#中的事件 。 我不明白为什么有语言功能,当它不提供任何好处。



Answer 3:

您是否获得来自两个ArgumentException的? 如果该异常是由刚刚新的处理程序抛出,那么我会认为这是向后兼容。

顺便说一句,我觉得你有你的意见混淆。 在C#3.0的:

button.Click += new EventHandler<EventArgs>(button_Click); // old

不会有运行。 这是C#4.0



文章来源: Event and delegate contravariance in .NET 4.0 and C# 4.0