Совместные и противоречивые ошибки в.NET 4.0

Некоторое странное поведение с поддержкой 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()? ;)

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

4 ответа

Решение

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

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

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

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

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

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

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

Другие вопросы по тегам