【问题标题】:C#: Overload LINQ generic methods with different type parametersC#:重载具有不同类型参数的 LINQ 泛型方法
【发布时间】:2012-10-30 01:40:17
【问题描述】:

我正在编写一组 LINQ 2 SQL 扩展方法。现在,我有一堆重复的方法,一个在 IEnumerable 集合上工作,另一个在 IQueryable 集合上工作。例如

public static IQueryable<X> LimitVisible(this IQueryable<X> items) {
    return items.Where(x => !x.Hidden && !x.Parent.Hidden);
}

public static IEnumerable<X> LimitVisible(this IEnumerable<X> items) {
    return items.Where(x => !x.Hidden && !x.Parent.Hidden);
}

我认为我需要这种重复,因为我希望能够在某些情况下保留 IQueryable 行为,以便将执行推迟到数据库。 这是一个正确的假设,还是单个 IEnumerable 方法(加上将输出转换为 IQueryable)有效?如果我确实需要两者,我不喜欢重复,所以我想将两者结合使用泛型方法

public static T LimitVisible<T>(this T items) where T : IEnumerable<X> {
    return (T)items.Where(x => !x.Hidden && !x.Parent.Hidden);
}

除了我还想为其编写 LimitVisible 函数的 X 以外的其他类型(比如 Y 和 Z)之外,这是可行的。但我不会写

public static T LimitVisible<T>(this T items) where T : IEnumerable<Y> {
    return (T)items.Where(y => !y.Hidden && !y.Parent.Hidden);
}

因为你可以基于泛型重载。我可以将这些方法放在不同的类中,但这似乎不是正确的解决方案。

有什么建议吗?也许我做出了不正确的假设,并且首先不需要 IQueryable 特定版本。

编辑:另一个选项

这是我过去用来避免重复的另一种模式

private static readonly Expression<Func<X, bool>> F = x => !x.Hidden && !x.Parent.Hidden;

public static IEnumerable<X> LimitVisible(this IEnumerable<X> e) {
    return e.Select(W.Compile());
}

public static IQueryable<X> LimitVisible(this IQueryable<X> q) {
    return q.Select(W);
}

这样做有什么危险吗?

【问题讨论】:

  • X、Y、Z 是否有用于HiddenParent 属性的通用接口或基类?
  • 不,它们是数据库中的不同表,恰好都有隐藏字段。

标签: c# linq generics


【解决方案1】:

真的没有什么好办法解决两种方法。 IQueryable WhereExpression&lt;Func&lt;T,bool&gt;&gt; 作为参数,而IEnumerableFunc&lt;T,bool&gt; 作为参数。由于 lambdas 的魔力,调用者的代码看起来相同,但编译器实际上对这两个不同的代码位做了截然不同的事情。

【讨论】:

  • 对不起,这是一个错字,我刚刚修正了。
  • @JordanCrittenden 删除了我回答的那一部分,但其余部分仍然相关。
【解决方案2】:

答案取决于您的使用情况:如果您不介意将数据集放入内存以执行查询,那么只保留 IEnumerable&lt;T&gt; 覆盖即可。

但是,此决定的必然结果是,在您使用 LimitVisible&lt;T&gt; 的那一刻,您的查询将被强制存入内存。这可能是也可能不是您想要的,并且可能会迫使您进入您宁愿避免的编码模式。例如,

var firstHundred = ctx
    .SalesOrders
    .LimitVisible()                     // Happens in memory
    .Where(ord => ord.UserId == myUser) // Also happens in memory
    .Take(100);

使用单个 IEnumerable&lt;T&gt; 会比使用 IQueryable&lt;T&gt; 覆盖可用性差很多,因为在确定其可见性之前,所有销售订单将被一一检索到内存中,而不是而不是检查服务器端的条件。如果按用户 ID 过滤可以消除服务器端的大部分销售订单,那么这个等效查询的性能差异可能会好几个数量级:

var firstHundred = ctx
    .SalesOrders
    .Where(ord => ord.UserId == myUser) // Happens in RDBMS
    .LimitVisible()                     // Happens in memory
    .Take(100);

如果您打算只自己使用代码,并且不介意在没有明显原因的情况下出现性能不佳的情况,您可以保留一个过载。如果您打算让其他人使用您的库,我强烈建议保留这两个覆盖。

编辑:为了加快您的替代实现,预编译您的表达式,如下所示:

private static readonly Expression<Func<X, bool>> F = x => !x.Hidden && !x.Parent.Hidden;
private static readonly Predicate<X> CompiledF = (Predicate<X>)F.Compile();

public static IEnumerable<X> LimitVisible(this IEnumerable<X> e) {
    return e.Select(CompiledF);
}

public static IQueryable<X> LimitVisible(this IQueryable<X> q) {
    return q.Select(F);
}

【讨论】:

  • 你证实了我的怀疑。我绝对不想把一切都带入记忆。使用两种方法的问题在于很容易意外更新其中一种方法的代码,但不能同时更新两种方法。
  • @JordanCrittenden 不幸的是,这可能是最简单的方法,无需编写大量复杂的代码来重用IQueryable&lt;T&gt; 实现中的 LINQ 表达式。
  • 是的,我刚刚编辑以显示替代方案。由于编译调用,我认为替代方法效率低下
  • @JordanCrittenden 不过,您可以通过创建已编译 lambda 的静态只读版本(查看编辑)轻松修复它。
  • @ErenErsönmez 如果您使用IEnumerator 方法,它不会快速加载它们,但关键是它最终(即迭代时)会加载所有 source 项目进入内存,然后过滤它们,并输出结果。如果您使用IQueryable 输入,那么过滤器的输出 就是需要带入内存的内容。关键是这个Where 过滤掉的所有项目都不会被带入内存(永远)。这也意味着后续调用发生在数据库中,而不是内存中,它们可以执行各种复杂的任务(即分组、连接等)或进行更多过滤。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多