LINQ查询"内存"

Ian*_*Ian 10 .net c# linq

LINQ没有办法在查询 "记住"以前的查询结果?

考虑以下情况:

public class Foo {
    public int Id { get; set; }
    public ICollection<Bar> Bars { get; set; }
}

public class Bar {
    public int Id { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果两个或更多Foo具有相同的集合Bar(无论顺序是什么),它们被认为是相似的 Foo.

例:

foo1.Bars = new List<Bar>() { bar1, bar2 };
foo2.Bars = new List<Bar>() { bar2, bar1 };
foo3.Bars = new List<Bar>() { bar3, bar1, bar2 };
Run Code Online (Sandbox Code Playgroud)

在上述情况下,foo1相似foo2但两者foo1foo2 相似foo3

因为我们有一个query结果由IEnumerableIOrderedEnumerableFoo.从query,我们要找到第一N foo这是 相似.

此任务似乎需要bars以前选择的集合的记忆.

部分 LINQ我们可以这样做:

private bool areBarsSimilar(ICollection<Bar> bars1, ICollection<Bar> bars2) {
    return bars1.Count == bars2.Count && //have the same amount of bars
        !bars1.Select(x => x.Id)
        .Except(bars2.Select(y => y.Id))
        .Any(); //and when excepted does not return any element mean similar bar
}

public void somewhereWithQueryResult(){
    .
    .
    List<Foo> topNFoos = new List<Foo>(); //this serves as a memory for the previous query
    int N = 50; //can be any number
    foreach (var q in query) { //query is IOrderedEnumerable or IEnumerable
        if (topNFoos.Count == 0 || !topNFoos.Any(foo => areBarsSimilar(foo.Bars, q.Bars)))
            topNFoos.Add(q);
        if (topNFoos.Count >= N) //We have had enough Foo
            break;
    }
}
Run Code Online (Sandbox Code Playgroud)

topNFoos List将作为上一个查询的内存,我们可以跳过Foo qforeach其中已经有相同的环BarsAnyFootopNFoos.

我的问题是,有没有办法LINQ(完全 LINQ)?

var topNFoos = from q in query
               //put something
               select q;
Run Code Online (Sandbox Code Playgroud)

如果所需的"内存"来自特定查询项q或查询之外的变量,那么我们可以使用let变量来缓存它:

int index = 0;
var topNFoos = from q in query
               let qc = index++ + q.Id //depends on q or variable outside like index, then it is OK
               select q;
Run Code Online (Sandbox Code Playgroud)

但如果它必须来自先前查询查询本身,那么事情开始变得更加麻烦.

有没有办法做到这一点?


编辑:

(我目前正在为答案创建一个测试用例(github链接).还在弄清楚如何公平地测试所有答案)

(下面的大部分答案都是为了解决我的特定问题,并且本身就很好(Rob's,Spender's和David B的答案使用得IEqualityComparer特别棒).但是,如果有人能够回答我更普遍的问题" LINQ是否有办法"记住"其先前的查询结果,同时查询",我也很高兴)

(除了我在使用完全/部分LINQ时所呈现的特定情况的性能上的显着差异,一个答案旨在回答我关于LINQ内存的一般问题是Ivan Stoev's.另一个具有良好组合的是Rob's.为了让自己更清楚,我寻找一般和有效的解决方案,如果有,使用LINQ)

spe*_*der 6

我不打算直接回答你的问题,而是提出一种方法,它可以非常有效地过滤前N个非相似项目.

首先,考虑编写一个IEqualityComparer<Foo>使用Bars集合来衡量相等性的东西.在这里,我假设列表可能包含重复的条目,因此对相似性有相当严格的定义:

public class FooSimilarityComparer:IEqualityComparer<Foo>
{
    public bool Equals(Foo a, Foo b)
    {
        //called infrequently
        return a.Bars.OrderBy(bar => bar.Id).SequenceEqual(b.Bars.OrderBy(bar => bar.Id));
    }
    public int GetHashCode(Foo foo)
    {
        //called frequently
        unchecked
        {
            return foo.Bars.Sum(b => b.GetHashCode());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

N通过使用HashSet上面的IEqualityComparer,您可以真正高效地获取顶级非相似项:

IEnumerable<Foo> someFoos; //= some list of Foo
var hs = new HashSet<Foo>(new FooSimilarityComparer());
foreach(var f in someFoos)
{
    hs.Add(f); //hashsets don't add duplicates, as measured by the FooSimilarityComparer
    if(hs.Count >= 50)
    {
        break;
    }
}
Run Code Online (Sandbox Code Playgroud)

@Rob上面的方法大致相似,并展示了如何在LINQ中直接使用比较器,但要注意我对他的回答所做的评论.


Rob*_*Rob 3

所以,这是……可能的。但这远非高性能代码。

var res = query.Select(q => new {
    original = q, 
    matches = query.Where(innerQ => areBarsSimilar(q.Bars, innerQ.Bars))
}).Select(g => new { original = g, joinKey = string.Join(",", g.matches.Select(m => m.Id)) })
.GroupBy (g => g.joinKey)
.Select(g => g.First().original.original)
.Take(N);
Run Code Online (Sandbox Code Playgroud)

这假设Id每个 Foo 的 s 都是唯一的(GetHashCode()我想你也可以使用它们的 )。

更好的解决方案是保留您所做的事情,或者实现自定义比较器,如下所示:


注意:正如 @spender 的评论所指出的,下面的EqualsGetHashCode不适用于具有重复项的集合。请参阅他们的答案以获得更好的实现 - 但是,使用代码将保持不变


class MyComparer : IEqualityComparer<Foo>
{
    public bool Equals(Foo left, Foo right)
    {
        return left.Bars.Count() == right.Bars.Count() && //have the same amount of bars
            left.Bars.Select(x => x.Id)
            .Except(right.Bars.Select(y => y.Id))
            .ToList().Count == 0; //and when excepted returns 0, mean similar bar
    }

    public int GetHashCode(Foo foo)
    {
        unchecked {
            int hc = 0;
            if (foo.Bars != null)
                foreach (var p in foo.Bars)
                hc ^= p.GetHashCode();
            return hc;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后你的查询就变得简单了:

var res = query
    .GroupBy (q => q, new MyComparer())
    .Select(g => g.First())
    .Take(N);
Run Code Online (Sandbox Code Playgroud)