从对集合数据去重到Distinct源码分析
今天在写代码的时候要对数据进行去重,正打算使用Distinct方法的时候,发现这个用了这么久的东西,竟然不知道它是怎么实现的,于是就有了这篇文章.
使用的.net core2.0
1.需求
假如我们有这样一个类
public class Model
{
public int Code { get; set; }
public int No { get; set; }
public override string ToString()
{
return "No:" + No + ",Code:" + Code;
}
}
还有这样一组数据
public static IEnumerable<Model> GetList()
{
return new List<Model>()
{
new Model(){No = 1,Code = 1},
new Model(){No = 1,Code = 2},
new Model(){No = 7,Code = 1},
new Model(){No = 11,Code = 1},
new Model(){No = 55,Code = 1},
new Model(){No = 11,Code = 1},//重复
new Model(){No = 6,Code = 7},
new Model(){No = 1,Code = 1},
new Model(){No = 6,Code = 7},//重复
};
}
我们要把集合中重复的数据去掉,对的就这么简单个需求,工作中可不会有这么简单的需求.
2.在刚学编程的时候我们可能这样写的
在很久以前一直使用这种简单粗暴的方法解决重复问题
/// <summary>
/// 双重循环去重
/// </summary>
/// <param name="list"></param>
/// <returns></returns>
public static IEnumerable<Model> MyDistinct(IEnumerable<Model> list)
{
var result = new List<Model>();
foreach (var item in list)
{
//标记
var flag = true;
foreach (var item2 in result)
{
//已经存在的标记为false
if (item2.Code == item.Code && item2.No == item.No)
{
flag = false;
}
}
if (flag)
{
result.Add(item);
}
}
return result;
}
3.后来认识了Distinct
后来知道了Distinct去重,我们写法变成了这样
/// <summary>
/// 比较器
/// </summary>
public class ModelEquality : IEqualityComparer<Model>
{
public bool Equals(Model x, Model y)
{
return x.No == y.No && x.Code == y.Code;
}
public int GetHashCode(Model obj)
{
return obj.No.GetHashCode() + obj.Code.GetHashCode();
}
}
//这样就可以得到去重后的集合
GetList().Distinct(new ModelEquality());
4.探究Distinct源码
我们去github找一下源码,微软开源的仓库地址:https://github.com/dotnet/corefx
为了篇幅我删掉了一些不相关的一些代码
namespace System.Linq
{
public static partial class Enumerable
{
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)
{
throw Error.ArgumentNull(nameof(source));
}
return new DistinctIterator<TSource>(source, comparer);
}
private sealed class DistinctIterator<TSource> : Iterator<TSource>, IIListProvider<TSource>
{
private readonly IEnumerable<TSource> _source;
private readonly IEqualityComparer<TSource> _comparer;
private Set<TSource> _set;
private IEnumerator<TSource> _enumerator;
public DistinctIterator(IEnumerable<TSource> source, IEqualityComparer<TSource> comparer)
{
_source = source;
_comparer = comparer;
}
public override bool MoveNext()
{
switch (_state)
{
case 1:
_enumerator = _source.GetEnumerator();
if (!_enumerator.MoveNext())
{
Dispose();
return false;
}
TSource element = _enumerator.Current;
_set = new Set<TSource>(_comparer);
_set.Add(element);
_current = element;
_state = 2;
return true;
case 2:
while (_enumerator.MoveNext())
{
element = _enumerator.Current;
if (_set.Add(element))
{
_current = element;
return true;
}
}
break;
}
Dispose();
return false;
}
public override void Dispose()
{
//省略...
}
}
}
}
Iterator<TSource>是一个抽象类实现了Iterator<TSource> : IEnumerable<TSource>, IEnumerator<TSource>
我们主要看DistinctIterator类中的代码,发现有这么一个私有成员Set<TSource> _set;,我们再看MoveNext方法中有这么一段
element = _enumerator.Current;
if (_set.Add(element))
{
_current = element;
return true;
}
到这里我似乎明白了什么,回忆下Set集合的特点"无序","不可重复",再看代码中只有对set Add成功才对_current赋值,return true.那么这个Set应该就是内部维护的一个集合,也就是我们要的去重后的数据,那么Set里的Add方法就是关键
同样去掉了一些没有用到的,加了注释
namespace System.Linq
{
/// <summary>
/// A lightweight hash set.
///一个 轻量级hash set
/// </summary>
/// <typeparam name="TElement">The type of the set's items.</typeparam>
internal sealed class Set<TElement>
{
/// <summary>
/// The comparer used to hash and compare items in the set.
/// </summary>
private readonly IEqualityComparer<TElement> _comparer;
/// <summary>
/// The hash buckets, which are used to index into the slots.
/// hash环,每一个指向了下面Slot中的index
/// </summary>
private int[] _buckets;
/// <summary>
/// The slots, each of which store an item and its hash code.
/// 数组的每一个储存了他们自身和自己的hash
/// </summary>
private Slot[] _slots;
/// <summary>
/// The number of items in this set.
/// </summary>
private int _count;
/// <summary>
/// Constructs a set that compares items with the specified comparer.
/// </summary>
/// <param name="comparer">
/// The comparer. If this is <c>null</c>, it defaults to <see cref="EqualityComparer{TElement}.Default"/>.
/// </param>
public Set(IEqualityComparer<TElement> comparer)
{
_comparer = comparer ?? EqualityComparer<TElement>.Default;
//初始化长度7
_buckets = new int[7];
//初始化长度7
_slots = new Slot[7];
}
/// <summary>
/// Attempts to add an item to this set.
/// 我们要看的方法
/// </summary>
/// <param name="value">The item to add.</param>
/// <returns>
/// <c>true</c> if the item was not in the set; otherwise, <c>false</c>.
/// </returns>
public bool Add(TElement value)
{
//取的当前项的hash
int hashCode = InternalGetHashCode(value);
//重复的hashCode的话, _buckets[hashCode % _buckets.Length] - 1的值就不会是-1
//就会进入下面的if判断
//
for (int i = _buckets[hashCode % _buckets.Length] - 1; i >= 0; i = _slots[i]._next)
{
//如果存在重复就会直接返回false,没有的话i会变为_next所指向的hash相等的元素,减少了循环次数,类似链表
if (_slots[i]._hashCode == hashCode && _comparer.Equals(_slots[i]._value, value))
{
return false;
}
}
//Slot数量满了后
if (_count == _slots.Length)
{
//对数组进行扩容
Resize();
}
//元素要添加进_slots的下标位置
int index = _count;
//对数量进行增加
_count++;
//对当前项的hash 取余
int bucket = hashCode % _buckets.Length;
//赋值
_slots[index]._hashCode = hashCode;
_slots[index]._value = value;
//当hash第一次出现的时候值为-1,重复出现的时候为上一个出现重复bucket值存放在slots中的索引,-1是因为下一行+1了
_slots[index]._next = _buckets[bucket] - 1;
//指向当前元素索引+1 出现重复的bucket值则会覆盖旧的bucket位置的值
_buckets[bucket] = index + 1;
return true;
}
/// <summary>
/// Expands the capacity of this set to double the current capacity, plus one.
/// 对set扩容
/// </summary>
private void Resize()
{
int newSize = checked((_count * 2) + 1);
int[] newBuckets = new int[newSize];
Slot[] newSlots = new Slot[newSize];
Array.Copy(_slots, 0, newSlots, 0, _count);
for (int i = 0; i < _count; i++)
{
int bucket = newSlots[i]._hashCode % newSize;
newSlots[i]._next = newBuckets[bucket] - 1;
newBuckets[bucket] = i + 1;
}
_buckets = newBuckets;
_slots = newSlots;
}
/// <summary>
/// The number of items in this set.
/// </summary>
public int Count => _count;
/// <summary>
/// Gets the hash code of the provided value with its sign bit zeroed out, so that modulo has a positive result.
/// </summary>
/// <param name="value">The value to hash.</param>
/// <returns>The lower 31 bits of the value's hash code.</returns>
private int InternalGetHashCode(TElement value) => value == null ? 0 : _comparer.GetHashCode(value) & 0x7FFFFFFF;
/// <summary>
/// An entry in the hash set.
/// </summary>
private struct Slot
{
/// <summary>
/// The hash code of the item.
/// hash值
/// </summary>
internal int _hashCode;
/// <summary>
/// In the case of a hash collision, the index of the next slot to probe.
/// 下一个用于检查的元素index
/// </summary>
internal int _next;
/// <summary>
/// The item held by this slot.
/// </summary>
internal TElement _value;
}
}
}
5.分析下去重的思路
图用自带画图画的,难看还请见谅.

我后面回放代码,一步一步调试可能会更容易理解.
1.假如我们第一个Model进行hash取余得到的为0,此时_buckets[0]为0,所以不会进入for循环条件,直接进行下面的赋值操作
_slots[0]=当前的元素 next=-1 hash=7
buckets[0]=1 指向当前元素索引+1
2.继续下一个Model进行hash取余,假如又为0,buckets[0]-1为0,满足循环条件,进入判断,取到_slots[0]的值,进行比较,发现相等的话则会直接返回.
3.继续上面的步骤,这次hash取余为3,没出现过,
_slots[1]=当前的元素 next=-1 hash=10
buckets[2]=2 指向当前元素索引+1
.........
4.这个时候又出现了一次hash取余为3,进入判断中,取到_slots[1]的值,进行比较发现不相等,next为-1不会有下一次循环,
_slots[3]=当前的元素 next=1 hash=10
buckets[2]=4 指向当前元素索引+1
注意此时next不是-1了,而是1,也就是上一个相同hash取余的元素在_slots中的位置,此时形成了一个链表.这样少了很多的比较次数.
5.这个时候又出现了一个hash取余为3的,进入判断中,取到_slots[3]的值,进行比较发现不相等,next为1,则再次与_slots[1]的元素进行比较,如果发现相等的舍弃,反之最后加入到set中
假如不相同,则:
_slots[4]=当前的元素 next=3 hash=10
buckets[2]=5 指向当前元素索引+1
6.结束
结束了,我们发现Distinct使用了hash进行去重,实现思路上感觉很值得我学习(我是写不出来的..).
Distinct很依赖于比较器的GetHashCode方法,如果随便返回一个固定值的话,会增加很大的开销.不要为了偷懒再返回一个固定int值了.
希望这篇文章可以对大家有帮助 有启发
代码地址:https://git.coding.net/changyell/DistinctDemo.git
本人是个菜鸟,文章如果有错误的地方,烦请大佬们指正,谢谢...
从对集合数据去重到Distinct源码分析的更多相关文章
- Linq聚合操作之Aggregate,Count,Sum,Distinct源码分析
Linq聚合操作之Aggregate,Count,Sum,Distinct源码分析 一:Linq的聚合运算 1. 常见的聚合运算:Aggregate,Count, Sum, Distinct,Max, ...
- 【集合框架】JDK1.8源码分析之HashMap(一) 转载
[集合框架]JDK1.8源码分析之HashMap(一) 一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化 ...
- 【集合框架】JDK1.8源码分析之ArrayList详解(一)
[集合框架]JDK1.8源码分析之ArrayList详解(一) 一. 从ArrayList字表面推测 ArrayList类的命名是由Array和List单词组合而成,Array的中文意思是数组,Lis ...
- vue 快速入门 系列 —— 侦测数据的变化 - [vue 源码分析]
其他章节请看: vue 快速入门 系列 侦测数据的变化 - [vue 源码分析] 本文将 vue 中与数据侦测相关的源码摘了出来,配合上文(侦测数据的变化 - [基本实现]) 一起来分析一下 vue ...
- 【集合框架】JDK1.8源码分析之ArrayList(六)
一.前言 分析了Map中主要的类之后,下面我们来分析Collection下面几种常见的类,如ArrayList.LinkedList.HashSet.TreeSet等.下面通过JDK源码来一起分析Ar ...
- 【集合框架】JDK1.8源码分析之LinkedList(七)
一.前言 在分析了ArrayList了之后,紧接着必须要分析它的同胞兄弟:LinkedList,LinkedList与ArrayList在底层的实现上有所不同,其实,只要我们有数据结构的基础,在分析源 ...
- Java集合详解及List源码分析
对于数组我们应该很熟悉,一个数组在内存中总是一块连续的存储空间,数组的创建使用new关键字,数组是引用类型的数据,一旦第一个元素的位置确定,那么后面的元素位置也就确定了,数组有一个最大的局限就是数组一 ...
- java集合【13】——— Stack源码分析走一波
前言 集合源码分析系列:Java集合源码分析 前面已经把Vector,ArrayList,LinkedList分析完了,本来是想开始Map这一块,但是看了下面这个接口设计框架图:整个接口框架关系如下( ...
- 【集合框架】JDK1.8源码分析之HashMap(一)
一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化,其中最重要的一个优化就是桶中的元素不再唯一按照链表组合,也 ...
随机推荐
- 007Spring Security
01.基于Spring AOP 和 Servlet规范中Filter实现 的安全框架 <dependency> <groupId>org.springframework.se ...
- windows10(本机)与VirtualBox中CentOS7(虚拟机)互相访问总结
先把我这里的环境说下: 本机(windows10),发布了一个tomcat服务:http://192.168.0.106:8080/axis/services/VPMService?wsdl 如下图: ...
- ubuntu16下面 redis 无法链接到客户端问题
1.今天从github上面下载了一个项目,链接到自己的tomcat里面的redis,结果在虚拟机里面可以链接成功,但是在客户端总是提示链接失败.google之后,原来是 因为 需要在redis里面设置 ...
- styling the SVG images
SVG不像canvas,SVG的所有元素都是以DOM元素存在于文档中的,我们可以像给任何普通的dom元素添加css样式一样,可以对svg的元素做styling.不过SVG元素的css样式名称和普通ht ...
- 使用 docker-machine 管理 Azure 容器虚拟机
安装 docker-machine 请参见该链接(https://docs.docker.com/machine/install-machine "https://docs.docker.c ...
- mysql在linux下的安装mysql-5.6.33
一.下载源码包 wget http://mirrors.sohu.com/mysql/MySQL-5.6/mysql-5.6.35-linux-glibc2.5-x86_64.tar.gz 二.解压源 ...
- Python初学者第十四天 三元运算及文件处理2
14day 1.三元运算: 又称三目运算,是对简单的条件语句的简写 如简单条件语句: if a > b: n = a else: n = b print(n) 三目运算语句: n = a if ...
- VC listBox控件的方法
获取listBox里所有行数 GetCount(); 设置某行的选中状态 SetSel(index,true);第一个参数是行号,第二个参数是否选中
- HTTP协议图--HTTP 协议报文结构
1.HTTP 报文 用于 HTTP 协议交互的信息被称为 HTTP 报文.请求端(客户端)的 HTTP 报文叫做请求报文:响应端(服务器端)的叫做响应报文.HTTP 报文本身是由多行(用 CR[car ...
- jquery 中 $.map 用法
$.map(data,function(item,index){return XXX}) 遍历data数组中的每个元素,并按照return中的计算方式 形成一个新的元素,放入返回的数组中 var b ...