使用IQueryable.Count<T>和IEnumerable<T>参数

15

想象一个类,比如用于分页的类,可以与 IList<T>IQueryable<T> 一起使用。

该类将具有一个 int TotalItems 属性,它将(并不令人惊讶地)获取/设置可查询或可枚举参数的计数。

如果我将参数设置为 IEnumerable<T>

//simplified
public Pagination(IEnumerable<T> query)
    {
        TotalItems = query.Count();
    }

Count() 方法将会是 Enumerable.Count(), 这也就是说,即使查询(query)是一个继承自 IEnumerable<T>IQueryable<T> ,它仍将被枚举 (这显然不是希望看到的"数据库查询"行为)。

那么,当我的 IEnumerable<T> 实际上是一个 IQueryable<T> 时,是否有方法可以使用 Queryable.Count() 方法,或者我必须更改设计,在这种情况下例如拥有两个构造函数?

//simplified
public Pagination(IEnumerable<T> query)
    {
         TotalItems = query.Count();
    }
public Pagination(IQueryable<T> query)
    {
         TotalItems = query.Count();
    }

编辑 我确实明白 IQueryable<T> 继承自 IEnumerable<T>IEnumerable<T>IQueryable<T> 有相同名称的扩展方法并没有任何关系,而且拥有“看起来做同样事情”的相同名称扩展方法很好,但我仍然认为有时会令人困惑...

好奇的泛型问题

在框架中,是否还有其他具有相同“架构”的示例:继承 + 具有公共扩展方法的相同名称?


为什么不直接接受一个“int”,如果计数来自于序列/查询,就让它们计数呢? - Servy
@AakashM 嗯,这个问题是在真正尝试并查看MiniProfiler结果之后提出的:我可能做了错误的解释,如果是这样,我会撞墙的;) - Raphaël Althaus
@Servy 正如您所指出的,这是一个简化的情况。我可能想在我的类中使用“query”参数进行其他用途,并调用其他“共享”的可查询或可枚举扩展方法,这将产生相同的问题(如果问题存在)。 - Raphaël Althaus
嗨,如果我没记错的话...如果IEnumerable<T>是参数,而你传递了IQueryable<T>,那么query.Count()将调用使用IQueryable定义的Count方法(默认多态行为)? - Dinesh
2
@DnshPly9 这就是问题所在的地方,“Count()”不是“IQueryable<T>”或“IEnumerable<T>”的方法,它们是扩展方法...因此,这些扩展方法只是具有相同的名称,但与多态机制没有任何关系。 - Raphaël Althaus
2个回答

8

你应该提供两个构造函数重载,如你在问题中所示。这样调用者就可以自行选择使用IQueryable 还是 IEnumerable 方法。

如果有人执行以下操作:

Pagination pager = new Pagination(query.AsEnumerable());

然后,他们明确希望将对象处理为IEnumearble而不是IQueryable。也许他们知道查询提供程序没有实现SkipTake,所以分页将失败,并且需要评估为Linq-to-objects。
通过拥有这两个重载,您可以让用户在处理内存序列或查询时做出明智的决策,而不是自己尝试弄清楚。

+1,好的例子使用Skip/Take,可能不被查询提供程序支持。 - ken2k
另外加一点,这是一个很好的例子,展示了在不同提供程序上未实现扩展方法。很快就会被接受,只是有一个问题:你不觉得我可以用 IList<T> 参数替换带有 IEnumerable<T> 参数的 ctor 吗?因为无论如何 Count() 都会枚举,这样会让事情更清晰,不是吗? - Raphaël Althaus

1

嗯,你可以这样做:

TotalItems = enumerable.AsQueryable().Count();

这将直接使用查询提供程序的Count实现来处理本地可查询对象,否则会回退到LINQ to Objects(使用EnumerableQuery会有一些开销)。
另一个解决方案(可能更有效):
var queryable = enumerable as IQueryable<T>;
TotalItems = queryable != null ? queryable.Count() : enumerable.Count();

然而,需要注意的是,如果你的可查询对象(queryable)有效实现了ICollectionICollection<T>接口(一些查询提供程序实现了这个),那么即使使用现有的解决方案也有可能因为LINQ中的优化而效果良好(它会尽可能地使用这些接口中的Count属性)。对于IList<T>,由于它实现了ICollection<T> 接口,因此它总是会使用此优化。

3
但是这可能会引起问题。如果有人拥有一个IQueryable并在其上调用AsEnumerable,则意味着他们希望将其解析为LINQ-to-objects,而不是执行针对查询提供程序的后续查询。 - Servy
@Servy 嗯,如果使用了 AsEnumerable(),查询提供程序是否会再次执行呢?必须承认,我不是很清楚使用 IQueryable<T>().AsEnumerable().AsQueryable() 会发生什么... - Raphaël Althaus
1
@RaphaëlAlthaus 不,AsEnumerable不会枚举任何内容,它实际上只是将对象转换为IEnumerable。完整的定义是:public static IEnumerable<T> AsEnumerable<T>(this IEnumerable<T> source) {return source;} - Servy
@Servy:我不明白你的观点。OP 特别想要查询提供程序的Count()实现原生查询。 - Ani
@Ani 但归根结底,使用哪种方法应该由调用者决定,他只希望在明知传入了IQueryable的情况下使用Queryable方法,而不是在没有传入时使用。 - Servy

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