C#Distinct()方法是否保持序列的原始顺序不变?


82

我想从列表中删除重复项,而不更改列表中唯一元素的顺序。

Jon Skeet和其他人建议使用以下内容

list = list.Distinct().ToList();

从列表C#中删除重复项

从C#中的List <T>中删除重复项

是否可以保证唯一元素的顺序与以前相同?如果是,请提供可确认此内容的参考,因为我在文档中找不到任何内容。


5
@ColonelPanic-此处的官方文档msdn.microsoft.com/zh-cn/library/bb348436(v=vs.110).aspx明确声明“ Distinct()方法返回不包含重复值的无序序列”。
Evk

@Evk“无序序列”与“序列的原始排序”不同。
Nitesh

3
我认为“不受限制”的意思是“没有特定的顺序”,这也意味着“没有必要按原始顺序排列”。
Evk '17

我只是有一个关于oracle12 Entity Framework 6与众不同的问题。在我的情况下,我在不知道linq子句之前就拥有了orderby,并且该命令消失了。select()。OrderBy()。Distinct()。ToList()工作时,select()。OrderBy()。Distinct()。ToList()不起作用。
卡尔,

2
@Karl,这些表达式是相同的。:)
pvgoran

Answers:


75

不能保证,但这是最明显的实现。如果按顺序返回结果,将很难以流方式实现(即使得它尽可能快地返回结果,并已尽可能少地读取结果)。

您可能需要阅读有关Distinct()Edulinq实现的博客文章。

请注意,即使为LINQ to Objects保证了这一点(我个人认为应该如此),对于LINQ to SQL之类的其他LINQ提供程序也没有任何意义。

IMO有时在LINQ中为对象提供的保证级别有些不一致。一些优化已记录在案,而其他则没有。哎呀,有些文件是完全错误的


我接受它是因为1)它明确地回答了我的担忧,即它是否得到保证2)链接的帖子更深入地研究了Distinct的未记录的方面3)链接的帖子还有一个示例实现,可以用作在以下方面实现Distinct的参考有保证的清单。
Nitesh

26

在.NET Framework 3.5中,分解的Linq-to-Objects实现的CILDistinct()显示保留了元素的顺序-但是,这没有记录的行为。

我对Reflector做了一些调查。拆解System.Core.dll,版本= 3.5.0.0后,您可以看到Distinct()是扩展方法,如下所示:

public static class Emunmerable
{
    public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source)
    {
        if (source == null)
            throw new ArgumentNullException("source");

        return DistinctIterator<TSource>(source, null);
    }
}

因此,有趣的是DistinctIterator,它实现了IEnumerable和IEnumerator。这是此IEnumerator的简化实现(删除了goto和lables):

private sealed class DistinctIterator<TSource> : IEnumerable<TSource>, IEnumerable, IEnumerator<TSource>, IEnumerator, IDisposable
{
    private bool _enumeratingStarted;
    private IEnumerator<TSource> _sourceListEnumerator;
    public IEnumerable<TSource> _source;
    private HashSet<TSource> _hashSet;    
    private TSource _current;

    private bool MoveNext()
    {
        if (!_enumeratingStarted)
        {
            _sourceListEnumerator = _source.GetEnumerator();
            _hashSet = new HashSet<TSource>();
            _enumeratingStarted = true;
        }

        while(_sourceListEnumerator.MoveNext())
        {
            TSource element = _sourceListEnumerator.Current;

             if (!_hashSet.Add(element))
                 continue;

             _current = element;
             return true;
        }

        return false;
    }

    void IEnumerator.Reset()
    {
        throw new NotSupportedException();
    }

    TSource IEnumerator<TSource>.Current
    {
        get { return _current; }
    }

    object IEnumerator.Current
    {        
        get { return _current; }
    }
}

如您所见-枚举按源可枚举(我们正在调用的列表)所提供的顺序进行DistinctHashset仅用于确定我们是否已经返回了此类元素。如果没有,我们将返回它,否则-继续在源代码中枚举。

因此,可以保证Distinct()将按完全相同的顺序返回元素,这些元素由应用Distinct的集合提供。


8
这是有据可查的行为吗?
abatishchev 2011年

4
链接的答案包含对文档的引用,该文档指出:“结果序列是无序的。”
mgronber

4
@lazyberezovsky:这个问题是关于保证的,不是常见的实现。(正如我已经说过的,如果实现跨平台/版本进行更改,我会感到惊讶,但这并不能保证。)
LukeH 2011年

5
@lazyberezovsky:我来自C \ C ++,那里的许多事情都没有定义,并且很常见的问题是是否要保证某些事情。我也在Silverlight应用程序中使用Distinct(),在Mac和Windows上都使用,这就是为什么我们不能保证采用“通用实现”的原因,因此必须保证这一点。
Nitesh

42
@lazyberezovsky:当人们谈论担保时,通常指的是有据可依的行为。例如,GroupBy的文档确实指定了行为,而Distinct的文档没有指定
乔恩·斯基特


6

是的,Enumerable.Distinct保留顺序。假设该方法是“懒惰的”,则“在看到它们后立即产生不同的值”,它会自动执行。想一想。

.NET参考源确认。它返回一个子序列,每个子类中的第一个元素。

foreach (TSource element in source)
    if (set.Add(element)) yield return element;

.NET核心实现是类似的。

令人沮丧的是,Enumerable.Distinct的文档在这一点上感到困惑:

结果序列是无序的。

我只能想象它们的意思是“结果序列未排序”。您可以通过预排序然后将每个元素与前一个元素进行比较实现Distinct,但这不会像上面定义的那样懒惰。


6
来源不是规格。您发现的是一个巧合,在下一次更新后可能无效。
Henk Holterman

@HenkHolterman一般来说,我同意,实现可以更改。例如,.NET 4.5更改了Array.Sort之后的排序算法。但是,在这种特殊情况下,任何明智的Enumerable.Distinct实现都肯定是懒惰的(“一旦看到它们就产生了不同的值”),并且随之而来的是保留顺序的属性。惰性评估是LINQ to Objects的核心宗旨。取消它是不可想象的。
上校恐慌

1
我已经看到了使用.net 4.6的实现,其中调用dbQuery.OrderBy(...).Distinct().ToList()未按谓词顺序指定的顺序返回列表-删除Distinct(碰巧是多余的)修复了我的问题
Rowland Shaw

1

默认情况下,使用Distinct时linq运算符使用Equals方法,但是您可以使用自己的IEqualityComparer<T>对象使用自定义逻辑实现GetHashCodeEquals方法指定何时两个对象相等。请记住:

GetHashCode不应使用繁琐的cpu比较(例如,仅使用一些显而易见的基本检查),并且首先将其用于说明两个对象肯定不同(如果返回不同的哈希码)还是可能相同(相同的哈希码)。在这种最新情况下,当两个对象具有相同的哈希码时,框架将逐步检查是否使用Equals方法作为有关给定对象是否相等的最终决定。

在拥有MyTypeMyTypeEqualityComparer跟随类的代码之后,请不要确保序列保持其顺序:

var cmp = new MyTypeEqualityComparer();
var lst = new List<MyType>();
// add some to lst
var q = lst.Distinct(cmp);

sci库中,我实现了一种扩展方法,以确保使用特定扩展方法时,Vector3D集保持顺序DistinctKeepOrder

相关代码如下:

/// <summary>
/// support class for DistinctKeepOrder extension
/// </summary>
public class Vector3DWithOrder
{
    public int Order { get; private set; }
    public Vector3D Vector { get; private set; }
    public Vector3DWithOrder(Vector3D v, int order)
    {
        Vector = v;
        Order = order;
    }
}

public class Vector3DWithOrderEqualityComparer : IEqualityComparer<Vector3DWithOrder>
{
    Vector3DEqualityComparer cmp;

    public Vector3DWithOrderEqualityComparer(Vector3DEqualityComparer _cmp)
    {
        cmp = _cmp;
    }

    public bool Equals(Vector3DWithOrder x, Vector3DWithOrder y)
    {
        return cmp.Equals(x.Vector, y.Vector);
    }

    public int GetHashCode(Vector3DWithOrder obj)
    {
        return cmp.GetHashCode(obj.Vector);
    }
}

简而言之,Vector3DWithOrder封装类型和顺序整数,而Vector3DWithOrderEqualityComparer封装原始类型比较器。

这是确保订单维持的方法助手

/// <summary>
/// retrieve distinct of given vector set ensuring to maintain given order
/// </summary>        
public static IEnumerable<Vector3D> DistinctKeepOrder(this IEnumerable<Vector3D> vectors, Vector3DEqualityComparer cmp)
{
    var ocmp = new Vector3DWithOrderEqualityComparer(cmp);

    return vectors
        .Select((w, i) => new Vector3DWithOrder(w, i))
        .Distinct(ocmp)
        .OrderBy(w => w.Order)
        .Select(w => w.Vector);
}

注意:进一步的研究可能允许找到更通用的(使用接口)和优化的方式(不封装对象)。


1

这在很大程度上取决于您的linq提供者。在Linq2Objects上,您可以停留在的内部源代码上Distinct,这使我们假设保留了原始顺序。

但是,例如,对于其他解析为某种SQL的提供程序而言,情况ORDER BY并非一定如此,因为-语句通常在任何聚合(例如Distinct)之后出现。因此,如果您的代码是这样的:

myArray.OrderBy(x => anothercol).GroupBy(x => y.mycol);

这被转换为类似于SQL中的以下内容:

SELECT * FROM mytable GROUP BY mycol ORDER BY anothercol;

显然,这首先会将您的数据分组,然后再对其进行排序。现在,您被DBMS自己如何执行的逻辑所束缚。在某些DBMS上甚至不允许这样做。想象以下数据:

mycol anothercol
1     2
1     1
1     3
2     1
2     3

执行时,myArr.OrderBy(x => x.anothercol).GroupBy(x => x.mycol)我们假设以下结果:

mycol anothercol
1     1
2     1

但是,DBMS可能会汇总另一个列,以便始终使用第一行的值,从而得到以下数据:

mycol anothercol
1    2
2    1

订购后将导致以下结果:

mycol anothercol
2    1
1    2

这类似于以下内容:

SELECT mycol, First(anothercol) from mytable group by mycol order by anothercol;

这与您的预期完全相反。

您会看到执行计划可能会有所不同,具体取决于基础提供程序是什么。这就是为什么文档中对此没有保证的原因。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.