LINQ - 按相同功能过滤和排序的最有效方法

Posted

技术标签:

【中文标题】LINQ - 按相同功能过滤和排序的最有效方法【英文标题】:LINQ - most efficent way to filter and sort by same function 【发布时间】:2019-10-07 17:55:05 【问题描述】:

我正在寻找一种有效的方法来按函数对集合进行排序,同时根据对所述函数的评估过滤集合。举例说明:

var materialsByExpiry = from m in materials
                        where m.ExpiryDate() >= today
                        orderby m.ExpiryDate()
                        select m;

由于materials 是一个大型集合,而ExpiryDate 是一个重要的计算,我想尽量减少对ExpiryDate 的调用次数。显然ExpiryDate 只需要为每种材料调用一次,但这段代码调用它 n + q 次,其中 n 是总数元素的数量,q 是通过过滤器的数量。

我可以看到一种可能性是定义一个结构来存储材料及其到期日期并使用它。但是有没有更有效的方法(不用花时间制定我自己的排序/过滤算法)?

【问题讨论】:

如果您从materials 中选择m,为什么需要orderby?如果ExpiryDate只需要计算一次,是不是最好运行一个单独的循环,在linq之前计算和更新一个属性? 为什么不在 ExpiryDate 函数上挂一个惰性求值器(Lazy)?第一次计算,然后返回相同的值。 动态计算ExpiryDate 是有原因的吗?它可以在运行时改变吗? @MongZhu 这是系统的工作方式。 materials 来自我正在编写的代码之外,我需要调用其类的方法来提取我们排序/过滤的值。 【参考方案1】:

你可以使用let关键字来临时保存ExpiryDate()返回的值

var materialsByExpiry = from m in materials
                        let date = m.ExpiryDate() 
                        where date >= today
                        orderby date
                        select m;

这会将调用次数减少到Where 子句所需的次数。

这是 LINQPad 的示例:

void Main()
   
    DateTime today = DateTime.Now;  
    List<Material> materials = new List<UserQuery.Material>();

    materials.Add(new Material  Hours = 6 );
    materials.Add(new Material  Hours = 2 );
    materials.Add(new Material  Hours = -6 );
    materials.Add(new Material  Hours = -6 );
    materials.Add(new Material  Hours = 4 );

    // normal version
    var materialsByExpiry = (from m in materials
                            where m.ExpiryDate() >= today
                            orderby m.ExpiryDate()
                            select m).ToList();

    Material.count.Dump("COUNT normal");
    Material.count = 0;

    // LET version
    var materialsByExpiry_Let = (from m in materials
                            let date = m.ExpiryDate() 
                            where date >= today
                            orderby date
                            select m).ToList();

    Material.count.Dump("COUNT using LET");
    materialsByExpiry.Dump();
    materialsByExpiry_Let.Dump();


public class Material

    public static int count = 0;
    public int Hours  get; set; 
    public DateTime ExpiryDate()
    
        count++;
        return DateTime.Now.AddHours(Hours);
    

这是比较输出:

【讨论】:

谢谢! let 简单、直接,并且加快了速度。我很想知道它在脱糖形式中是什么样子(写成 LINQ 方法调用而不是花哨的 LINQ 语法)。我在 System.Linq 中没有看到 Let 方法。 @Stewart 不客气。我猜它在 lambda like this 中看起来并不那么花哨;)它看起来也会在整个集合中重复 1 次。【参考方案2】:

您可以创建一个通用扩展方法来调用计算密集型方法,并将输出与对象本身一起存储在 ValueTuple 中。

public static IEnumerable<(TSource Item, TProperty1 Property1)>
    Precompute<TSource, TProperty1>(this IEnumerable<TSource> source,
    Func<TSource, TProperty1> property1Selector)

    return source.Select(item => (item, property1Selector(item)));

那么你可以像这样使用它:

var materialsByExpiry = from m in materials.Precompute(m => m.ExpiryDate())
                        where m.Property1 >= today
                        orderby m.Property1
                        select m.Item;

虽然命名很尴尬。您不能以这种方式为元组的属性赋予有意义的名称。

【讨论】:

以上是关于LINQ - 按相同功能过滤和排序的最有效方法的主要内容,如果未能解决你的问题,请参考以下文章

Linq:按计算结果过滤并重用此结果

尝试使用linq过滤数据时,字符串未被识别为有效的DateTime

过滤器设计中的 Linq 铸造

排序和过滤核心数据关系的更有效方法是啥?

如何使用React过滤和排序相同的表数据?

过滤具有一对多父子映射的表的最有效方法