概要
Distinct方法是LINQ代码库中的一个重要扩展方法,它可以帮忙我们将数组或其他集合中的重复元素过滤掉。
为了更好的使用该方法,我们从源码角度分析一下该方法,从而更好的了解其是如何将重复元素过滤,如何解决多线程同时读取的问题。
关键源码分析
Distinct方法
public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source) => Distinct(source, null);
public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource>? comparer)
{
if (source == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
return new DistinctIterator<TSource>(source, comparer);
}
- LINQ对外提供给用户的Distinct方法有两个
- 第一个Distinct方法就是以空比较器作为参数调用第二个Distinct方法,主要用于过滤像数字,字符等系统有默认比较器的数据类型。
- 第二个Distinct方法主要用于需要指定比较器的过滤操作, 我们必须告诉程序如何判定这些自定义对象是否是重复的,例如我们可以将对象中的Id一样的数据,视为重复数据,这就需要用户自定义比较器,程序才能知道如何进行过滤。
- 两个Distinct方法最后都是返回DistinctIterator类的实例。所以该方法也是不返回具体数据,只返回迭代器,支持延迟加载。
private sealed partial class DistinctIterator<TSource> : Iterator<TSource>
{
private readonly IEnumerable<TSource> _source;
private readonly IEqualityComparer<TSource>? _comparer;
private HashSet<TSource>? _set;
private IEnumerator<TSource>? _enumerator;
public DistinctIterator(IEnumerable<TSource> source, IEqualityComparer<TSource>? comparer)
{
Debug.Assert(source != null);
_source = source;
_comparer = comparer;
}
public override Iterator<TSource> Clone() {
return new DistinctIterator<TSource>(_source, _comparer);
}
public override bool MoveNext()
{
switch (_state)
{
case 1:
_enumerator = _source.GetEnumerator();
if (!_enumerator.MoveNext())
{
Dispose();
return false;
}
TSource element = _enumerator.Current;
_set = new HashSet<TSource>(DefaultInternalSetCapacity, _comparer);
_set.Add(element);
_current = element;
_state = 2;
return true;
case 2:
Debug.Assert(_enumerator != null);
Debug.Assert(_set != null);
while (_enumerator.MoveNext())
{
element = _enumerator.Current;
if (_set.Add(element))
{
_current = element;
return true;
}
}
break;
}
Dispose();
return false;
}
public override void Dispose()
{
if (_enumerator != null)
{
_enumerator.Dispose();
_enumerator = null;
_set = null;
}
base.Dispose();
}
}
}
}
- DistinctIterator迭代器继承自基类迭代器Iterator,Iterator类是一个抽象类,其中包含了多线程支持和嵌套循环支持的功能,LINQ中的其他迭代器也都继承了该类。Iterator类代见附录。
- Iterator是抽象类,可以提前在构造方法中给所有迭代器都用到的栏位赋值,例如_state默认赋值为1
结合DistinctIterator类的代码,如果我们通过foreach循环,逐个读取过滤后的元素,代码如下,其中Student类和比较器的代码见附录:
var stuList = studentList.Distinct(new StudentEqualityComparer());
foreach(var stu in stuList){
Console.WriteLine(stu.Name);
}
执行流程如下:
- DistinctIterator构造方法会将要迭代的集合和集合元素比较器初始化。
- Distinct方法返回DistinctIterator实例的对象。
- 当过滤结果通过foreach循环读取时,会调先调用基类Iterator的GetEnumerator方法,判定是否有多个线程在使用该迭代器,如果不是将返回当前迭代器对象,否则调用DistinctIterator的Clone()方法,克隆一个新的迭代器对象返回。基类Iterator代码见附录。
- 在基类Iterator的GetEnumerator方法中,将_state改为1,表示该迭代器处于等待开始迭代的状态。
- foreach调用DistinctIterator实例的MoveNext方法,启动迭代。
- 进入MoveNext方法,_state值是1,进入case 1子句,如果_enumerator.MoveNext()返回为false,即要进行迭代的集合为空,直接返回。
- 实例化HashSet,初始大小是7。
- 将集合的第一个元素放入HashSet中,HashSet是一种高效的存储结构,不能包含任何重复元素。如果要添加的是重复元素,则Add方法会返回false。由于是第一次添加,因此不需要进行是否添加成功的检查。
- 将_state改为2,表示该迭代器正式开始工作。
- 从第二的元素开始,foreach调用DistinctIterator对象的MoveNext方法,
- 直接进入case 2,如果元素可以正常添加到HashSet中,则表示该元素在集合中是唯一的,所以返回true,否则返回false。
- 当迭代完成,调用Dispose方法,从内存中移除该迭代器对象。
DistinctBy方法
源码验证
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => DistinctBy2(source, keySelector, null);
public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey>? comparer)
{
if (source is null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
if (keySelector is null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.keySelector);
}
return DistinctByIterator(source, keySelector, comparer);
}
- LINQ对外提供给用户的DistinctBy方法有两个,该方法让我们根据一个指定的key值进行过滤操作,比较器也是针对指定的key值进行比较。
- 第一个DictinctBy方法实际上,就是以空比较器作为参数调用第二个Distinct方法,主要用于过滤像数字,字符等系统有默认比较器的数据类型。
- 第二个DistinctBy方法主要用于需要指定比较器的过滤操作。
- 在进行了迭代元素集合和获取Key值的委托不能为空的检查后,调用DistinctByIterator方法,真正开始过滤重复的元素。
private static IEnumerable<TSource> DistinctByIterator<TSource, TKey>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey>? comparer)
{
using IEnumerator<TSource> enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
{
var set = new HashSet<TKey>(DefaultInternalSetCapacity, comparer);
do
{
TSource element = enumerator.Current;
if (set.Add(keySelector(element)))
{
yield return element;
}
}
while (enumerator.MoveNext());
}
}
- DistinctByIterator方法包含三个参数,第一个是扩展方法参数,第二个是获取Key值的系统委托KeySelector,可以是Lambda表达式,最后一个参数是针对Key值的比较器。
- 调用基类Iterator的GetEnumerator方法,获取迭代器实例。
- 迭代开始后,调用系统委托KeySelector,获取Key值。
- 实例化HashSet,初始大小是7。
- 将Key值存入HashSet中,使用传入的比较器对Key进行比较。
- 如果Key值在HashSet中不存在重复项,则通过yield进行返回。yield作为迭代器实例的语法糖,在实际使用上显然要比定义具体的迭代器类DistinctIterator要简单的多。
结论
Distinct或DistinctBy方法在实现上都是使用HasSet来过滤掉集合中的重复元素,支持延迟加载特性。
附录
Iterator类源码
using System.Collections;
using System.Collections.Generic;
public static partial class Enumerable
{
internal abstract class Iterator<TSource> : IEnumerable<TSource>, IEnumerator<TSource>
{
private readonly int _threadId;
internal int _state;
internal TSource _current = default!;
/// <summary>
/// Initializes a new instance of the <see cref="Iterator{TSource}"/> class.
/// </summary>
protected Iterator()
{
_threadId = Environment.CurrentManagedThreadId;
}
/// <summary>
/// The item currently yielded by this iterator.
/// </summary>
public TSource Current => _current;
/// <summary>
/// Makes a shallow copy of this iterator.
/// </summary>
/// <remarks>
/// This method is called if <see cref="GetEnumerator"/> is called more than once.
/// </remarks>
public abstract Iterator<TSource> Clone();
/// <summary>
/// Puts this iterator in a state whereby no further enumeration will take place.
/// </summary>
/// <remarks>
/// Derived classes should override this method if necessary to clean up any
/// mutable state they hold onto (for example, calling Dispose on other enumerators).
/// </remarks>
public virtual void Dispose()
{
_current = default!;
_state = -1;
}
/// <summary>
/// Gets the enumerator used to yield values from this iterator.
/// </summary>
/// <remarks>
/// If <see cref="GetEnumerator"/> is called for the first time on the same thread
/// that created this iterator, the result will be this iterator. Otherwise, the result
/// will be a shallow copy of this iterator.
/// </remarks>
public IEnumerator<TSource> GetEnumerator()
{
Iterator<TSource> enumerator = _state == 0 && _threadId == Environment.CurrentManagedThreadId ? this : Clone();
enumerator._state = 1;
return enumerator;
}
/// <summary>
/// Retrieves the next item in this iterator and yields it via <see cref="Current"/>.
/// </summary>
/// <returns><c>true</c> if there was another value to be yielded; otherwise, <c>false</c>.</returns>
public abstract bool MoveNext();
/// <summary>
/// Returns an enumerable that maps each item in this iterator based on a selector.
/// </summary>
/// <typeparam name="TResult">The type of the mapped items.</typeparam>
/// <param name="selector">The selector used to map each item.</param>
public virtual IEnumerable<TResult> Select<TResult>(Func<TSource, TResult> selector)
{
return new SelectEnumerableIterator<TSource, TResult>(this, selector);
}
/// <summary>
/// Returns an enumerable that filters each item in this iterator based on a predicate.
/// </summary>
/// <param name="predicate">The predicate used to filter each item.</param>
public virtual IEnumerable<TSource> Where(Func<TSource, bool> predicate)
{
return new WhereEnumerableIterator<TSource>(this, predicate);
}
object? IEnumerator.Current => Current;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
void IEnumerator.Reset() => throw new NotSupportedException();
}
}
Student类源码
public class Student : IEnumerator, IEnumerable {
public string Id { get; set; }
public string Name { get; set; }
public string Classroom { get; set; }
public Student(string id, string name, string classroom)
{
this.Id = id;
this.Name = name;
this.Classroom = classroom;
}
}
}