只能在 IEnumerable 上调用 Cast 和 OfType

Only able to call Cast and OfType on IEnumerable

在我使用的库中,我在调用 IEnumerable 以外的任何 LINQ 方法时遇到问题。我有一个 class 层次结构如下(命名有点奇怪,因为它是从内部代码混淆的)

Item : GeneralObject

ItemCollection : GenericCollection<ItemCollection, Item>

GenericCollection<TCollection, TItem> : GeneralObjectCollection, IEnumerable<TItem>
    where TCollection : GenericCollection<TCollection, TItem>
    where TItem : GeneralObject

GeneralObjectCollection : ICollection, IEnumerable<GeneralObject>

可以看出,IEnumerable在ItemCollection的class层级中出现了两次,所以有两个GetEnumerator方法,一个提供GeneralObject,一个提供GeneralObject一个 Item.

当我查看 VS2019 中通过元数据提供的 class 定义时,每个实现 IEnumerable<TItem> 的 class 也显示为已实现 IEnumerable

使用此设置,我无法在任何 ItemCollection 实例上调用大多数 LINQ 方法,如 Select、Where 等,只能在 IEnumerable 上调用这些方法。看起来它也应该清楚地支持 IEnumerable<T> 上的方法,但出于某种原因我必须先转换它。

转换为 IEnumerable<TItem> 和使用 .Cast<TItem> 都有效,但这些似乎是不必要的。

下面的代码示例。第二个例子无法编译:

private ItemCollection GetItemsFromDatabase(string query)
{
    // Internal logic.
}

List<Item> newItemList = ((IEnumerable<Item>)GetItemsFromDatabase(itemQuery))
                                        .Select(x => new ItemInfo(x.Name, x.Id, x.Guid)).ToList();

private ItemCollection GetItemsFromDatabase(string query)
{
    // Internal logic.
}

List<Item> newItemList = GetItemsFromDatabase(itemQuery).Select(x => new ItemInfo(x.Name, x.Id, x.Guid)).ToList();

错误是: 'ItemCollection' 不包含 'Select' 的定义,并且找不到可访问的扩展方法 'Select' 接受类型 'ItemCollection' 的第一个参数(您是否缺少 using 指令或程序集参考?)。

我在此文件的其他地方使用 LINQ 没有问题,所以这不是提供正确使用或实际程序集引用的问题。

对于其他有类似问题的人,问题是由扩展方法解析引起的。

当我用模板参数调用select时,例如:

List<Item> newItemList = GetItemsFromDatabase(itemQuery).Select<Item, ItemInfo>(x => new ItemInfo(x.Name, x.Id, x.Guid)).ToList();

代码正确编译。

似乎在解析方法调用时,在用尽实例方法后,仅使用 Select(...) 时也无法解析任何扩展方法。我的猜测是 Select 显然存在,但由于类型定义中的歧义(IEnumerable<GeneralObject>IEnumerable<Item> 以及推理引擎无法解析类型而无法解析致电 Select。因此,没有扩展方法与 ItemCollection 匹配,最终错误表明 select 未定义,而不是指定无法为扩展方法调用推断类型,需要指定类型。

正如您所猜想的那样,问题的出现是因为在类型推断过程中发现了歧义。当你说:

class Ark : IEnumerable<Turtle>, IEnumerable<Giraffe> 
{ ... }

然后你说

Ark ark = whatever;
ark.Select(x => whatever);

编译器必须以某种方式知道您的意思是 a.Select<Turtle, Result> 还是 a.Select<Giraffe, Result>。在任何情况下,C# 都不会尝试猜测您的意思是 a.Select<Animal, Result>a.Select<object, Result>,因为 这不是所提供的选择之一 。 C#只从可用的类型中进行选择,可用的类型有IEnumerable<Turtle>IEnumerable<Giraffe>

如果没有做出决定的依据,那么类型推断就会失败。由于我们仅在所有其他重载解析尝试失败后才使用扩展方法,因此此时我们可能会导致重载解析失败。

有很多方法可以实现这一点,但所有方法都涉及以某种方式向 C# 提示您的意思。最简单的方法是

ark.Select<Turtle, Result>(x => whatever);

不过你也可以

ark.Select((Turtle x) => whatever);

((IEnumerable<Turtle>)ark).Select(x => whatever);

(ark as IEnumerable<Turtle>).Select(x => whatever);

这些都很好。你推断这编译:

ark.Cast<Turtle>().Select(x => whatever); 
// NEVER DO THIS IN THIS SCENARIO
// USE ANY OF THE OTHER TECHNIQUES, NEVER THIS ONE

你知道它为什么危险吗?在您理解为什么这可能是错误的之前不要继续。讲道理。


一般来说,实现一个实现两个 "the same" 通用接口的类型是一种危险的做法,因为 可能会发生非常奇怪的事情 。语言和运行时并不是为了优雅地处理这种统一而设计的。例如,考虑协方差是如何工作的;如果我们将 ark 转换为 IEnumerable<Animal> 会发生什么?看看你能不能想出来;然后尝试一下,看看你是否正确。

不幸的是,你的处境更糟;如果你实例化 GenericCollection<TCollection, TItem> 使得 TItemGeneralObject 怎么办? 现在您已经实现了 IEnumerable<GeneralObject> 两次! 这确实让用户感到困惑,而且 CLR 根本不喜欢这样。

更好的做法是让 Ark 实现两个接口,而是公开两个方法,一个是 returns 海龟,一个是 returns 长颈鹿。你应该强烈考虑在你的 class 中做同样的事情。更好的设计是让 GenericCollection<TCollection, TItem> 不实现 IEnumerable<TItem> 而是实现 属性 IEnumerable<TItem> Items { get { ... } }

通过该设计,您可以 collection.Select 获取一般对象,或 collection.Items.Select 获取项目,问题就迎刃而解了。