Сравнение событий и делегатов в .NET 4.0 и C # 4.0 - PullRequest
27 голосов
/ 13 июля 2009

При изучении этого вопроса мне стало интересно, как новые функции ковариации / контравариантности в C # 4.0 повлияют на него.

В бета-версии 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: делегаты должны быть одного типа).

Это нормально, если вы добавляете только один из двух типов делегатов. Но комбинация двух разных типов в многоадресной рассылке вызывает исключение при добавлении второго.

Я полагаю, что это ошибка в CLR в бета-версии 1 (поведение компилятора выглядит, надеюсь, правильным).

Обновление для кандидата на выпуск:

Приведенный выше код больше не компилируется. Должно быть, откат 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 # теперь могут принять новое правило: не формировать многоадресных делегатов из нескольких типов делегатов, связанных ковариацией / контравариантностью. И если вы не знаете, что это значит, просто избегайте использования Action, чтобы события были в безопасности.

Конечно, этот вывод имеет значение для первоначального вопроса о том, что этот вопрос вырос из ...

Ответы [ 3 ]

9 голосов
/ 13 июля 2009

Очень интересно. Вам не нужно использовать события, чтобы увидеть, как это происходит, и я считаю, что проще использовать простые делегаты.

Рассмотрим 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, но вы не могли бы по-настоящему выразить соответствующие типы осмысленно.

Стоит отметить, что ковариантное преобразование является эталонным преобразованием - в приведенном выше примере multi1 и stringFactory относятся к одному и тому же объекту: это , а не то же, что и запись

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

(На этом этапе следующая строка будет выполняться без исключения.) Во время выполнения BCL действительно имеет дело с объединением Func<string> и Func<object>; у него нет другой информации.

Это противно, и я серьезно надеюсь, что это каким-то образом исправят. Я оповестю Мэдса и Эрика на этот вопрос, чтобы мы могли получить более информированный комментарий.

2 голосов
/ 05 октября 2012

Мне просто нужно было исправить это в моем приложении. Я сделал следующее:

// 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 # . Я не понимаю, почему существует языковая функция, которая не дает никаких преимуществ.

0 голосов
/ 13 июля 2009

Получаете ли вы ArgumentException от обоих? Если исключение генерируется только новым обработчиком, то я думаю, что оно обратно совместимо.

Кстати, я думаю, что вы перепутали свои комментарии. В C # 3.0 это:

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

не побежал бы. Это с # 4.0

...