组内的LINQ到对象索引+用于不同的分组(也称为ROW_NUMBER与PARTITION BY等效)

经过大量的Google搜索和代码实验,我对一个复杂的C#LINQ-to-objects问题感到难过,在SQL中使用一对ROW_NUMBER()… PARTITION BY函数和一个或两个子查询很容易解决这个问题。

用语言来说,这就是我在代码中尝试做的事情 – 基本要求是从列表中删除重复的文档:

  1. 首先,按(Document.Title,Document.SourceId)对列表进行分组,假设(简化)类定义如下:
     class Document
     {
        字符串标题;
         int SourceId;  //来源优先(ID = 1比ID = 2好)
     } 
  2. 在该组中,为每个文档分配一个索引(例如,索引0 = =第一个文档,此标题来自此来源,索引1 =第二个文档,此标题来自此来源,等等)。 我喜欢SQL中相当于ROW_NUMBER()的东西!
  3. 现在分组(Document.Title,Index),其中Index在步骤#2中计算。 对于每个组,只返回一个文档:Document.SourceId最低的文档。

步骤#1很简单(例如codepronet.blogspot.com/2009/01/group-by-in-linq.html),但我对步骤#2和#3感到困惑。 我似乎无法构建一个红色无波形的C#LINQ查询来解决所有这三个步骤。

Anders Heilsberg关于这个post的post是我认为如果我能正确理解语法,那么上面的步骤#2和#3的答案。

我倾向于避免使用外部局部变量来执行索引计算,正如slodge.blogspot.com/2009/01/adding-row-number-using-linq-to-objects.html所建议的那样,因为该解决方案中断了如果外部变量被修改。

最佳地,可以首先完成逐个标题步骤,因此“内部”分组(首先由Source计算索引,然后通过索引来过滤掉重复项)可以对每个“按标题”中的少量对象进行操作group,因为每个by-title组中的文档数量通常都在100以下。我真的不想要N 2解决方案!

我当然可以用嵌套的foreach循环解决这个问题,但看起来像LINQ这个问题应该很简单。

有任何想法吗?

我认为jpbochi错过了你希望你的分组是成对的值(Title + SourceId然后是Title + Index)。 这是一个LINQ查询(主要)解决方案:

var selectedFew = from doc in docs group doc by new { doc.Title, doc.SourceId } into g from docIndex in g.Select((d, i) => new { Doc = d, Index = i }) group docIndex by new { docIndex.Doc.Title, docIndex.Index } into g select g.Aggregate((a,b) => (a.Doc.SourceId <= b.Doc.SourceId) ? a : b); 

首先我们按Title + SourceId分组(我使用匿名类型,因为编译器为分组查找构建了一个好的哈希码)。 然后我们使用Select将分组的索引附加到文档,我们在第二个分组中使用它。 最后,对于每个组,我们选择最低的SourceId。

鉴于此输入:

 var docs = new[] { new { Title = "ABC", SourceId = 0 }, new { Title = "ABC", SourceId = 4 }, new { Title = "ABC", SourceId = 2 }, new { Title = "123", SourceId = 7 }, new { Title = "123", SourceId = 7 }, new { Title = "123", SourceId = 7 }, new { Title = "123", SourceId = 5 }, new { Title = "123", SourceId = 5 }, }; 

我得到这个输出:

 { Doc = { Title = ABC, SourceId = 0 }, Index = 0 } { Doc = { Title = 123, SourceId = 5 }, Index = 0 } { Doc = { Title = 123, SourceId = 5 }, Index = 1 } { Doc = { Title = 123, SourceId = 7 }, Index = 2 } 

更新:我刚看到你关于按标题分组的问题。 您可以使用标题组上的子查询来执行此操作:

 var selectedFew = from doc in docs group doc by doc.Title into titleGroup from docWithIndex in ( from doc in titleGroup group doc by doc.SourceId into idGroup from docIndex in idGroup.Select((d, i) => new { Doc = d, Index = i }) group docIndex by docIndex.Index into indexGroup select indexGroup.Aggregate((a,b) => (a.Doc.SourceId <= b.Doc.SourceId) ? a : b) ) select docWithIndex; 

说实话,我对你的问题很困惑。 也许你应该解释你想要解决的问题。 无论如何,我会尽力回答我的理解。

1)首先,我假设您已经有一个按Title + SourceId分组的文档列表。 出于测试目的,我将列表硬编码如下:

 var docs = new [] { new { Title = "ABC", SourceId = 0 }, new { Title = "ABC", SourceId = 4 }, new { Title = "ABC", SourceId = 2 }, new { Title = "123", SourceId = 7 }, new { Title = "123", SourceId = 5 }, }; 

2)要在每个项目中放置索引,可以使用Select扩展方法,传递Func选择器函数。 像这样:

 var docsWithIndex = docs .Select( (d, i) => new { Doc = d, Index = i } ); 

3)根据我的理解,下一步是按Title对最后的结果进行分组。 这是怎么做的:

 var docsGroupedByTitle = docsWithIndex .GroupBy( a => a.Doc.Title ); 

GroupBy函数(在上面使用)返回IEnumerable> 。 由于一个组也是可枚举的,我们现在有一个可枚举的枚举。

4)现在,对于上面的每个组,我们将只获得具有最小SourceId的项目。 要进行此操作,我们需要2级递归。 在LINQ中,外层是一个选择(对于每个组,获取其中一个项目),内层是一个聚合(获取具有最低SourceId的项目):

 var selectedFew = docsGroupedByTitle .Select( g => g.Aggregate( (a, b) => (a.Doc.SourceId <= b.Doc.SourceId) ? a : b ) ); 

为了确保它有效,我用一个简单的foreach测试它:

 foreach (var a in selectedFew) Console.WriteLine(a); //The result will be: //{ Doc = { Title = ABC, SourceId = 0 }, Index = 0 } //{ Doc = { Title = 123, SourceId = 5 }, Index = 4 } 

我不确定那是你想要的。 如果没有,请评论答案,我可以解决问题。 我希望这有帮助。

Obs。:我测试中使用的所有类都是匿名的 。 因此,您实际上不需要定义DocumentWithIndex类型。 实际上,我甚至没有宣布一个Document类。

基于方法的语法:

 var selectedFew = docs.GroupBy(doc => new {doc.Title, doc.SourceId}, doc => doc) .SelectMany((grouping) => grouping.Select((doc, index) => new {doc, index})) .GroupBy(anon => new {anon.doc.Title, anon.index}) .Select(grouping => grouping.Aggregate((a, b) => a.doc.SourceId <= b.doc.SourceId ? a : b)); 

你会说上面是等效的基于方法的语法吗?

我实现了一个扩展方法。 它支持按字段分区的多个分区以及多个顺序条件。

 public static IEnumerable Partition( this IEnumerable source, Func keySelector, Func, IOrderedEnumerable> sorter, Func selector) { AssertUtilities.ArgumentNotNull(source, "source"); return source .GroupBy(keySelector) .Select(arg => sorter(arg).Select(selector)) .SelectMany(arg => arg); } 

用法:

 var documents = new[] { new { Title = "Title1", SourceId = 1 }, new { Title = "Title1", SourceId = 2 }, new { Title = "Title2", SourceId = 15 }, new { Title = "Title2", SourceId = 14 }, new { Title = "Title3", SourceId = 100 } }; var result = documents .Partition( arg => arg.Title, // partition by arg => arg.OrderBy(x => x.SourceId), // order by (arg, rowNumber) => new { RowNumber = rowNumber, Document = arg }) // select .Where(arg => arg.RowNumber == 0) .Select(arg => arg.Document) .ToList(); 

结果:

 { Title = "Title1", SourceId = 1 }, { Title = "Title2", SourceId = 14 }, { Title = "Title3", SourceId = 100 }