30

在调查这个问题时,我很好奇 C# 4.0 中新的协变/逆变特性将如何影响它。

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

public event EventHandler<ClickEventArgs> Click;

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

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

...编译器会呕吐,因为它们是不兼容的委托类型。但在 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: Delegates 必须是相同的类型)。

如果您只添加两种委托类型中的任何一种,就可以了。但是多播中两种不同类型的组合在添加第二种时会导致异常。

我想这是 beta 1 中的 CLR 中的一个错误(编译器的行为看起来是正确的)。

发布候选更新:

上面的代码不再编译。一定是委托类型中的逆变性TEventArgs已经EventHandler<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# 编码标准现在可以采用一个新规则:不要从通过协变/逆变相关的多个委托类型形成多播委托。如果您不知道这意味着什么,请避免使用Actionfor 事件以确保安全。

当然,这个结论对最初的问题有影响,这个问题源于......

4

4 回答 4

11

很有意思。您不需要使用事件来查看这种情况,而且我确实发现使用简单的委托更简单。

考虑Func<string>Func<object>。在 C# 4.0 中,您可以将 a 隐式转换为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 确实必须处理 aFunc<string>和 aFunc<object>的组合;它没有其他信息可以继续。

这很讨厌,我真的希望它以某种方式得到修复。我会提醒 Mads 和 Eric 这个问题,以便我们获得更明智的评论。

于 2009-07-13T19:03:53.647 回答
3

我只需要在我的应用程序中解决这个问题。我做了以下事情:

// 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# 中的事件。我不明白为什么有语言功能,但它没有提供任何优势。

于 2012-10-05T06:52:15.263 回答
0

您是否从两者都获得了 ArgumentException ?如果仅由新处理程序引发异常,那么我认为它是向后兼容的。

顺便说一句,我认为您的评论混淆了。在 C# 3.0 中:

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

不会跑的。那是c#4.0

于 2009-07-13T18:24:14.243 回答
0

如果您在附加/分离时从内部将委托规范化为相同类型,则以下代码实际上可以工作!我没想到分离会起作用,但确实如此。它可能不是最优化的解决方案,但它简洁明了,它解决了我在 Foundation 中遇到的一个严重问题,其中协方差很重要,例如 IObservableList : IObservableListOut。在某些时候,扩展根据上下文 T 创建了不同的委托类型,其中 T 可以是每个上下文 2 个不同的接口,因此创建的委托最终具有不同的类型。直到今天我才真正理解这个问题是如何产生的。分离不适用于普通的“-值”。

    private event MyEventHandler<T> happened;
    public event MyEventHandler<T> Happened
    {
        add => this.happened += new MyEventHandler<T>(value);
        remove => this.happened -= new MyEventHandler<T>(value);
    }
于 2020-03-16T23:20:50.127 回答