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

Почему интерфейс не работает, но абстрактный класс делает это с общим ограничением класса?

В приведенном ниже коде показан общий класс с типом ограничения (Pub<T>). У класса есть событие, которое он может повысить, что позволяет нам передавать сообщение подписчикам. Ограничение состоит в том, что сообщение должно реализовать IMsg (или наследовать от IMsg, когда оно является абстрактным классом).

Pub<T> также предоставляет метод Subscribe, позволяющий объектам подписываться на событие notify тогда и только тогда, когда объект реализует IHandler<IMsg>.

Используя .NET 4, приведенный ниже код показывает ошибку на baseImplementer.NotifyEventHandler, в которой указано, что:
"No overload for 'IHandler<IMsg>.NotifyEventHandler(IMsg)' matches delegate 'System.Action<T>'"

Вопрос: (с обновленным методом Subscribe)

Почему ошибка исчезает, как только я меняю "IMsg" на абстрактный класс вместо интерфейса?

public interface IMsg { }        // Doesn't work
//public abstract class IMsg { } // Does work

public class Msg : IMsg { }

public class Pub<T> where T : IMsg
{
    public event Action<T> notify;

    public void Subscribe(object subscriber)
    {
        // Subscriber subscribes if it implements IHandler of the exact same type as T
        // This always compiles and works
        IHandler<T> implementer = subscriber as IHandler<T>;
        if (implementer != null)
            this.notify += implementer.NotifyEventHandler;
        // If subscriber implements IHandler<IMsg> subscribe to notify (even if T is Msg because Msg implements IMsg)
        // This does not compile if IMsg is an interface, only if IMsg is an abstract class
        IHandler<IMsg> baseImplementer = subscriber as IHandler<IMsg>;
        if (baseImplementer != null)
            this.notify += baseImplementer.NotifyEventHandler;
    }
}

public interface IHandler<T> where T : IMsg
{
    void NotifyEventHandler(T data);
}

Ниже приведен код ниже, чтобы воспроизвести проблему... но показывает, как использовать приведенный выше код. Очевидно, что IMsg (и производные классы Msg) будут определять или реализовывать методы, которые можно было бы вызвать в обработчике.

public class SubA : IHandler<Msg>
{
    void IHandler<Msg>.NotifyEventHandler(Msg data) { }
}

public class SubB : IHandler<IMsg>
{
    void IHandler<IMsg>.NotifyEventHandler(IMsg data) { }
}

class MyClass
{
    Pub<Msg> pub = new Pub<Msg>();
    SubA subA = new SubA();
    SubB subB = new SubB();

    public MyClass()
    {
        //Instead of calling...
        this.pub.notify += (this.subA as IHandler<Msg>).NotifyEventHandler;
        this.pub.notify += (this.subB as IHandler<IMsg>).NotifyEventHandler;

        //I want to call...
        this.pub.Subscribe(this.subA);
        this.pub.Subscribe(this.subB);

        //...except that the Subscribe method wont build when IMsg is an interface
    }
}
4b9b3361

Ответ 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. Если вы такой человек, которому это нравится, когда компилятор приносит проблемы вашему вниманию, а не догадывается о том, что вы имели в виду, С# может быть лучше для вас.

Ответ 2

Замените IMsg на T

public interface IMsg { }        // Doesn't work

public class Msg : IMsg { }

public class Pub<T> where T : IMsg
{
    public event Action<T> notify;

    public void Subscribe(object subscriber)
    {
        IHandler<T> implementer = subscriber as IHandler<T>; // here

        if (implementer != null)
        {
            this.notify += implementer.NotifyEventHandler;
        }
    }
}

public interface IHandler<T> where T : IMsg
{
    void NotifyEventHandler(T data);
}