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

Ошибки Co- и Contraвариантности в .NET 4.0

Некоторое странное поведение с поддержкой сочетанной и контравариантной поддержки С# 4.0:

using System;

class Program {
  static void Foo(object x) { }
  static void Main() {
    Action<string> action = _ => { };

    // C# 3.5 supports static co- and contravariant method groups
    // conversions to delegates types, so this is perfectly legal:
    action += Foo;

    // since C# 4.0 much better supports co- and contravariance
    // for interfaces and delegates, this is should be legal too:
    action += new Action<object>(Foo);
  }
}

Это результат с ArgumentException: Delegates must be of the same type.

Странно, не так ли? Почему Delegate.Combine() (который вызывается при выполнении операции += для делегатов) не поддерживает co- и contravariance во время выполнения?

Кроме того, я обнаружил, что тип делегата BCL System.EventHandler<TEventArgs> не имеет контравариантной аннотации на его общем параметре TEventArgs! Зачем? Это совершенно легальный тип TEventArgs, используемый только в позиции ввода. Может быть, нет контравариантной аннотации, потому что она прекрасно скрывает ошибку с помощью Delegate.Combine()? ;)

p.s. Все это влияет на VS2010 RC и более поздние версии.

4b9b3361

Ответ 1

Короче говоря: объединение делегатов все испорчено по отношению к разнице. Мы обнаружили это в конце цикла. Мы работаем с командой CLR, чтобы узнать, можем ли мы придумать какой-то способ сделать все общие сценарии работать без нарушения обратной совместимости и т.д., Но все, что мы придумали, вероятно, не попадет в версию 4.0. Надеюсь, мы получим все это в каком-то пакете обновления. Приносим извинения за неудобства.

Ответ 2

Ковариация и контравариантность задают отношение наследования между родовыми типами. Когда у вас есть ковариация и контравариантность, классы G<A> и G<B> могут находиться в некотором отношении наследования в зависимости от того, что A и B. Вы можете воспользоваться этим при вызове общих методов.

Однако метод Delegate.Combine не является общим и отчет ясно говорит, когда будет выбрано исключение:

ArgumentException - Оба и b не являются ссылкой null (Nothing в Visual Basic), а a и b не являются экземплярами одного и того же типа делегата.

Теперь Action<object> и Action<string> - это, конечно, экземпляры другого типа делегата (даже если они связаны через отношения наследования), поэтому в соответствии с документацией он генерирует исключение. Звучит разумно, что метод Delegate.Combine мог поддерживать этот сценарий, но это всего лишь возможное предложение (очевидно, это не было до сих пор, потому что вы не можете объявить унаследованные делегаты, поэтому до co/contra-variance у делегатов не было наследования отношения).

Ответ 3

Одна из трудностей с комбинацией делегатов заключается в том, что, если не указывать, какой операнд должен быть подтипом и который является супертипом, он не понимает, какой тип должен быть результатом. Можно написать оболочку factory, которая преобразует любой делегат с указанным числом аргументов и шаблоном byval/byref в супертип, но вызов такого factory несколько раз с тем же делегатом даст разные обертки (это может играть хаос с отменой события). В качестве альтернативы можно создать версии Delegate.Combine, которые будут принуждать правостороннего делегата к левому типу делегирования (в качестве бонуса, возврат не должен быть typecast), но нужно было бы написать специальную версию делегата.Удалить его.

Ответ 4

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

Сначала вам нужен вспомогательный метод:

public static class DelegateExtensions
{
    public static Delegate ConvertTo(this Delegate self, Type type)
    {
        if (type == null) { throw new ArgumentNullException("type"); }
        if (self == null) { return null; }

        if (self.GetType() == type)
            return self;

        return Delegate.Combine(
            self.GetInvocationList()
                .Select(i => Delegate.CreateDelegate(type, i.Target, i.Method))
                .ToArray());
    }

    public static T ConvertTo<T>(this Delegate self)
    {
        return (T)(object)self.ConvertTo(typeof(T));
    }
}

Когда у вас есть делегат:

public delegate MyEventHandler<in T>(T arg);

Вы можете использовать его, комбинируя делегаты, просто переведя делегат в нужный тип:

MyEventHandler<MyClass> handler = null;
handler += new MyEventHandler<MyClass>(c => Console.WriteLine(c)).ConvertTo<MyEventHandler<MyClass>>();
handler += new MyEventHandler<object>(c => Console.WriteLine(c)).ConvertTo<MyEventHandler<MyClass>>();

handler(new MyClass());

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

Полный код с некоторыми примерами вы можете найти здесь: http://ideone.com/O6YcdI