使用LINQ保持顺序

415

我在一个已排序的数组上使用LINQ to Objects指令。我应该避免哪些操作,以确保数组的顺序不会改变?

7个回答

745

我检查了System.Linq.Enumerable的方法,剔除了返回非IEnumerable结果的任何方法。我检查了每个方法的备注,以确定其结果顺序与源顺序的不同之处。

完全保留顺序。您可以通过索引将源元素映射到结果元素

  • AsEnumerable
  • Cast
  • Concat
  • Select
  • ToArray
  • ToList

保留顺序。元素被过滤或添加,但不重新排序。

  • Distinct
  • Except
  • Intersect
  • OfType
  • Prepend(在.net 4.7.1中新增)
  • Skip
  • SkipWhile
  • Take
  • TakeWhile
  • Where
  • Zip(在.net 4中新增)

破坏顺序 - 我们不知道期望的结果顺序。

  • ToDictionary
  • ToLookup

明确重新定义顺序 - 使用这些来更改结果的顺序

  • OrderBy
  • OrderByDescending
  • Reverse
  • ThenBy
  • ThenByDescending

根据某些规则重新定义顺序。

  • GroupBy - IGrouping对象按照源中产生第一个键的顺序呈现。组中的元素按它们在源中出现的顺序呈现。
  • GroupJoin - GroupJoin保留outer元素的顺序,并为每个outer元素保留inner匹配元素的顺序。
  • Join - 保留outer元素的顺序,并为每个outer元素保留inner匹配元素的顺序。
  • SelectMany - 对于source的每个元素,都会调用selector并返回一系列值。
  • Union - 当枚举此方法返回的对象时,Union首先枚举first和second,然后按顺序产生尚未产生的每个元素。

编辑:根据实现,我将Distinct移到了保留顺序。

    private static IEnumerable<TSource> DistinctIterator<TSource>
      (IEnumerable<TSource> source, IEqualityComparer<TSource> comparer)
    {
        Set<TSource> set = new Set<TSource>(comparer);
        foreach (TSource element in source)
            if (set.Add(element)) yield return element;
    }

3
实际上,我认为 Distinct 会保留原始(第一次出现的)顺序 - 所以 {1,2,1,3,1,3,4,1,5} 会变成 {1,2,3,4,5}。 - Marc Gravell
10
Distinct<(Of <(TSource>)>)(IEnumerable<(Of <(TSource>)>)>) 方法返回一个不包含重复值的无序序列。请注意,该方法不会更改原始集合。 - Amy B
13
Marc说:你所说的可能是真的,但依靠那种行为是不明智的。 - Amy B
5
@Amy B 是的,但它不适用于Linq to Objects。在Linq to Sql中,distinct()会将distinct关键字放入生成的sql中,并不能保证从sql中进行排序。我很想看到一个针对Linq to Objects的distinct实现,它不保留顺序并且比保留顺序的实现更加高效。例如,您可以消耗整个输入并将其放入hashset中,然后通过枚举hashset来产生值(失去顺序),但这样做更糟。所以,是的,我不介意偶尔违反文档 :) - dan
4
文档(针对“Distinct”方法)可能只是想说“未排序”,而不是“以不可预测的顺序”。我认为,“Distinct”与“Where”一样属于上面提到的筛选类别。 - Jeppe Stig Nielsen
显示剩余32条评论

40

你在谈论SQL还是数组?换句话说,你正在使用LINQ to SQL还是LINQ to Objects?

LINQ to Objects操作符并不会实际改变它们的原始数据源 - 它们构建的序列实际上是由数据源支持的。唯一更改顺序的操作是OrderBy/OrderByDescending/ThenBy/ThenByDescending - 即使是这些操作,对于相同排序的元素来说也是稳定的。当然,许多操作会过滤掉一些元素,但返回的元素将保持相同的顺序。

如果转换为不同的数据结构,例如通过ToLookup或ToDictionary,我不认为此时会保留顺序 - 但这也有些不同。 (我相信查找的值映射到相同的键的顺序被保留了。)


因为OrderBy是一种稳定的排序算法,所以seq.OrderBy(_ => .Key)将会按照与seq.GroupBy( => .Key).SelectMany( => _)完全相同的顺序排列元素。这正确吗? - dmg
1
@dmg:不会的。只有GroupBy后跟SelectMany才能按键分组,但不是按升序键顺序...它们将按键最初出现的顺序排列。 - Jon Skeet
@JonSkeet 如果我使用 OrderBy,它是否保证具有相同键的“n”个对象将保留其原始序列,除了它们都在一起。例如:在 list<x> {a b c d e f g} 中,如果 c、d 和 e 都具有相同的键,则结果序列将包含 c、d 和 e 按顺序紧挨在一起。我似乎找不到一个明确的基于 MS 的答案。 - Paulustrious
1
@Paulustrious:在LINQ to Objects中是可以的。在其他提供程序中,这取决于具体实现。 - Jon Skeet
@Paulustrious:来自https://msdn.microsoft.com/en-us/library/bb534966(v=vs.110).aspx的内容:“此方法执行稳定排序;也就是说,如果两个元素的键相等,则元素的顺序被保留。相比之下,不稳定的排序不会保留具有相同键的元素的顺序。” - Jon Skeet
显示剩余5条评论

8
如果你正在处理一个数组,那么你使用的应该是LINQ-to-Objects,而不是SQL;你能确认一下吗?大多数LINQ操作不会重新排序任何内容(输出将与输入相同),因此不要再应用其他排序(OrderBy[Descending]/ThenBy[Descending])。
[编辑:如Jon所说,LINQ通常会创建一个新序列,保留原始数据]
请注意,将数据推入Dictionary<,>(ToDictionary)将打乱数据,因为字典不尊重任何特定的排序顺序。
但是,最常见的操作(Select,Where,Skip,Take)应该没问题。

如果我没记错的话,ToDictionary() 只是不对顺序做出承诺,但实际上会保持输入顺序(直到你从中删除某些内容)。我并不是说要依赖这一点,但“混乱”似乎不准确。 - Timo

5
我在类似问题中找到了一个很好的答案,其中引用了官方文档。原话如下:
对于《Enumerable》方法(适用于《List》),您可以依赖于SelectWhereGroupBy返回的元素顺序。但是,对于像ToDictionaryDistinct这样本质上无序的事物,情况并非如此。
Enumerable.GroupBy文档中可以看出: IGrouping<TKey, TElement>对象按照每个分组的第一个键在源中产生的元素的顺序进行生成。组内的元素按它们在源中出现的顺序生成。
但对于IQueryable扩展方法(其他LINQ提供程序),这并不一定正确。
来源:Do LINQ's Enumerable Methods Maintain Relative Order of Elements?

2
任何“group by”或“order by”都可能改变顺序。

0
这里的问题特指LINQ-to-Objects。
如果你使用的是LINQ-to-SQL,那么除非你使用类似以下方式强制排序,否则不会有任何顺序:
mysqlresult.OrderBy(e=>e.SomeColumn)

如果您不使用LINQ-to-SQL,那么结果的顺序可能会因后续查询(即使是相同的数据)而有所不同,这可能会导致间歇性错误。

0
对我而言,问题在于确定默认排序顺序,结果发现是按照下面显示的两列排序。经过多次迭代,我成功找到了默认排序顺序,并在我的LINQ查询中重新进行了设置。为了去除重复项,我使用了一个简单的foreach循环来创建一个新的字符串列表,其中不包含重复项。
//original sorting order lost
var inv2 = db.Inventories
.GroupBy(l => l.VendorFullSKU)
.Select(cl => new Inventory2
{
    VariantID = cl.FirstOrDefault() == null ? 0 : cl.FirstOrDefault().VariantID,
    Quan = cl.Sum(c => c.Quan), 
    Color = cl.FirstOrDefault() == null ? "" : cl.FirstOrDefault().Color    
});


//original sorting order restored
var bl = (from pv in db.ProductVariants
join inv in inv2 on pv.VariantID equals inv.VariantID
orderby inv.VariantID, inv.Color //sort
select inv.Color
).ToList();


//remove duplicates while preserving original sort order
var colorsDistinct = new List<string>();
foreach (var item in bl)
{
    if (!colorsDistinct.Contains(item))
        colorsDistinct.Add(item);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接