Совместные и противоречивые ошибки в .NET 4.0 - PullRequest
35 голосов
/ 21 февраля 2010

Некоторое странное поведение с поддержкой C # 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() (который вызывается при выполнении операции += с делегатами) не поддерживает ко-и контрвариантность во время выполнения?

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

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

Ответы [ 4 ]

38 голосов
/ 22 февраля 2010

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

6 голосов
/ 21 февраля 2010

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

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

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

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

1 голос
/ 23 декабря 2010

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

0 голосов
/ 05 мая 2015

Это решение было первоначально опубликовано 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

...