C#如何将继承的泛型接口进行转换

20

我在理解如何转换我设计的接口方面遇到了一些问题。这是一个针对C# Windows Forms的MVP设计。我有一个IView类,在我的表单类上实现它。还有一个IPresenter,我将其派生为各种特定的Presenter。每个Presenter将根据不同的角色以不同的方式管理IView,例如使用AddPresenter打开对话框以输入新数据,而使用EditPresenter编辑现有数据,并将数据预加载到表单上。它们都继承自IPresenter。我想这样使用代码:

AddPresenter<ConcreteView> pres = new AddPresenter<ConcreteView>();

我基本上已经把这个做好了,但是这些Presenter和它们所管理的View被捆绑成插件,这些插件是在运行时后加载的,这意味着我需要一个Manager类作为插件接口来接受一个"mode"参数。该模式参数用于工厂方法创建Add或Edit Presenter,但是因为对话框的调用稍后才进行,所以我需要通过IPresenter接口进行调用,如下所示:
private IPresenter<IView> pres;
public ShowTheForm()
{
    pres.ShowDialog();
}

现在我在将一个AddPresenter实例的大小写转换为'pres'成员时遇到问题。以下是我所拥有的简化版本:

interface IView
{
    void ViewBlah();
}

interface IPresenter<V> where V : IView
{
    void PresBlah();
}

class CView : IView
{
    public void ViewBlah()
    {        
    }
}

class CPresenter<T> : IPresenter<T> where T : IView
{
    public void PresBlah()
    {
    }
}

private void button3_Click(object sender, EventArgs e)
{
    CPresenter<CView> cpres = new CPresenter<CView>();
    IPresenter<IView> ipres = (IPresenter<IView>)cpres;
}

这是错误信息:

Unable to cast object of type 'CPresenter`1[MvpApp1.MainForm+CView]' to type 'IPresenter`1[MvpApp1.MainForm+IView]'.

据我所知,Presenter和Generic类型规范都是接口的子类,所以我不明白为什么它无法进行强制转换。
有什么想法吗?
史蒂夫
2个回答

32

问题在于泛型类型参数。如果将接口参数协变,则强制转换将起作用。

可以通过添加out关键字来实现:

interface IPresenter<out V> where V : IView
{
    void PresBlah();

}
你可以通过以下 MSDN 文章了解这个功能的更多信息:泛型中的协变性和逆变性。其中的具有协变类型参数的通用接口部分特别适用于你的问题。
更新:确保查看 @phoog 和我之间的评论。如果你的实际代码接受一个输入为 V,你将无法使它协变。引用的文章和 @phoog 的答案对这种情况进行了进一步的说明。

4
只有在V仅用于输出位置的情况下,此方法才能起作用。例如,在类型为V的方法参数或属性设置器中将无法使用该方法。 - phoog
2
@phoog,你是正确的。这与OP发布的代码一致,并在引用的文章中得到了充分的解释。 - smartcaveman
2
但是楼主的“简化版本”示例省略了类型参数的所有用法。可以推测,在实际接口中,类型参数确实被用在某个地方;它可能被用在一个输入位置,所以该接口可能不适合协变。 - phoog
1
@StephenYork,听起来你可能还想在V上加一个“new”约束。 - smartcaveman
非常感谢您的解释和参考资料。我的设计思路是Presenter会接收它所呈现的表单类型,并在内部构建它,使用'view = new V();'。然后,通过各种方法向视图传递数据以填充控件、启用、执行表单验证等操作。 我只需要在具体的Presenter中进行转换,因为稍后我需要调用接口方法,这样就可以减少管理工作量。 我会认真阅读上面提到的文章,并尝试在明天让它正常工作。 - Stephen York

10

CPresenter<CView>不是一个IPresenter<IView>,就像List<int[]>不是一个IList<IEnumerable>一样。

考虑一下,如果你可以得到一个IList<IEnumerable>引用到一个List<int>,你可以向其中添加一个string[],这将不得不抛出异常。静态类型检查的整个目的是防止编译这样的代码。

如果接口允许,你可以将类型参数声明为协变的(IPresenter<out V> where V : ...)。然后接口将更像IEnumerable<out T>。只有当类型参数从未在输入位置使用时才可能这样做。

回到List<int[]>的例子,将其视为IEnumerable<IEnumerable>是安全的,因为你不能向IEnumerable<T>引用中添加任何内容;你只能读取其中的内容,并且依次,将int[]视为IEnumerable也是安全的,所以一切都很好。


感谢 phoog 提供的信息。我完全不知道发生了什么,所以根本没有在寻找协方差。但这很有道理。 “只有在类型参数从未用于输入位置时才可能实现”在我的接口中得到满足。CPresenter将在IView的实现内部调用new,并在内部进行管理。 - Stephen York

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接