C#泛型、接口和继承

6

我有两个接口:

public interface IAmA
{
}

public interface IAmB<T> where T : IAmA
{
}

还有两个类实现这些接口,如下:

public class ClassA : IAmA
{
}

public class ClassB : IAmB<ClassA>
{
}

当试图使用以下类时:

public class Foo
{
    public void Bar()
    {
        var list = new List<IAmB<IAmA>>();
        list.Add(new ClassB());
    }
}

我收到了这个编译器错误信息:

无法将 'ClassB' 转换为 'IAmB<IAmA>'

我知道可以通过以下方式使编译器满意:

public class ClassB : IAmB<IAmA>
{
}

但我需要能够成为IAmB<>的类型参数,在ClassB中实现IAmA


1
“传递 IAmA 的实现” 是什么意思?ClassB 并没有从 ClassA 继承任何东西,它仍然必须实现 IAmB 的每个成员。为什么你必须从 IAmB<ClassA> 继承而不是 IAmB<IAmA>?你试图解决的实际问题是什么? - Panagiotis Kanavos
我更新了问题。 - Alex
更新没有解释任何东西 - 从一开始就很清楚你想使用具体类 - 问题是 为什么 - Panagiotis Kanavos
1
虽然您可以将T声明为协变参数(out T),但您只能在返回参数中使用它。您试图解决的实际问题是什么? - Panagiotis Kanavos
当然了 - ClassB不是IAmB<IAmA>。为什么你想这样处理它呢?如果你可以隐式地将object作为string参数传递给方法,那么这是否合理呢?因为这正是你在这里做的事情。 - Luaan
@PanagiotisKanavos 感谢您的问题“为什么?”我注意到我根本不需要对IAmB使用通用类型参数。 - Alex
5个回答

11

简单的回答是,如果类型参数IAmB<T>被声明为协变,只有当该类型作为返回类型使用时才能按你的要求执行:

public interface IAmB<out T> where T : IAmA
{
    T SomeMethod(string someparam);
}

out T 表示你可以使用比约束中指定的更具体的类型。

你不能将 T 用作参数。以下代码无法编译:

public interface IAmB<out T> where T : IAmA
{
    void SomeMethod(T someparam);
}

从文档中可以看到:

你可以将协变类型参数作为属于接口的方法的返回值,或者作为委托的返回类型。但是,你不能将协变类型参数用作接口方法的泛型类型约束。

这不是编译器的怪癖。 假设你可以声明一个协变的方法参数,那么你的列表最终会包含一些无法处理 IAmB<IAmA> 参数的对象 - 它们会期望输入 ClassA 或更具体的内容。你的代码会编译通过,但在运行时会失败。

这就引出了一个问题 - 为什么要使用 IAmB<ClassA> ?

在使用之前,你应该考虑一下,因为可能有其他更合适的方式来解决你实际的问题。通常很少使用一个实现具体类型的泛型接口,然后尝试将其用作实现另一个接口。

你可以查看 MSDN 文档中关于协变和逆变的部分,以及 Eric Lippert 和 Jon Skeet 在这个 SO 问题中的回答:协变和逆变的区别


3
快速答案:在您的接口中使泛型类型协变(请参见MSDN)。
public interface IAmB<out T> where T : IAmA
{
}

这将解决编译器问题。

但这并不能回答 Panagiotis Kanavos 提出的“为什么”问题!


2

关键在于将 IAmB<T> 中的类型约束 T 声明为 协变,使用 out 关键字:

public interface IAmB<out T> where T : IAmA
{
}

这使您可以使用比最初指定的更具体的类型,在这种情况下,允许您将IAmB<ClassA>分配给类型为IAmB<IAmA>的变量。

有关更多信息,请参见文档


0

我只是解释为什么会报错。

如果你的IAmB有一个方法

public interface IAmB<T> where T : IAmA
{
    void foo(T p);
}

public class ClassB : IAmB<ClassA>
{
    void foo(ClassA p)
    {
        p.someIntField++;
    }
}

我们还有另一个类

public class ClassC : IAmB<ClassA2>
{
    void foo(ClassA2 p)
    {
        p.someOtherIntField++;
    }
}

我们假设 List<IAmB<IAmA>>.Add(T p) 的实现如下

IAmA mParam = xxxx;
void Add(IAmB<IAmA>> p){
    p.foo(mParam);
}

认为所有编译都没有问题。你将一个ClassB实例传递给List.Add,它就变成了

void Add(IAmB<IAmA>> p){
    //p is ClassB now
    p.foo(mParam);//COMPILER CAN NOT MAKE SURE mParam fit ClassB.foo
}

这不是一个真正的答案。 - Panagiotis Kanavos

0

可以使用逆变和协变来解决。

public interface IAmA
{
}

**public interface IAmB<out T> where T : IAmA
{
}**


public class ClassA : IAmA
{
}

public class ClassB : IAmB<ClassA>
{
}


public class Foo
{
    public void Bar()
    {
        var list = new List<IAmB<IAmA>>();
        **list.Add(new ClassB());**
    }
}

现在你不会得到编译器错误。编译器很高兴。


不完全正确 - 协变性只允许 T 作为返回类型使用。 - Panagiotis Kanavos

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