在c#中返回集合时如何处理协方差?

时间:2011-09-11 16:49:31

标签: c# interface implementation covariance

我遇到了返回收集和协方差的问题,我想知道是否有人有更好的解决方案。

情景如下:

我有2个版本的实现,我希望将版本实现完全分开(即使它们可能具有相同的逻辑)。在实现中,我想返回一个项目列表,因此在界面中,我将返回该项目的接口列表。但是,在实际的接口实现中,我想返回该项的具体对象。在代码中,它看起来像这样。

interface IItem
{
    // some properties here
}

interface IResult
{
    IList<IItem> Items { get; }
}

然后,将有2个名称空间具有这些接口的具体实现。例如,

命名空间版本1

class Item : IItem

class Result : IResult
{
    public List<Item> Items
    {
        get { // get the list from somewhere }
    }

    IList<IItem> IResult.Items
    {
        get
        {
            // due to covariance, i have to convert it
            return this.Items.ToList<IItem>();
        }
    }
}

命名空间Version2下会有另一个相同的实现。

要创建这些对象,将有一个工厂接受版本并根据需要创建适当的具体类型。

如果调用者知道确切的版本并执行以下操作,则代码可以正常运行

Version1.Result result = new Version1.Result();
result.Items.Add(//something);

但是,我希望用户能够做到这样的事情。

IResult result = // create from factory
result.Items.Add(//something);

但是,因为它已被转换为另一个列表,所以add不会做任何事情,因为该项不会被添加回原始结果对象。

我可以想到一些解决方案,例如:

  1. 我可以同步两个列表,但这似乎是额外的工作
  2. 返回IEnumerable而不是IList并添加创建/删除集合的方法
  3. 创建一个采用TConcrete和TInterface的自定义集合
  4. 我理解为什么会发生这种情况(由于类型安全而且全部),但我认为没有一种解决方法看起来非常优雅。有没有人有更好的解决方案或建议?

    提前致谢!

    更新

    在考虑了这个之后,我想我可以做到以下几点:

    public interface ICustomCollection<TInterface> : ICollection<TInterface>
    {
    }
    
    public class CustomCollection<TConcrete, TInterface> : ICustomCollection<TInterface> where TConcrete : class, TInterface
    {
        public void Add(TConcrete item)
        {
            // do add
        }
    
        void ICustomCollection<TInterface>.Add(TInterface item)
        {
            // validate that item is TConcrete and add to the collection.
            // otherwise throw exception indicating that the add is not allowed due to incompatible type
        }
    
        // rest of the implementation
    }
    

    然后我可以

    interface IResult
    {
        ICustomCollection<IItem> Items { get; }
    }
    
    then for implementation, I will have
    
    class Result : IResult
    {
        public CustomCollection<Item, IItem> Items { get; }
    
        ICustomCollection<TItem> IResult.Items
        {
            get { return this.Items; }
        }
    }
    

    这样,如果调用者正在访问Result类,它将通过已经是TConcrete的CustomCollection.Add(TConcrete项)。如果调用者通过IResult接口访问,它将通过customCollection.Add(TInterface项)并进行验证并确保该类型实际上是TConcrete。

    我会试一试,看看这是否有用。

3 个答案:

答案 0 :(得分:2)

你面临的问题是因为想要公开一个类型(其中包括)“你可以向我添加任何IItem”,但它实际上会做的是“你只能添加{{ 1}}对我来说“。我认为最好的解决方案是实际公开Item。使用它的代码必须知道具体的IList<Item>,如果它应该将它们添加到列表中。

但如果你真的想这样做,我认为最干净的解决方案是3.,如果我理解正确的话。它将成为Item的包装器,它将实现IList<TConcrete>。如果你试图在其中放入一些不是IList<TInterface>的东西,它会抛出异常。

答案 1 :(得分:0)

+1给布伦特的评论:返回不可修改的集合并提供类的修改方法。

重申为什么你不能以可解释的方式工作:

如果您尝试添加到仅仅实现List<Item>但不属于类型(或派生自)的IItem元素,则您将无法在列表中存储此新项目。因此,接口的行为将非常不一致 - 实现IItem的一些元素可以正常添加,sime会失败,当您将实现更改为版本2时,行为也会发生变化。

简单的解决方法是存储IList<IItem> List<Item>的insitead,但直接暴露收集需要仔细思考。

答案 2 :(得分:0)

问题是Microsoft使用一个接口IList&lt; T&gt;来描述可以附加到的集合和不能附加的集合。虽然理论上一个类可以实现IList&lt; Cat&gt;以可变的方式并实现IList&lt; Animal&gt;以不可变的方式(前一个接口的ReadOnly属性将返回false,后者将返回true),类没有办法指定它实现IList&lt; Cat&gt;一种方式,IList&lt; T&gt;其中cat:T,另一种方式。我希望微软已经使IList&lt; T&gt;列表与LT; T&GT;实现IReadableByIndex&lt; out T&gt;,IWritableByIndex&lt; in T&gt;,IReadWriteByIndex&lt; T&gt;,IAppendable&lt; in T&gt;和ICountable,因为那些会允许协方差和逆变,但他们没有。自己实现这样的接口并为它们定义包装器可能会有所帮助,具体取决于协方差有多大的帮助。