Ответ 1
Почему ошибка исчезает, как только я изменяю
IMsg
на абстрактный класс вместо интерфейса?
Хороший вопрос!
Причина, по которой это не удается, заключается в том, что вы полагаетесь на формальную параметрическую контравариантность при преобразовании из группы методов в тип делегата, но ковариантные и контравариантные преобразования групп методов делегатам разрешены только тогда, когда каждый различный тип известен как ссылочный тип.
Почему переменный тип не "известен как ссылочный тип"? Поскольку ограничение интерфейса на T не также ограничивает T ссылочным типом. Он ограничивает T как любой тип, реализующий интерфейс, но типы структур также могут реализовывать интерфейсы!
Когда вы делаете ограничение абстрактным классом вместо интерфейса, тогда компилятор знает, что T должен быть ссылочным типом, потому что только ссылочные типы могут расширять предоставляемые пользователем абстрактные классы. Затем компилятор знает, что дисперсия безопасна и позволяет это.
Посмотрите на гораздо более простую версию вашей программы и посмотрите, как это происходит, если вы разрешите преобразование, которое вы хотите:
interface IMsg {}
interface IHandler<T> where T : IMsg
{
public void Notify(T t);
}
class Pub<T> where T : IMsg
{
public static Action<T> MakeSomeAction(IHandler<IMsg> handler)
{
return handler.Notify; // Why is this illegal?
}
}
Это незаконно, потому что вы могли бы сказать:
struct SMsg : IMsg { public int a, b, c, x, y, z; }
class Handler : IHandler<IMsg>
{
public void Notify(IMsg msg)
{
}
}
...
Action<SMsg> action = Pub<SMsg>.MakeSomeAction(new Handler());
action(default(SMsg));
Хорошо, теперь подумайте о том, что это делает. На стороне вызывающего абонента ожидается, что действие поместит 24-байтовую структуру S в стек вызовов и ожидает, что вызывающая сторона обработает ее. Вызов, Handler.Notify, ожидает, что в стек кучи будет четыре или восемь байтов, чтобы быть в стеке. Мы просто смещали стек между 16 и 20 байтами, и первое поле или два из структуры будет интерпретироваться как указатель на память, сбой среды выполнения.
Вот почему это незаконно. Структура должна быть помещена в бокс до того, как действие будет обработано, но нигде вы не указали какой-либо код, который блокирует структуру!
Существует три способа сделать эту работу.
Во-первых, если вы гарантируете, что все будет ссылочным типом, тогда все это получится. Вы можете либо сделать IMsg типом класса, тем самым гарантируя, что любой производный тип является ссылочным типом, или вы можете поместить ограничение "class" на различные "T" s в вашей программе.
Во-вторых, вы можете последовательно использовать T:
class Pub<T> where T : IMsg
{
public static Action<T> MakeSomeAction(IHandler<T> handler) // T, not IMsg
{
return handler.Notify;
}
}
Теперь вы не можете передать Handler<IMsg>
в C<SMsg>.MakeSomeAction
- вы можете передать только Handler<SMsg>
, чтобы его метод Notify ожидал передачу структуры.
В-третьих, вы можете написать код, который делает бокс:
class Pub<T> where T : IMsg
{
public static Action<T> MakeSomeAction(IHandler<IMsg> handler)
{
return t => handler.Notify(t);
}
}
Теперь компилятор видит, ах, он не хочет использовать handler.Notify напрямую. Скорее, если необходимо преобразование бокса, то промежуточная функция позаботится об этом.
Имеют смысл?
Преобразования групп методов в делегаты были контравариантными по своим типам параметров и ковариантны в своих возвращаемых типах с С# 2.0. В С# 4.0 мы также добавили ковариацию и контравариантность в преобразованиях на интерфейсах и типах делегатов, которые помечаются как безопасные для дисперсии. Это похоже на то, что вы делаете здесь, что вы можете использовать эти аннотации в своих объявлениях интерфейса. См. Мою длинную серию о конструктивных факторах этой функции для необходимого фона. (Начните снизу.)
http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/
Кстати, если вы попытаетесь вытащить эти виды махинаций преобразования в Visual Basic, это с радостью позволит вам. VB будет делать эквивалент последней вещи; он обнаружит, что существует несоответствие типа, и вместо того, чтобы сообщать вам об этом, чтобы вы могли его исправить, он будет тихо вставлять от вашего имени другой делегат, который исправляет типы для вас. С одной стороны, это хороший вид "сделать то, что я имею в виду не то, что я говорю", в том кодексе, который выглядит так, как будто он должен работать просто работает. С другой стороны, довольно неожиданно, что вы просите делегата сделать из метода "Уведомлять", и делегат, которого вы получите, связан с совершенно другим методом, который является прокси-сервером для "Уведомлять".
В VB философия дизайна больше связана с "молчанием моих ошибок и тем, что я имел в виду" в конце спектра. В С# философия дизайна больше касается "расскажите мне о моих ошибках, чтобы я мог решить, как их исправить". Оба являются разумными философиями; если вы такой человек, который любит, когда компилятор делает хорошие догадки для вас, вы можете рассмотреть возможность поиска в VB. Если вы такой человек, которому это нравится, когда компилятор приносит проблемы вашему вниманию, а не догадывается о том, что вы имели в виду, С# может быть лучше для вас.