EventHandlers и Covariance - PullRequest
       19

EventHandlers и Covariance

5 голосов
/ 31 марта 2012

Я пытался создать общее событие. В основном это должно выглядеть так:

namespace DelegateTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var lol = new SomeClass();
            lol.SomeEvent += handler;
        }

        static void handler(object sender, SomeDerivedClass e)
        {

        }

    }

    class SomeClass
    {

        public delegate void SomeEventDelegate<in T>(object sender, T data);
        public event SomeEventDelegate<ISomeInterface> SomeEvent;

    }

    interface ISomeInterface
    {
    }

    class SomeDerivedClass : ISomeInterface
    {
    }
}

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

"in" указывает на разницу, верно? Это означает, что если API ожидает чего-то более общего, вы можете передать ему что-то более конкретное (в моей базе «ISomeInterface» будет общим, а мой «SomeDerivedClass» будет конкретным). Мне, однако, сообщают моему компилятору, что «никакая перегрузка для обработчика метода не соответствует DelegateTest.SomeClass.SomeEventDelegate».

Мне интересно, почему это не работает. Какие проблемы могут быть вызваны, если бы это было? Или я что-то упустил для работы?

Заранее спасибо!

Ответы [ 2 ]

6 голосов
/ 31 марта 2012

"in" указывает на разницу, верно?

Да.

Это означает, что если API ожидает чего-то более общего, вы можете передатьэто что-то более конкретное (в моей базе «ISomeInterface» будет общим, а мой «SomeDerivedClass» будет конкретным).

Нет.Контравариантность делегата позволяет делегату ссылаться на метод с типами параметров, которые являются менее производными, чем в типе делегата.Например, предположим, что ISomeInterface имеет базовый интерфейс:

interface ISomeBaseInterface
{
}

interface ISomeInterface : ISomeBaseInterface
{
}

И предположим, что handler занял ISomeBaseInterface вместо SomeDerivedClass:

static void handler(object sender, ISomeBaseInterface e) 

Тогда new SomeClass().SomeEvent += handler будетработа.

Вот почему исходный код не является безопасным типом: когда SomeClass повышает SomeEvent, он может потенциально передать что угодно , которое реализует ISomeInterface в качестве аргумента data,Например, он мог бы передать экземпляр SomeDerivedClass, но он также мог бы передать экземпляр

class SomeOtherDerivedClass : ISomeInterface
{
}

. Если бы вы могли зарегистрировать void handler(object sender, SomeDerivedClass e) с событием, этот обработчик будет вызванс SomeOtherDerivedClass, который не работает.

Таким образом, вы можете зарегистрировать обработчики событий, которые на более общие , чем тип события, а не обработчики событий, которые более конкретны.

ОБНОВЛЕНИЕ: Вы прокомментировали:

Ну, на самом деле я хочу перебрать список и проверить типы.Поэтому, если событие должно было быть запущено с объектом данных типа, скажем, SomeOtherDerivedObject, тогда программа будет перебирать список методов, которые подписаны на событие, пока не найдет тот, который соответствует сигнатуре (объект, SomeOtherDerivedObject).Таким образом, само событие будет использоваться только для хранения, а не для фактического вызова делегатов.

Я не думаю, что C # позволяет вам объявить event, который работает с произвольными типами делегатов.Вот как вы можете написать методы, которые добавляют обработчики событий и вызывают их:

class SomeClass
{
    private Delegate handlers;

    public delegate void SomeEventDelegate<in T>(object sender, T data);

    public void AddSomeEventHandler<T>(SomeEventDelegate<T> handler)
    {
        this.handlers = Delegate.Combine(this.handlers, handler);
    }

    protected void OnSomeEvent<T>(T data)
    {
        if (this.handlers != null)
        {
            foreach (SomeEventDelegate<T> handler in
                this.handlers.GetInvocationList().OfType<SomeEventDelegate<T>>())
            {
                handler(this, data);
            }
        }
    }
}
2 голосов
/ 14 декабря 2012

Одно существенное неудобство с противоречивостью делегата состоит в том, что в то время как делегат типа, например Action<Fruit> может быть передано подпрограмме, ожидающей Action<Banana>, попытка объединить два делегата с фактическими типами Action<Fruit> и Action<Banana> не удастся *, даже если оба делегата имеют тип "время компиляции" Action<Banana>. Чтобы обойти это, я бы предложил использовать метод, подобный следующему:

    static T As<T>(this Delegate del) where T : class
    {
        if (del == null || del.GetType() == typeof(T)) return (T)(Object)del;
        Delegate[] invList = ((Delegate)(Object)del).GetInvocationList();
        for (int i = 0; i < invList.Length; i++)
            if (invList[i].GetType() != typeof(T))
                invList[i] = Delegate.CreateDelegate(typeof(T), invList[i].Target, invList[i].Method);
        return (T)(Object)Delegate.Combine(invList);
    }

Учитывая делегат и тип делегата, этот метод проверяет, точно ли тип переданного делегата точно соответствует указанному типу; если это не так, но метод (ы) в исходном делегате имеют надлежащие подписи для указанного типа, будет создан новый делегат с необходимыми характеристиками. Обратите внимание, что если в двух отдельных случаях этой функции передаются делегаты, которые не имеют надлежащего типа, но которые сравниваются равными друг другу, делегаты, возвращенные этим методом, также будут сравниваться равными друг другу. Таким образом, если у человека есть событие, которое должно принять делегата типа Action<string>, можно использовать вышеупомянутый метод для преобразования, например. переданный Action<object> в «настоящий» Action<string> перед добавлением или удалением его из события.

Если кто-то будет добавлять или вычитать переданный делегат из поля с соответствующим типом делегата, вывод типа и поведение Intellisense могут быть улучшены, если используются следующие методы:

    static void AppendTo<T>(this Delegate newDel, ref T baseDel) where T : class
    {
        newDel = (Delegate)(Object)newDel.As<T>();
        T oldBaseDel, newBaseDel;
        do
        {
            oldBaseDel = baseDel;
            newBaseDel = (T)(Object)Delegate.Combine((Delegate)(object)oldBaseDel, newDel);
        } while (System.Threading.Interlocked.CompareExchange(ref baseDel, newBaseDel, oldBaseDel) != oldBaseDel);
    }

    static void SubtractFrom<T>(this Delegate newDel, ref T baseDel) where T : class
    {
        newDel = (Delegate)(Object)newDel.As<T>();
        T oldBaseDel, newBaseDel;
        do
        {
            oldBaseDel = baseDel;
            newBaseDel = (T)(Object)Delegate.Remove((Delegate)(object)oldBaseDel, newDel);
        } while (System.Threading.Interlocked.CompareExchange(ref baseDel, newBaseDel, oldBaseDel) != oldBaseDel);
    }

Эти методы появятся как методы расширения для типов, производных от Delegate, и позволят добавлять или вычитать экземпляры таких типов из переменных или полей подходящих типов делегатов; такое сложение или вычитание будет выполняться в поточно-ориентированном режиме, поэтому возможно использование этих методов в методах добавления / удаления событий без дополнительной блокировки.

...