Совместные и противоречивые ошибки в.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