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

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

В бета-версии 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>"s T быть ковариантным.

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

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

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

3 ответа

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

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

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

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

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

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

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

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

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

Следующий код действительно работает, если у вас есть делегаты, нормализованные к одному и тому же типу изнутри при присоединении / отсоединении! Я не ожидал, что отсоединение сработает, но оно работает. Возможно, это не самое оптимизированное решение, но оно чистое и простое, и оно решает серьезную проблему, с которой я столкнулся с моим 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);
    }
Другие вопросы по тегам