【问题标题】:Constrained generics with hierarchy in type parameter类型参数中具有层次结构的约束泛型
【发布时间】:2023-04-03 14:52:01
【问题描述】:

我对 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&lt;IElement&gt;

IProvider<IElement> provider = new MyProvider();

尽管MyProviderIProvider&lt;MyElement&gt;MyElementIElement,但我必须对IProvider&lt;IElement&gt; 进行强制转换。我可以通过使MyProvider 也实现IProvider&lt;MyElement&gt; 来避免强制转换,但为什么它不能解析类型参数中的层次结构?

编辑:根据 Thomas 的建议,我们可以使其在 T 中成为协变的。但是如果有像下面这样的其他方法有T类型的参数怎么办?

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

【问题讨论】:

    标签: c# generics types parameters hierarchy


    【解决方案1】:

    由于T只出现在你的IProvider&lt;T&gt;接口的输出位置,你可以在T中使其成为协变的:

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

    这将使该指令合法:

    IProvider<IElement> provider = new MyProvider();
    

    此功能需要 C# 4。阅读Covariance and Contravariance in Generics 了解更多详情。

    【讨论】:

    • 非常感谢,这解决了这个问题,但是当IProvider&lt;T&gt; 本身扩展另一个接口时会发生什么?请参阅上面的编辑。
    • @caerolus,对于 C# IProvider 和 IProvider 是不同的类型。此设计决策是有意做出的,以防止不当上铸。更简洁的是你有 IProvider,然后将它向上转换为 IProvider 并向下转换为 IProvider - 繁荣,异常。
    • 很抱歉,我看不出这有什么关系。是的,它们是不同的类型,我不能向下转换为 IProvider,因为我认为 MyElement2 与 MyElement 无关。但我为什么要从 IProvider 向上转换为 IProvider
    • @caerolus,根据您的编辑:如果T 仅出现在输出位置,则界面可以是协变的; T只出现在输入位置,界面可以逆变;但它T在输入和输出位置,界面根本不能变...
    • @ThomasLevesque 所以没有简单的解决方案,对吧?我应该让 MyProvider 也实现 IProvider 还是在任何地方使用演员表?这可能必须由 .net 3.5 用户使用,所以也许实现 IProvider 更好?谢谢!
    【解决方案2】:

    尽管MyProviderIProvider&lt;MyElement&gt;MyElementIElement,但我必须对IProvider&lt;IElement&gt; 进行强制转换。为什么不解析类型参数中的层次结构?

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

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

    现在您的问题是:“尽管MyListIList&lt;Giraffe&gt;GiraffeIAnimal,但我必须对IList&lt;IAnimal&gt; 进行强制转换。为什么这不起作用?”

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

    m.Add(new Tiger());
    

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

    这一定会失败,因为IList&lt;T&gt; 有一个接受 T 的 Add 方法。现在,也许您的接口没有接受 T 的方法。在这种情况下,您可以将接口标记为 协变 , 编译器将验证接口对于方差是否真正安全并允许您想要的方差。

    【讨论】:

    • @EricLippet 非常感谢您的解释。我理解正确,是的,很有意义。
    【解决方案3】:

    如果您使用对IProvider&lt;IElement&gt; 的引用来访问在输出位置具有T 的方法,您可以接口分成两部分(请查找更好的名字,比如ISink&lt;in T&gt; 用于逆变):

    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> ...
    

    【讨论】:

    • 非常感谢!我想我现在明白了整个协方差 - 逆变的事情。以前从未听说过,但话说回来,我从来没有实现我的泛型。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-14
    • 2013-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多