只能在 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>
使得 TItem
是 GeneralObject
怎么办? 现在您已经实现了 IEnumerable<GeneralObject>
两次! 这确实让用户感到困惑,而且 CLR 根本不喜欢这样。
更好的做法是让 Ark
实现两个接口,而是公开两个方法,一个是 returns 海龟,一个是 returns 长颈鹿。你应该强烈考虑在你的 class 中做同样的事情。更好的设计是让 GenericCollection<TCollection, TItem>
不实现 IEnumerable<TItem>
而是实现 属性 IEnumerable<TItem> Items { get { ... } }
。
通过该设计,您可以 collection.Select
获取一般对象,或 collection.Items.Select
获取项目,问题就迎刃而解了。
在我使用的库中,我在调用 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>
使得 TItem
是 GeneralObject
怎么办? 现在您已经实现了 IEnumerable<GeneralObject>
两次! 这确实让用户感到困惑,而且 CLR 根本不喜欢这样。
更好的做法是让 Ark
实现两个接口,而是公开两个方法,一个是 returns 海龟,一个是 returns 长颈鹿。你应该强烈考虑在你的 class 中做同样的事情。更好的设计是让 GenericCollection<TCollection, TItem>
不实现 IEnumerable<TItem>
而是实现 属性 IEnumerable<TItem> Items { get { ... } }
。
通过该设计,您可以 collection.Select
获取一般对象,或 collection.Items.Select
获取项目,问题就迎刃而解了。