Подтвердить что ты не робот

Контравариантность событий и делегатов в .NET 4.0 и С# 4.0

Изучая этот вопрос, мне стало интересно, как это повлияет на новые возможности ковариации/контравариантности в С# 4.0.

В бета-версии 1 С#, похоже, не согласен с CLR. Вернитесь в С# 3.0, если у вас есть:

public event EventHandler<ClickEventArgs> Click;

... а затем в другом месте:

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

... компилятор будет barf, потому что это несовместимые типы делегатов. Но в С# 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 является ковариантным.

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

Итак, я полагаю, что стандарты кодирования С# теперь могут принять новое правило: не формируйте делегаты многоадресной передачи из нескольких типов делегатов, связанных с ковариацией/контравариантностью. И если вы не знаете, что это значит, просто избегайте использования Action для событий, находящихся на безопасной стороне.

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

4b9b3361

Ответ 1

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

Рассмотрим Func<string> и Func<object>. В С# 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

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

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

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

Кстати: Мне никогда не нравились события на С#. Я не понимаю, почему существует языковая функция, когда она не дает никаких преимуществ.

Ответ 3

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

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

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

не запускался. Это С# 4.0