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

Изучая этот вопрос, мне стало любопытно, как на него повлияют новые функции ковариации / контравариантности в 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);

То же самое касается T Func<T> ковариантности.

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

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

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


events c# delegates c#-4.0 clr4.0
person Daniel Earwicker    schedule 13.07.2009    source источник
comment
Хотя интересно хочешь вопрос?   -  person Rune FS    schedule 05.10.2012
comment
Я удивлен, что эта дыра ускользнула от внимания команды C #, должно быть, это одна из первых вещей, которые они бы протестировали после введения дисперсии для общих делегатов, не так ли? C # 5 тоже демонстрирует это (версия clr такая же).   -  person nawfal    schedule 08.07.2014


Ответы (4)


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

Рассмотрим 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>; у него нет другой информации, чтобы продолжить.

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

person Jon Skeet    schedule 13.07.2009
comment
Круто, я получил практически тот же пример кода, когда возился с ним в поезде домой. Было бы очень интересно услышать корявые подробности. - person Daniel Earwicker; 13.07.2009
comment
Какой у этого статус? Я хотел бы использовать универсальные шаблоны и ограничение вариации в экземплярах делегатов во время выполнения, и мне не нужна возможность многоадресной рассылки, но я хотел бы предотвратить следующий разработчик, предполагающий, что многоадресная рассылка уже задействована, и она дает сбой с этим инструментом для обработки разума. Есть ли устоявшаяся передовая практика или, возможно, предупреждение компилятора, которое обнаружит это и предоставит подсказку для использования шаблона конструктора New. - person Andyz Smith; 28.07.2019
comment
@AndyzSmith: Боюсь, что никаких изменений, о которых я знаю, не произошло. - person Jon Skeet; 28.07.2019

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

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

person Stefan Steinegger    schedule 05.10.2012
comment
Основное отличие состоит в том, что делегаты многоадресной рассылки неизменяемы, поэтому добавление / удаление обработчиков во время вызова является потокобезопасным. В вашей реализации список может изменяться во время вызова события с непредсказуемыми результатами. - person Søren Boisen; 27.05.2015
comment
@ SørenBoisen: очень хорошее замечание. Спасибо за это. Я мог бы использовать ConcurrentBag<T> вместо списка. - person Stefan Steinegger; 28.05.2015
comment
Другой вариант - использовать ImmutableArray или ImmutableList из blogs.msdn.com/b/dotnet/archive/2013/09/25/. Это будет оптимизировать для отправки по сравнению с добавлением / удалением, но, что более важно, они доступны в проектах PCL :-) - person Søren Boisen; 28.05.2015
comment
отличный старт - уверен, можно оптимизировать - person Demetris Leptos; 17.03.2020
comment
это действительно сработало (!): произошло частное событие MyEventHandler ‹T›; публичное событие MyEventHandler ‹T› Happvened {add = ›this.happened + = new MyEventHandler ‹T› (значение); remove = ›this.happened - = new MyEventHandler ‹T› (значение); } - person Demetris Leptos; 17.03.2020

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

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

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

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

person Wesley Wiser    schedule 13.07.2009
comment
Я удалил все ссылки на управление версиями в вопросе, так как это, кажется, только вызывало у вас замешательство. Комментарии о новом и старом, связанные с проблемой, о которой я изначально думал, из которой я отделил этот вопрос. Этот вопрос не имеет ничего общего с обратной совместимостью как таковой. Речь идет о том, что компилятор предполагает, что делегат многоадресной рассылки может связываться с методами контравариантных типов, что затем оказывается неверным во время выполнения. - person Daniel Earwicker; 13.07.2009

Следующий код действительно работает, если у вас есть делегаты, нормализованные к одному и тому же типу изнутри при присоединении / отсоединении! Я не ожидал, что отсоединение сработает, но оно работает. Возможно, это не самое оптимизированное решение, но оно чистое и простое, и оно решает серьезную проблему, с которой я столкнулся с моим Foundation, где важна ковариация, например IObservableList: IObservableListOut. В какой-то момент расширения создавали разные типы делегатов в зависимости от контекста T, где T может быть двумя разными интерфейсами для каждого контекста, и поэтому созданные делегаты в конечном итоге имели другой тип. Только сегодня я действительно понял, чем закончилась эта проблема. Отсоединение не будет работать с обычным "- значением".

    private event MyEventHandler<T> happened;
    public event MyEventHandler<T> Happened
    {
        add => this.happened += new MyEventHandler<T>(value);
        remove => this.happened -= new MyEventHandler<T>(value);
    }
person Demetris Leptos    schedule 16.03.2020