LINQ with Querying" Memory"

时间:2016-03-10 02:06:55

标签: c# .net linq

LINQ是否有办法记住"它以前的查询结果在查询时

考虑以下情况:

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

public class Bar {
    public int Id { get; set; }
}

现在,如果两个或更多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 };

在上述情况下,foo1 类似foo2foo1foo2 类似foo3

鉴于我们的query结果包含IEnumerableIOrderedEnumerable Foo。从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;
    }
}

topNFoos List将作为上一个查询的记忆,我们可以跳过Foo q循环中foreach已经相同的Bars Any FootopNFoos的{​​{1}}。

我的问题是,有没有办法在LINQ完全 LINQ)中执行此操作?

var topNFoos = from q in query
               //put something
               select q;

如果&#34;记忆&#34; required来自特定查询项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;

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

有没有办法做到这一点?

修改

(我现在是creating a test case(github链接)的答案。还在弄清楚如何公平地测试所有答案)

(下面的大部分答案都是为了解决我的特定问题并且本身就很好(Rob&#39; s,花费&#39; s和David B&#39的回答使用{{1但是,如果有人能够回答我更普遍的问题&#34; LINQ是否有办法在查询时记住&#34;以前的查询结果&#34;我也很高兴)

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

5 个答案:

答案 0 :(得分: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());
        }
    }
}

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

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;
    }
}
上面的@Rob方法大致相似,并展示了如何在LINQ中直接使用比较器,但要注意我对他的回答所做的评论。

答案 1 :(得分: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);

这假设Id对每个Foo都是唯一的(我猜你也可以使用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;
        }
    }
}

然后您的查询变得简单:

var res = query
    .GroupBy (q => q, new MyComparer())
    .Select(g => g.First())
    .Take(N);

答案 2 :(得分:2)

import UIKit

class GroupsTableViewController: UITableViewController /* UITableViewDataSource, UITableViewDelegate */ {


@IBOutlet weak var table: UITableView!


var items=["Dog","Cat","Cow","Platypus"]

override func viewDidLoad() {
    super.viewDidLoad()

    self.table.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")
    self.table.dataSource = self
    self.table.delegate = self

    // Uncomment the following line to preserve selection between presentations
    // self.clearsSelectionOnViewWillAppear = false

    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem()
}

override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
}

// MARK: - Table view data source


override func numberOfSectionsInTableView(tableView: UITableView) ->         Int {
    // #warning Incomplete implementation, return the number of sections
    return 0
}

override func tableView(tableView: UITableView, numberOfRowsInSection  section: Int) -> Int {
    // #warning Incomplete implementation, return the number of rows
    return items.count
}



override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = self.table.dequeueReusableCellWithIdentifier("cell")! as UITableViewCell
    cell.textLabel!.text = self.items[indexPath.row]

    // Configure the cell...

    return cell
}

       override func tableView(tableView: UITableView, didDeselectRowAtIndexPath indexPath: NSIndexPath) {
    print("You tapped on cell # \(indexPath.row)")
}

有时,您可能不喜欢上述查询中groupby的行为。在枚举查询时,groupby将枚举整个源。如果您只想要部分枚举,那么您应该切换到Distinct和Comparer:

IEnumerable<Foo> dissimilarFoos =
  from foo in query
  let key = string.Join('|',
    from bar in foo.Bars
    order by bar.Id
    select bar.Id.ToString())
  group foo by key into g
  select g.First();

IEnumerable<Foo> firstDissimilarFoos =
  dissimilarFoos.Take(50);

然后写:

class FooComparer : IEqualityComparer<Foo>
{
  private string keyGen(Foo foo)
  {
    return string.Join('|',
      from bar in foo.Bars
      order by bar.Id
      select bar.Id.ToString());
  }
  public bool Equals(Foo left, Foo right)
  {
    if (left == null || right == null) return false;
    return keyGen(left) == keyGen(right);
  }
  public bool GetHashCode(Foo foo)
  {
    return keyGen(foo).GetHashCode();
  }
}

答案 3 :(得分:1)

观。您可以通过在“let x = ...”子句中捕获的缓存设计自己流畅的mutator界面来破解某些东西,

from q in query
let qc = ... // your cache mechanism here
select ...

但我怀疑你必须小心将缓存的更新限制为只允许那些“让...”,因为我怀疑标准Linq运算符和扩展方法的实现是否会很高兴如果允许的话通过在“where”,“join”,“group by”等条款中应用的谓词在他们的背后发生副作用。

“HTH,

答案 4 :(得分:1)

我想通过“完整的LINQ”你的意思是标准的LINQ运算符/ Enumerable扩展方法。

我认为这不能用LINQ查询语法完成。从标准方法来看,唯一支持可变处理状态的方法是Enumerable.Aggregate,但它只提供了比普通foreach更多的LINQ风格:

var result = query.Aggregate(new List<Foo>(), (list, next) =>
{
    if (list.Count < 50 && !list.Any(item => areBarsSimilar(item.Bars, next.Bars)))
        list.Add(next);
    return list;
});

由于看起来我们被允许使用辅助方法(如areBarsSimilar),我们所能做的最好是通过定义和使用自定义扩展方法使其至少看起来更像LINQ-ish

var result = query.Aggregate(new List<Foo>(), (list, next) => list.Count < 50 && 
    !list.Any(item => areBarsSimilar(item.Bars, next.Bars)) ? list.Concat(next) : list);

自定义方法

public static class Utils
{
    public static List<T> Concat<T>(this List<T> list, T item) { list.Add(item); return list; }
}

但请注意,与香草foreach相比,Aggregate还有一个缺点,就是无法提前退出,因此将消耗整个输入序列(除了性能之外还意味着它不会使用无限序列)。

结论:虽然这应该回答您的原始问题,即技术上可以做您要求的事情,但LINQ(与标准SQL一样)并不适合这种类型的处理。