约束泛型与类型参数层次结构

6

我在C#中遇到了泛型的问题,希望你能帮我解决。

public interface IElement { }

public interface IProvider<T> where T : IElement {
    IEnumerable<T> Provide();
}

到目前为止,这很简单。我希望提供程序返回特定元素的可枚举对象。 以下是接口的具体实现:

public class MyElement : IElement { }

public class MyProvider : IProvider<MyElement> {
    public IEnumerable<MyElement> Provide() {
        [...]
    }
}

但是当我想要使用它时,问题就出现了。这段代码无法编译,因为它不能隐式地将MyProvider转换为IProvider<IElement>

IProvider<IElement> provider = new MyProvider();

尽管MyProvider是一个IProvider<MyElement>,而MyElement是一个IElement,但我仍然需要将其强制转换为IProvider<IElement>。我可以通过让MyProvider也实现IProvider<MyElement>来避免强制转换,但为什么它不能解决类型参数中的层次结构呢?
编辑:根据Thomas的建议,我们可以使其在T上协变。但如果存在其他像下面这样的具有T类型参数的方法怎么办?
public interface IProvider<T> where T : IElement {
    IEnumerable<T> Provide();
    void Add(T t);
}
3个回答

8

尽管MyProvider是一个IProvider<MyElement>,而且MyElement是一个IElement,但我必须将其转换为IProvider<IElement>。为什么它不能解析类型参数中的层次结构?

这是一个非常常见的问题。考虑以下等效问题:

interface IAnimal {}
class Tiger : IAnimal {}
class Giraffe : IAnimal {}
class MyList : IList<Giraffe> { ... }
...
IList<IAnimal> m = new MyList();

现在你的问题是:“尽管MyList是一个IList,而Giraffe是一个IAnimal,我仍然必须将其转换为IList。为什么这不起作用?”

它不起作用是因为...假设它确实起作用:

m.Add(new Tiger());

m是一个动物列表。你可以将一只老虎添加到动物列表中。但实际上m是一个MyList,而且一个MyList只能包含长颈鹿!如果我们允许这样做,那么你就可以将一只老虎添加到长颈鹿列表中

这必须失败,因为IList<T>有一个接受T的Add方法。现在,也许你的接口没有接受T的方法。在这种情况下,您可以将接口标记为协变,编译器将验证接口是否真正安全可变,并允许所需的变异。


@EricLippet 非常感谢您的解释。我理解得很好,确实很有道理。 - Julián Urbano

7

由于 T 只出现在您的 IProvider<T> 接口的输出位置中,因此您可以使其在 T 上具有协变性:

public interface IProvider<out T> where T : IElement {
    IEnumerable<T> Provide();
}

这将使这个指令合法:
IProvider<IElement> provider = new MyProvider();

此功能需要 C# 4。详细信息请参见泛型中的协变和逆变


非常感谢,这解决了问题,但是当 IProvider<T> 本身扩展另一个接口时会发生什么?请参见上面的编辑。 - Julián Urbano
@caerolus,对于C#来说,IProvider<IElement>和IProvider<MyElement>是不同的类型。这个设计决策是有意为之的,以防止不当的向上转型。假设你有一个IProvider<MyElement>,然后将其向上转型为IProvider<IElement>并向下转型为IProvider<MyElement2> - 瞬间出现异常。 - Valera Kolupaev
抱歉,我不明白这与什么相关。 是的,它们是不同的类型,我无法将其向下转换为IProvider<MyElement2>,因为我猜MyElement2与MyElement没有关联。但为什么我应该将IProvider<MyElement>向上转换为IProvider<IElement>? - Julián Urbano
1
@caerolus,根据您的编辑:如果T仅出现在输出位置,则接口可以是协变的;如果T仅出现在输入位置,则接口可以是逆变的;但是如果T同时出现在输入和输出位置,则接口根本不能是变体的... - Thomas Levesque
@ThomasLevesque,所以没有简单的解决方案,对吗?我应该让MyProvider也实现IProvider<IElement>,还是在所有地方都使用强制转换?这可能需要被.NET 3.5用户使用,所以实现IProvider<IElement>会更好吗?谢谢! - Julián Urbano
@caerolus,没有简单的解决方案...你可以创建一个非泛型的IProvider接口,并在你的类中实现它。 - Thomas Levesque

2

如果你只使用对 IProvider<IElement> 的引用来访问输出位置中具有 T 的方法,那么你可以将接口分为两个(请为它们找到更好的名称,如逆变的 ISink<in T>):

public interface IProviderOut<out T> where T : IElement {
  IEnumerable<T> Provide();
}
public interface IProviderIn<in T> where T : IElement {
  void Add(T t);
}

你的类同时实现了以下两个功能:
public class MyProvider : IProviderOut<MyElement>, IProviderIn<MyElement> {
  public IEnumerable<MyElement> Provide() {
    ...
  }
  public void Add(MyElement t) {
    ...
  }
}

当你需要向上转型时,现在可以使用协变接口:

IProviderOut<IElement> provider = new MyProvider();

或者,您的接口可以同时继承两个接口:

public interface IProvider<T> : IProviderIn<T>, IProviderOut<T> 
  where T : IElement { 
  // you can add invariant methods here...
}

你的类实现它:

public class MyProvider : IProvider<MyElement> ...

非常感谢!我现在认为我完全理解了协变性和逆变性的整个概念。以前从未听说过,但是再想想,我从未实现过我的泛型。 - Julián Urbano

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