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

scyqe7ek  于 2023-02-27  发布在  其他
关注(0)|答案(7)|浏览(273)

我想从列表中删除重复项,但不更改列表中唯一元素的顺序。
Jon Skeet和其他人建议使用以下方法:

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

参考:

是否保证唯一元素的顺序与以前相同?如果是,请给予一个参考,以确认这一点,因为我无法找到任何关于它的文档。

xoefb8l8

xoefb8l81#

虽然不能保证,但这是最明显的实现方式。如果不按顺序返回结果,就很难以流的方式实现(即尽可能快地返回结果,尽可能少地读取)。
你可能想看看我关于Edulinq implementation of Distinct()的博客文章。
请注意,即使LINQ to Objects保证了这一点(我个人认为它 * 应该 * 如此),这对其他LINQ提供程序(如LINQ to SQL)也没有任何意义。
在LINQ to Objects中提供的保证水平有时候会有点不一致,IMO。有些优化有文档记录,有些没有。见鬼,有些文档是彻头彻尾的 * 错误 *。

nuypyhwy

nuypyhwy2#

在.NET Framework 3.5中,对Distinct()的Linq-to-Objects实现的CIL进行反汇编表明元素的顺序得到了保留,但这不是文档中记录的行为。
我对Reflector做了一些调查,在对System.Core.dll(Version=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的简化实现(移除了后藤和标签):

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; }
    }
}

如你所见-枚举按照source enumerable(list,我们在其上调用Distinct)提供的顺序进行。Hashset仅用于确定我们是否已经返回了这样的元素。如果没有,我们将返回它,否则-继续在source上枚举。
因此,可以保证Distinct()将以完全相同的顺序**返回元素,这些元素由应用了Distinct的集合提供。

0sgqnhkj

0sgqnhkj3#

根据documentation,序列是无序的。

nr7wwzry

nr7wwzry4#

是的,可枚举。Distinct保持顺序。假设方法是懒惰的,“一看到distinct值就产生distinct值”,它会自动跟随。考虑一下。

NET引用源确认。它返回一个子序列,即每个等价类中的第一个元素。

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

.NET Core implementation与此类似。
令人沮丧的是,Enumerable.Distinct的文档在这一点上很混乱:
结果序列无序。
我只能想象他们的意思是“结果序列没有排序”。您 * 可以 * 通过预先排序然后将每个元素与前一个元素进行比较来实现Distinct,但这不会像上面定义的那样懒惰。

qcbq4gxm

qcbq4gxm5#

虽然有点晚了,但是还没有人真正发布完成这个IMO的最好的完整代码,所以让我提供这个代码(它本质上与.NET Framework使用Distinct()所做的相同)*:

public static IEnumerable<T> DistinctOrdered<T>(this IEnumerable<T> items)
    {
        HashSet<T> returnedItems = new HashSet<T>();
        foreach (var item in items)
        {
            if (returnedItems.Add(item))
                yield return item;
        }                       
    }

这保证了原始的顺序,而不依赖于未记录的或假定的行为。我也相信这比使用多个LINQ方法更有效,尽管我在这里愿意接受纠正。
(*).NET Framework源代码使用内部Set类,该类看起来与HashSet基本相同。

vecaoik1

vecaoik16#

默认情况下,当使用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 library中,我实现了一个扩展方法,以确保在使用特定扩展方法DistinctKeepOrder时Vector3D集保持顺序:
相关代码如下:

/// <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);
}
    • 注**:进一步研究可以找到一种更通用(接口的使用)和优化的方式(不封装对象)。
d5vmydt9

d5vmydt97#

这在很大程度上取决于你的linq提供者,在Linq 2 Objects上你可以保留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;

这和你预想的顺序完全相反。
你可以看到,执行计划可能会根据底层提供者的不同而有所不同,这就是为什么在文档中没有对此做出保证的原因。

相关问题