C#中返回集合时如何处理协方差?
我在返回集合和协方差方面遇到问题,我想知道是否有人有更好的解决方案。
场景是这样的:
我有 2 个版本的实现,我想保持版本实现完全独立(即使它们可能具有相同的逻辑)。在实现中,我想返回一个项目列表,因此在界面中,我将返回项目的界面列表。但是,在接口的实际实现中,我想返回该项目的具体对象。在代码中,它看起来像这样。
interface IItem
{
// some properties here
}
interface IResult
{
IList<IItem> Items { get; }
}
然后,将有 2 个命名空间,它们具有这些接口的具体实现。例如,
命名空间 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);
但是,由于它已转换为另一个列表,因此添加不会执行任何操作,因为该项目不会被添加回原始结果对象。
我可以想到一些解决方案,例如:
- 我可以同步两个列表,但这似乎是额外的工作
- 返回 IEnumerable 而不是 IList 并添加创建/删除集合的方法
- 创建一个采用 TConcrete 和 TInterface
I 的 自定义集合理解为什么会发生这种情况(由于类型安全),但我认为没有一个解决方法看起来非常优雅。有人有更好的解决方案或建议吗?
提前致谢!
更新
经过更多思考后,我想我可以执行以下操作:
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。
我会尝试一下,看看这是否有效。
I have a problem with returning collection and covariance and I was wondering if anyone has a better solution.
The scenario is this:
I have 2 version of implementation and I would like to keep the version implementation completely separate (even though they might have the same logic). In the implementation, I would like to return a list of items and therefore in the interface, I would return a list of interface of the item. However, in the actual implementation of the interface, I would like to return the concrete object of the item. In code, it looks something like this.
interface IItem
{
// some properties here
}
interface IResult
{
IList<IItem> Items { get; }
}
Then, there would be 2 namespaces which has concrete implementation of these interfaces. For example,
Namespace 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>();
}
}
}
There will be another implementation of the same thing under namespace Version2.
To create these objects, there will be a factory that takes the version and create the appropriate concrete type as needed.
If the caller knows the exact version and does the following, the code works fine
Version1.Result result = new Version1.Result();
result.Items.Add(//something);
However, I would like the user to be able to do something like this.
IResult result = // create from factory
result.Items.Add(//something);
But, because it's been converted to another list, the add will not do anything because the item will not be added back to the original result object.
I can think of a few solutions such as:
- I could synchronize the two lists but that seems to be extra work to do
- Return IEnumerable instead of IList and add a method for create/delete collections
- Create a custom collection that takes the TConcrete and TInterface
I understand why this is happening (due to the type safe and all), but none of workaround I think can of seems very elegant. Does anybody have better solutions or suggestions?
Thanks in advance!
Update
After thinking about this more, I think I can do the following:
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
}
then I can have
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; }
}
}
that way, if the caller is accessing the Result class, it will go through the CustomCollection.Add(TConcrete item) which is already TConcrete. If the caller is accessing through IResult interface, it will go through customCollection.Add(TInterface item) and the validation will happen and make sure the type is actually TConcrete.
I will give it a try and see if this would work.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
您面临的问题是因为想要公开一个类型(除其他外)“您可以向我添加任何
IItem
”,但它实际上要做的是“您只能添加>物品给我”。我认为最好的解决方案是实际公开
IList
。无论如何,使用它的代码必须知道具体的Item
是否应该将它们添加到列表中。但如果你真的想这样做,我认为最干净的解决方案是 3.,如果我理解正确的话。它将成为
IList
的包装器,并将实现IList
。如果您尝试将非TConcrete
的内容放入其中,它将引发异常。The problem you are facing is because want to expose a type that says (among other things) “you can add any
IItem
to me”, but what it actually will do is “You can add onlyItem
s to me”. I think the best solution would be to actually exposeIList<Item>
. The code that uses this will have to know about the concreteItem
anyway, if it should add them to the list.But if you really want to do this, I think the cleanest solution would be 3., if I understand you correctly. It's going to be a wrapper around
IList<TConcrete>
and it will implementIList<TInterface>
. If you try to put something into it that is notTConcrete
, it will throw an exception.对布伦特的评论+1:返回不可修改的集合并提供类的修改方法。
只是重申为什么你不能让 1 以可解释的方式工作:
如果你尝试添加到
List
元素,该元素仅实现IItem
但不是类型 (或派生自)项目,您将无法将此新项目存储在列表中。由于结果接口的行为将非常不一致 - 一些实现 IItem 的元素可以很好地添加,但 sime 会失败,并且当您将实现更改为 version2 时,行为也会改变。简单的修复方法是将
IList
存储在List
中,但直接公开集合需要仔细考虑。+1 to Brent's comment: return non-modifyable collection and provide modification methods on the classes.
Just to reiterate why you can't get 1 working in explainable way:
If you try to add to
List<Item>
element that simply implementsIItem
but is not of type (or derived from) Item you will not be able to store this new item in the list. As result interface's behavior will be very inconsistent - some elements that implement IItem can be added fine, sime would fail, and when you change implementation to version2 behavior will cahnge too.Simple fix would be to store
IList<IItem>
insitead ofList<Item>
, but exposing colection directly requires careful thinking.问题在于 Microsoft 使用一个接口 IList来描述可以附加的集合和不能附加的集合。虽然理论上一个类可以实现 IList;以可变的方式并实现 IList以不可变的方式(前一个接口的 ReadOnly 属性将返回 false,而后者将返回 true),类无法指定它实现 IList一种方式,并且 IList其中 cat:T,另一种方式。我希望 Microsoft 能够将 IList制作为 IList。列表实现 IReadableByIndex、IWritableByIndex、IReadWriteByIndex、IAppendable和 ICountable,因为这些允许协变和逆变,但他们没有。自己实现此类接口并为它们定义包装器可能会有所帮助,具体取决于协方差的帮助程度。
The problem is that Microsoft uses one interface, IList<T>, to both describe both collections that can be appended to, and collections that cannot. While it would be theoretically possible for a class to implement IList<Cat> in mutable fashion and also implement IList<Animal> in immutable fashion (the former interface's ReadOnly property would return false, and the latter would return true), there's no way for a class to specify that it implements IList<Cat> one way, and IList<T> where cat:T, another way. I wish Microsoft had made IList<T> List<T> implement IReadableByIndex<out T>, IWritableByIndex<in T>, IReadWriteByIndex<T>, IAppendable<in T>, and ICountable, since those would have allowed for covariance and contravariance, but they didn't. It may be helpful to implement such interfaces yourself and define wrappers for them, depending upon the extent to which covariance would be helpful.