如何在C#中返回集合时处理协变性?

9
我有一个问题需要解决,涉及到集合的返回与协变问题。我想知道是否有更好的解决方案。
情景如下:
我有两种实现版本,并希望将这两个版本完全分离(尽管它们可能具有相同的逻辑)。在实现中,我想返回一组项目并且在接口中,我会返回项目接口的列表。然而,在接口的实际实现中,我想返回项目的具体对象。代码示例如下。
interface IItem
{
    // some properties here
}

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

然后,将会有两个命名空间来实现这些接口。例如:
命名空间 Version1
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);

但是,由于它已被转换为另一个列表,因此添加不会做任何事情,因为该项不会被添加回原始结果对象。
我可以想到一些解决方案,例如:
1.我可以同步这两个列表,但这似乎是额外的工作。
2.返回IEnumerable而不是IList,并添加一个用于创建/删除集合的方法。
3.创建一个自定义集合,它采用TConcrete和TInterface。
我理解为什么会发生这种情况(由于类型安全等),但我认为我所想到的解决方法都不太优雅。有人有更好的解决方案或建议吗?
提前感谢!
更新
经过更多思考,我认为我可以这样做:
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类,它将通过CustomCollection.Add(TConcrete item)进行处理,该项已经是TConcrete类型。如果调用者通过IResult接口进行访问,则会通过customCollection.Add(TInterface item)进行处理,并进行验证以确保类型实际上是TConcrete。

我会尝试并查看是否有效。


1
我认为选择方案#2需要的代码最少。它还将减少界面的表面积,因为实现不需要负责完整的IList支持,而调用者可能也不需要。 - Brent M. Spell
你的解决方案看起来不错,但为什么不直接使用ICollection<T>而是使用ICustomCollection<T>呢? - svick
我完全可以。唯一的原因是我有一些专门针对集合的方法,这些方法对于常规的ICollection<T>不可用,需要通过内部代码访问,这一点我忘记提到了。 - Khronos
3个回答

2
你面临的问题是想公开一个类型,其中包括“你可以向我添加任何IItem”,但它实际上将执行的操作是“你只能向我添加Item”。我认为最好的解决方案是实际公开IList<Item>。使用此代码的代码将必须了解具体的Item,如果应将其添加到列表中。
但是,如果你真的想这样做,如果我理解正确,我认为最干净的解决方案是第3个。它将是IList<TConcrete>的包装器,并将实现IList<TInterface>。如果您尝试将不是TConcrete的内容放入其中,它将引发异常。

0
+1给Brent的评论:返回不可修改的集合并在类上提供修改方法。
只是为了重申为什么你不能以可解释的方式让1工作:
如果您尝试向List 添加元素,该元素仅实现IItem但不属于(或派生自)Item类型,则无法将此新项存储在列表中。因此,接口的行为将非常不一致-一些实现IItem的元素可以添加,而一些则会失败,并且当您将实现更改为version2时,行为也会发生变化。
简单的修复方法是存储IList ,而不是List ,但直接公开集合需要仔细考虑。

0
问题在于Microsoft使用一个接口IList<T>来描述既可以追加的集合,也可以不追加的集合。虽然理论上一个类可以以可变方式实现IList<Cat>,并以不可变方式实现IList<Animal>(前者接口的ReadOnly属性将返回false,后者将返回true),但是没有办法让一个类指定它以一种方式实现IList<Cat>,以cat:T,以另一种方式实现IList<T>。我希望Microsoft将IList<T> List<T>实现IReadableByIndex<out T>、IWritableByIndex<in T>、IReadWriteByIndex<T>、IAppendable<in T>和ICountable,因为这些将允许协变和逆变,但他们没有这样做。根据协变的程度,实现这样的接口并为它们定义包装器可能会有所帮助。

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