【问题标题】:Why doesn't an interface work but an abstract class does with a generic class constraint?为什么接口不起作用,而抽象类对泛型类约束起作用?
【发布时间】:2011-09-26 03:38:09
【问题描述】:

下面的代码显示了一个具有类型约束 (Pub<T>) 的泛型类。该类有一个可以引发的事件,允许我们将消息传递给订阅者。约束是消息必须实现IMsg(或者当它是抽象类时从IMsg继承)。

Pub<T> 还提供了一个Subscribe 方法,当且仅当对象实现IHandler<IMsg> 时,才允许对象订阅notify 事件。

使用 .NET 4,以下代码在 baseImplementer.NotifyEventHandler 上显示错误,指出:
"No overload for 'IHandler<IMsg>.NotifyEventHandler(IMsg)' matches delegate 'System.Action<T>'"

问题:(使用更新的订阅方法)

为什么我将 `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
    }
}

【问题讨论】:

  • 如果你在 Pub 类中为 T 添加 class 约束,它会编译,但我知道为什么。
  • 呃,我的意思是我“不”知道为什么。这可能很明显。 :)
  • @Charles:很好的观察。请参阅我的答案以获得解释。
  • @Charles... 我误解了您最初的评论。好的电话虽然。有了 Eric 的解释……现在说得通了。

标签: c# generics interface abstract-class constraints


【解决方案1】:

为什么我将IMsg 更改为抽象类而不是接口后,错误就消失了?

好问题!

失败的原因是您在从方法组委托类型的转换中依赖形参逆变,但是covariant and contravariant method group conversions 仅在每个可变类型都已知为引用类型时才合法。

为什么可变类型不是“已知为引用类型”?因为对 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 期望堆内存的 4 或 8 字节引用位于堆栈上。我们刚刚将堆栈错位了 16 到 20 个字节,结构的第一个或两个字段将被解释为指向内存的指针,从而导致运行时崩溃。

这就是为什么这是非法的。在处理操作之前需要对结构进行装箱,但您没有提供任何将结构装箱的代码!

有三种方法可以完成这项工作。

首先,如果您保证所有内容都是引用类型,那么一切正常。您可以将 IMsg 设为类类型,从而保证任何派生类型都是引用类型,或者您可以将“类”约束放在程序中的各种“T”上。

其次,你可以一致地使用 T:

class Pub<T> where T : IMsg
{
    public static Action<T> MakeSomeAction(IHandler<T> handler) // T, not IMsg
    {
        return handler.Notify; 
    }
}

现在您不能将Handler&lt;IMsg&gt; 传递给C&lt;SMsg&gt;.MakeSomeAction - 您只能传递Handler&lt;SMsg&gt;,这样它的Notify 方法就需要将要传递的结构。

第三,你可以编写装箱的代码:

class Pub<T> where T : IMsg
{
    public static Action<T> MakeSomeAction(IHandler<IMsg> handler) 
    {
        return t => handler.Notify(t); 
    }
}

现在编译器看到了,啊,他不想直接使用handler.Notify。相反,如果需要进行装箱转换,则中间函数会处理它。

有意义吗?

自 C# 2.0 以来,方法组到委托的转换在其参数类型上是逆变的,在其返回类型上是协变的。在 C# 4.0 中,我们还为接口和委托类型的转换添加了协变和逆变,这些类型被标记为对变体安全。从您在这里所做的各种事情看来,您可能会在接口声明中使用这些注释。有关必要背景,请参阅我关于此功能的设计因素的长系列。 (从底部开始。)

http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/

顺便说一句,如果您尝试在 Visual Basic 中使用这些类型的转换恶作剧,它会很高兴地允许您这样做。 VB 会做最后一件事;它会检测到类型不匹配,而不是告诉你它以便你修复它,它会代表你静默插入一个不同的委托,为你修复类型。一方面,这是一种很好的“按我的意思做,而不是按我说的做”功能,在那个看起来应该可以工作的代码中,它可以正常工作。另一方面,您要求从方法“Notify”中生成一个委托是相当出乎意料的,而您返回的委托被绑定到一个完全不同的方法,它是一个“通知”的代理。

在 VB 中,设计理念更多的是“默默地修正我的错误并按照我的意思行事”。在 C# 中,设计理念更多的是“告诉我我的错误,以便我可以自己决定如何修复它们”结束。两者都是合理的哲学;如果你是那种喜欢编译器为你做出好的猜测的人,你可能会考虑研究 VB。如果您是那种喜欢编译器将问题引起您注意而不是猜测您的意思的人,那么 C# 可能更适合您。

【讨论】:

  • 这是好东西!感谢您提供非常详细的解释。我第一次尝试找出我的问题的答案是在网上搜索接口和纯虚拟抽象类之间的所有差异。但我遇到的一切都是明显的多接口与单基。没有人真正提到过引用类型的概念。再次感谢!
  • 再次感谢您的精彩帖子和博客链接(也很棒)!通读后,我看到我现在可以在 IHandler 中使 T 逆变(IHandler),并且不再需要在我的订阅方法中检查订阅者是否是 IHandler... IHandler 会小心的就像我最初想要的那样。
【解决方案2】:

用 T 替换 IMsg

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);
}

【讨论】:

  • 有趣的事情......在我的实际应用中......我也这样做。我更新了我的订阅方法以显示两种方式。如果您使用我提供的额外代码运行它(我的问题的底部)。你会看到(当 IMsg 是一个抽象类时)baseImplementer 对于 subA 为 null,对于 subB 为 null(这就是我希望两者都工作的原因)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-08
  • 2016-01-26
  • 2014-09-23
  • 2012-01-23
相关资源
最近更新 更多