浅析C# Dictionary实现原理


  • 一、前言

  • 二、理论知识

    • 1、Hash 算法

    • 2、Hash 桶算法

    • 3、解决冲突算法

  • 三、Dictionary 实现

    • 1. Entry 结构体

    • 2. 其它关键私有变量

    • 3. Dictionary - Add 操作

    • 4. Dictionary - Find 操作

    • 5. Dictionary - Remove 操作

    • 6. Dictionary - Resize 操作(扩容)

    • 7. Dictionary - 再谈 Add 操作

    • 8. Collection 版本控制

  • 四、参考文献及总结


一、前言

本篇文章配图以及文字其实整理出来很久了,但是由于各种各样的原因推迟到现在才发出来,还有之前立 Flag 的《多线程编程》的笔记也都已经写好了,只是说还比较糙,需要找个时间整理一下才能和大家见面。

对于 C#中的Dictionary类相信大家都不陌生,这是一个Collection(集合)类型,可以通过Key/Value(键值对的形式来存放数据;该类最大的优点就是它查找元素的时间复杂度接近O(1),实际项目中常被用来做一些数据的本地缓存,提升整体效率。

那么是什么样的设计能使得Dictionary类能实现O(1)的时间复杂度呢?那就是本篇文章想和大家讨论的东西;这些都是个人的一些理解和观点,如有表述不清楚、错误之处,请大家批评指正,共同进步。

二、理论知识

对于 Dictionary 的实现原理,其中有两个关键的算法,一个是Hash算法,一个是用于应对 Hash 碰撞冲突解决算法。

1、Hash 算法

Hash 算法是一种数字摘要算法,它能将不定长度的二进制数据集给映射到一个较短的二进制长度数据集,常见的 MD5 算法就是一种 Hash 算法,通过 MD5 算法可对任何数据生成数字摘要。而实现了 Hash 算法的函数我们叫她Hash 函数。Hash 函数有以下几点特征。

  1. 相同的数据进行 Hash 运算,得到的结果一定相同。HashFunc(key1) == HashFunc(key1)

  2. 不同的数据进行 Hash 运算,其结果也可能会相同,(Hash 会产生碰撞)。key1 != key2 => HashFunc(key1) == HashFunc(key2).

  3. Hash 运算时不可逆的,不能由 key 获取原始的数据。key1 => hashCode但是hashCode =\=> key1

下图就是 Hash 函数的一个简单说明,任意长度的数据通过 HashFunc 映射到一个较短的数据集中。

f0a5ac37feaca5d56ef076a8b8b536ad.png
1548491108167

关于 Hash 碰撞下图很清晰的就解释了,可从图中得知Sandra DeeJohn Smith通过 hash 运算后都落到了02的位置,产生了碰撞和冲突。341cd024897cbf6f34987b2aaaa8ac57.png常见的构造 Hash 函数的算法有以下几种。

**1. 直接寻址法:**取 keyword 或 keyword 的某个线性函数值为散列地址。即 H(key)=key 或 H(key) = a•key + b,当中 a 和 b 为常数(这样的散列函数叫做自身函数)

**2. 数字分析法:**分析一组数据,比方一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体同样,这种话,出现冲突的几率就会非常大,可是我们发现年月日的后几位表示月份和详细日期的数字区别非常大,假设用后面的数字来构成散列地址,则冲突的几率会明显减少。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。

**3. 平方取中法:**取 keyword 平方后的中间几位作为散列地址。

**4. 折叠法:**将 keyword 切割成位数同样的几部分,最后一部分位数能够不同,然后取这几部分的叠加和(去除进位)作为散列地址。

**5. 随机数法:**选择一随机函数,取 keyword 的随机值作为散列地址,通经常使用于 keyword 长度不同的场合。

**6. 除留余数法:**取 keyword 被某个不大于散列表表长 m 的数 p 除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅能够对 keyword 直接取模,也可在折叠、平方取中等运算之后取模。对 p 的选择非常重要,一般取素数或 m,若 p 选的不好,容易产生碰撞.

2、Hash 桶算法

说到 Hash 算法大家就会想到Hash 表,一个 Key 通过 Hash 函数运算后可快速的得到 hashCode,通过 hashCode 的映射可直接 Get 到 Value,但是 hashCode 一般取值都是非常大的,经常是 2^32 以上,不可能对每个 hashCode 都指定一个映射。

因为这样的一个问题,所以人们就将生成的 HashCode 以分段的形式来映射,把每一段称之为一个Bucket(桶),一般常见的 Hash 桶就是直接对结果取余。

假设将生成的 hashCode 可能取值有 2^32 个,然后将其切分成一段一段,使用8个桶来映射,那么就可以通过bucketIndex = HashFunc(key1) % 8这样一个算法来确定这个 hashCode 映射到具体的哪个桶中。

大家可以看出来,通过 hash 桶这种形式来进行映射,所以会加剧 hash 的冲突。

3、解决冲突算法

对于一个 hash 算法,不可避免的会产生冲突,那么产生冲突以后如何处理,是一个很关键的地方,目前常见的冲突解决算法有拉链法(Dictionary 实现采用的)、开放定址法、再 Hash 法、公共溢出分区法,本文只介绍拉链法与再 Hash 法,对于其它算法感兴趣的同学可参考文章最后的参考文献。

**1. 拉链法:**这种方法的思路是将产生冲突的元素建立一个单链表,并将头指针地址存储至 Hash 表对应桶的位置。这样定位到 Hash 表桶的位置后可通过遍历单链表的形式来查找元素。

**2. 再 Hash 法:**顾名思义就是将 key 使用其它的 Hash 函数再次 Hash,直到找到不冲突的位置为止。

对于拉链法有一张图来描述,通过在冲突位置建立单链表,来解决冲突。

b66385e2fccebbe36c2e67dd8ea345b9.png
1548485607652

三、Dictionary 实现

Dictionary 实现我们主要对照源码来解析,目前对照源码的版本是**.Net Framwork 4.7**。地址可戳一戳这个链接 源码地址:Link[1]

这一章节中主要介绍 Dictionary 中几个比较关键的类和对象,然后跟着代码来走一遍插入、删除和扩容的流程,相信大家就能理解它的设计原理。

1. Entry 结构体

首先我们引入Entry这样一个结构体,它的定义如下代码所示。这是 Dictionary 种存放数据的最小单位,调用Add(Key,Value)方法添加的元素都会被封装在这样的一个结构体中。

private struct Entry {public int hashCode;    // 除符号位以外的31位hashCode值, 如果该Entry没有被使用,那么为-1public int next;        // 下一个元素的下标索引,如果没有下一个就为-1public TKey key;        // 存放元素的键public TValue value;    // 存放元素的值
}

2. 其它关键私有变量

除了 Entry 结构体外,还有几个关键的私有变量,其定义和解释如下代码所示。

private int[] buckets;  // Hash桶
private Entry[] entries; // Entry数组,存放元素
private int count;   // 当前entries的index位置
private int version;  // 当前版本,防止迭代过程中集合被更改
private int freeList;  // 被删除Entry在entries中的下标index,这个位置是空闲的
private int freeCount;  // 有多少个被删除的Entry,有多少个空闲的位置
private IEqualityComparer<TKey> comparer; // 比较器
private KeyCollection keys;  // 存放Key的集合
private ValueCollection values;  // 存放Value的集合

上面代码中,需要注意的是buckets、entries这两个数组,这是实现 Dictionary 的关键。

3. Dictionary - Add 操作

经过上面的分析,相信大家还不是特别明白为什么需要这么设计,需要这么做。那我们现在来走一遍 Dictionary 的 Add 流程,来体会一下。

首先我们用图的形式来描述一个 Dictionary 的数据结构,其中只画出了关键的地方。桶大小为 4 以及 Entry 大小也为 4 的一个数据结构。

51af540ec0ac026c4a2e8c76cad550b0.png
1548491185593

然后我们假设需要执行一个Add操作,dictionary.Add("a","b"),其中key = "a",value = "b"

  1. 根据key的值,计算出它的 hashCode。我们假设"a"的 hash 值为 6(GetHashCode("a") = 6)。

  2. 通过对 hashCode 取余运算,计算出该 hashCode 落在哪一个 buckets 桶中。现在桶的长度(buckets.Length)为 4,那么就是6 % 4最后落在index为 2 的桶中,也就是buckets[2]

  3. 避开一种其它情况不谈,接下来它会将hashCode、key、value等信息存入entries[count]中,因为count位置是空闲的;继续count++指向下一个空闲位置。上图中第一个位置,index=0 就是空闲的,所以就存放在entries[0]的位置。

  4. Entry的下标entryIndex赋值给buckets中对应下标的bucket。步骤 3 中是存放在entries[0]的位置,所以buckets[2]=0

  5. 最后version++,集合发生了变化,所以版本需要+1。只有增加、替换和删除元素才会更新版本

    上文中的步骤 1~5 只是方便大家理解,实际上有一些偏差,后文再谈 Add 操作小节中会补充。

完成上面 Add 操作后,数据结构更新成了下图这样的形式。

34abcef5ef11fbaebb80dde118f1f386.png
1548492100757

这样是理想情况下的操作,一个 bucket 中只有一个 hashCode 没有碰撞的产生,但是实际上是会经常产生碰撞;那么 Dictionary 类中又是如何解决碰撞的呢。

我们继续执行一个Add操作,dictionary.Add("c","d"),假设GetHashCode(“c”)=6,最后6 % 4 = 2。最后桶的index也是 2,按照之前的步骤 1~3是没有问题的,执行完后数据结构如下图所示。

f692a8b18a008a7cff371992188c526e.png
1548493287583

如果继续执行步骤 4那么buckets[2] = 1,然后原来的buckets[2]=>entries[0]的关系就会丢失,这是我们不愿意看到的。现在 Entry 中的next就发挥大作用了。

如果对应的buckets[index]有其它元素已经存在,那么会执行以下两条语句,让新的entry.next指向之前的元素,让buckets[index]指向现在的新的元素,就构成了一个单链表。

entries[index].next = buckets[targetBucket];
...
buckets[targetBucket] = index;

实际上步骤 4也就是做一个这样的操作,并不会去判断是不是有其它元素,因为buckets中桶初始值就是-1,不会造成问题。

经过上面的步骤以后,数据结构就更新成了下图这个样子。

67db61fe79c171e71cdc1cfe7eb5a4e5.png
1548494357566

4. Dictionary - Find 操作

为了方便演示如何查找,我们继续 Add 一个元素dictionary.Add("e","f")GetHashCode(“e”) = 7; 7% buckets.Length=3,数据结构如下所示。

06e1931124edd96779c0a9e79d5d19e2.png
1548494583006

假设我们现在执行这样一条语句dictionary.GetValueOrDefault("a"),会执行以下步骤.

  1. 获取 key 的 hashCode,计算出所在的桶位置。我们之前提到,"a"的hashCode=6,所以最后计算出来targetBucket=2

  2. 通过buckets[2]=1找到entries[1],比较 key 的值是否相等,相等就返回entryIndex,不想等就继续entries[next]查找,直到找到 key 相等元素或者next == -1的时候。这里我们找到了key == "a"的元素,返回entryIndex=0

  3. 如果entryIndex >= 0那么返回对应的entries[entryIndex]元素,否则返回default(TValue)。这里我们直接返回entries[0].value

整个查找的过程如下图所示.

5b626392118c2e957860cd32cb5e0243.png
1548495296415

将查找的代码摘录下来,如下所示。

// 寻找Entry元素的位置
private int FindEntry(TKey key) {if( key == null) {ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);}if (buckets != null) {int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; // 获取HashCode,忽略符号位// int i = buckets[hashCode % buckets.Length] 找到对应桶,然后获取entry在entries中位置// i >= 0; i = entries[i].next 遍历单链表for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next) {// 找到就返回了if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;}}return -1;
}
...
internal TValue GetValueOrDefault(TKey key) {int i = FindEntry(key);// 大于等于0代表找到了元素位置,直接返回value// 否则返回该类型的默认值if (i >= 0) {return entries[i].value;}return default(TValue);
}

5. Dictionary - Remove 操作

前面已经向大家介绍了增加、查找,接下来向大家介绍 Dictionary 如何执行删除操作。我们沿用之前的 Dictionary 数据结构。

f243f824302990126e6d939f29ccbf00.png
1548494583006

删除前面步骤和查找类似,也是需要找到元素的位置,然后再进行删除的操作。

我们现在执行这样一条语句dictionary.Remove("a"),hashFunc 运算结果和上文中一致。步骤大部分与查找类似,我们直接看摘录的代码,如下所示。

public bool Remove(TKey key) {if(key == null) {ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);}if (buckets != null) {// 1. 通过key获取hashCodeint hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;// 2. 取余获取bucket位置int bucket = hashCode % buckets.Length;// last用于确定是否当前bucket的单链表中最后一个元素int last = -1;// 3. 遍历bucket对应的单链表for (int i = buckets[bucket]; i >= 0; last = i, i = entries[i].next) {if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {// 4. 找到元素后,如果last< 0,代表当前是bucket中最后一个元素,那么直接让bucket内下标赋值为 entries[i].next即可if (last < 0) {buckets[bucket] = entries[i].next;}else {// 4.1 last不小于0,代表当前元素处于bucket单链表中间位置,需要将该元素的头结点和尾节点相连起来,防止链表中断entries[last].next = entries[i].next;}// 5. 将Entry结构体内数据初始化entries[i].hashCode = -1;// 5.1 建立freeList单链表entries[i].next = freeList;entries[i].key = default(TKey);entries[i].value = default(TValue);// *6. 关键的代码,freeList等于当前的entry位置,下一次Add元素会优先Add到该位置freeList = i;freeCount++;// 7. 版本号+1version++;return true;}}}return false;
}

执行完上面代码后,数据结构就更新成了下图所示。需要注意varsion、freeList、freeCount的值都被更新了。

5f7f46ad4d5630c72f82d2904958c691.png
1548496815179

6. Dictionary - Resize 操作(扩容)

有细心的小伙伴可能看过了Add操作以后就想问了,buckets、entries不就是两个数组么,那万一数组放满了怎么办?接下来就是我所要介绍的**Resize(扩容)**这样一种操作,对我们的buckets、entries进行扩容。

6.1 扩容操作的触发条件

首先我们需要知道在什么情况下,会发生扩容操作;**第一种情况自然就是数组已经满了,没有办法继续存放新的元素。**如下图所示的情况。

7f6733fa0399ffa868ba13717dc99de1.png
1548498710430

从上文中大家都知道,Hash 运算会不可避免的产生冲突,Dictionary 中使用拉链法来解决冲突的问题,但是大家看下图中的这种情况。

c09686b303fb8d1f02a52e1fc99824f0.png
1548498901496

所有的元素都刚好落在buckets[3]上面,结果就是导致了时间复杂度 O(n),查找性能会下降;所以**第二种,Dictionary 中发生的碰撞次数太多,会严重影响性能,**也会触发扩容操作。

目前.Net Framwork 4.7 中设置的碰撞次数阈值为 100.

public const int HashCollisionThreshold = 100;

6.2 扩容操作如何进行

为了给大家演示的清楚,模拟了以下这种数据结构,大小为 2 的 Dictionary,假设碰撞的阈值为 2;现在触发 Hash 碰撞扩容。

050813b76eb1a5734879d7b6d89b38e8.png
1548499708530

开始扩容操作。

1.申请两倍于现在大小的 buckets、entries > 2.将现有的元素拷贝到新的 entries

完成上面两步操作后,新数据结构如下所示。

9ff6de307c7f9203d266db74eec65ddf.png
1548499785441

3、如果是 Hash 碰撞扩容,使用新 HashCode 函数重新计算 Hash 值

上文提到了,这是发生了 Hash 碰撞扩容,所以需要使用新的 Hash 函数计算 Hash 值。新的 Hash 函数并一定能解决碰撞的问题,有可能会更糟,像下图中一样的还是会落在同一个bucket上。

41121af1766fb98154d76b2884b5223d.png
1548500174305

4、对 entries 每个元素 bucket = newEntries[i].hashCode % newSize 确定新 buckets 位置

**5、重建 hash 链,newEntries[i].next=buckets[bucket]; buckets[bucket]=i; **

因为buckets也扩充为两倍大小了,所以需要重新确定hashCode在哪个bucket中;最后重新建立 hash 单链表.

363c7399a3dcbbe3c3aac23188f98329.png
1548500290419

这就完成了扩容的操作,如果是达到Hash 碰撞阈值触发的扩容可能扩容后结果会更差。

在 JDK 中,HashMap如果碰撞的次数太多了,那么会将单链表转换为红黑树提升查找性能。目前**.Net Framwork中还没有这样的优化,.Net Core中已经有了类似的优化,以后有时间在分享.Net Core**的一些集合实现。

每次扩容操作都需要遍历所有元素,会影响性能。所以创建 Dictionary 实例时最好设置一个预估的初始大小。

private void Resize(int newSize, bool forceNewHashCodes) {Contract.Assert(newSize >= entries.Length);// 1. 申请新的Buckets和entriesint[] newBuckets = new int[newSize];for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1;Entry[] newEntries = new Entry[newSize];// 2. 将entries内元素拷贝到新的entries总Array.Copy(entries, 0, newEntries, 0, count);// 3. 如果是Hash碰撞扩容,使用新HashCode函数重新计算Hash值if(forceNewHashCodes) {for (int i = 0; i < count; i++) {if(newEntries[i].hashCode != -1) {newEntries[i].hashCode = (comparer.GetHashCode(newEntries[i].key) & 0x7FFFFFFF);}}}// 4. 确定新的bucket位置// 5. 重建Hahs单链表for (int i = 0; i < count; i++) {if (newEntries[i].hashCode >= 0) {int bucket = newEntries[i].hashCode % newSize;newEntries[i].next = newBuckets[bucket];newBuckets[bucket] = i;}}buckets = newBuckets;entries = newEntries;
}

7. Dictionary - 再谈 Add 操作

在我们之前的Add操作步骤中,提到了这样一段话,这里提到会有一种其它的情况,那就是有元素被删除的情况。

  1. 避开一种其它情况不谈,接下来它会将hashCode、key、value等信息存入entries[count]中,因为count位置是空闲的;继续count++指向下一个空闲位置。上图中第一个位置,index=0 就是空闲的,所以就存放在entries[0]的位置。

因为count是通过自增的方式来指向entries[]下一个空闲的entry,如果有元素被删除了,那么在count之前的位置就会出现一个空闲的entry;如果不处理,会有很多空间被浪费。

这就是为什么Remove操作会记录freeList、freeCount,就是为了将删除的空间利用起来。实际上Add操作会优先使用freeList的空闲entry位置,摘录代码如下。

private void Insert(TKey key, TValue value, bool add){if( key == null ) {ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);}if (buckets == null) Initialize(0);// 通过key获取hashCodeint hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;// 计算出目标bucket下标int targetBucket = hashCode % buckets.Length;// 碰撞次数int collisionCount = 0;for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) {if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {// 如果是增加操作,遍历到了相同的元素,那么抛出异常if (add) {ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);}// 如果不是增加操作,那可能是索引赋值操作 dictionary["foo"] = "foo"// 那么赋值后版本++,退出entries[i].value = value;version++;return;}// 每遍历一个元素,都是一次碰撞collisionCount++;}int index;// 如果有被删除的元素,那么将元素放到被删除元素的空闲位置if (freeCount > 0) {index = freeList;freeList = entries[index].next;freeCount--;}else {// 如果当前entries已满,那么触发扩容if (count == entries.Length){Resize();targetBucket = hashCode % buckets.Length;}index = count;count++;}// 给entry赋值entries[index].hashCode = hashCode;entries[index].next = buckets[targetBucket];entries[index].key = key;entries[index].value = value;buckets[targetBucket] = index;// 版本号++version++;// 如果碰撞次数大于设置的最大碰撞次数,那么触发Hash碰撞扩容if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer)){comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);Resize(entries.Length, true);}
}

上面就是完整的Add代码,还是很简单的对不对?

8. Collection 版本控制

在上文中一直提到了version这个变量,在每一次新增、修改和删除操作时,都会使version++;那么这个version存在的意义是什么呢?

首先我们来看一段代码,这段代码中首先实例化了一个 Dictionary 实例,然后通过foreach遍历该实例,在foreach代码块中使用dic.Remove(kv.Key)删除元素。

dc7ed71fe45a7550a660c1895fade5ca.png
1548504444217

结果就是抛出了System.InvalidOperationException:"Collection was modified..."这样的异常,迭代过程中不允许集合出现变化。如果在 Java 中遍历直接删除元素,会出现诡异的问题,所以.Net 中就使用了version来实现版本控制。

那么如何在迭代过程中实现版本控制的呢?我们看一看源码就很清楚的知道。

9692436e42223d91bb85a0a32c5a273f.png
1548504844162

在迭代器初始化时,就会记录dictionary.version版本号,之后每一次迭代过程都会检查版本号是否一致,如果不一致将抛出异常。

这样就避免了在迭代过程中修改了集合,造成很多诡异的问题。

四、参考文献及总结

本文在编写过程中,主要参考了以下文献,在此感谢其作者在知识分享上作出的贡献!

  1. http://www.cnblogs.com/mengfanrong/p/4034950.html

  2. https://en.wikipedia.org/wiki/Hash_table

  3. https://www.cnblogs.com/wuchaodzxx/p/7396599.html

  4. https://www.cnblogs.com/liwei2222/p/8013367.html

  5. https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,fd1acf96113fbda9

笔者水平有限,如果错误欢迎各位批评指正!

参考资料

[1]

Link: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,d3599058f8d79be0

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/281516.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

猫晚流量再创记录,阿里云直播方案护航优酷2500万用户体验

2019独角兽企业重金招聘Python工程师标准>>> 对“剁手党而言&#xff0c;天猫双11早已经超越了简单的“买买买”&#xff0c;更是一场边看边玩的狂欢盛宴。今年的天猫双11狂欢夜晚会&#xff08;简称“猫晚”&#xff09;在上海举办&#xff0c;这台兼具年轻潮流与国…

python实现二叉树和它的七种遍历

介绍&#xff1a; 树是数据结构中非常重要的一种&#xff0c;主要的用途是用来提高查找效率&#xff0c;对于要重复查找的情况效果更佳&#xff0c;如二叉排序树、FP-树。另外可以用来提高编码效率&#xff0c;如哈弗曼树。 代码&#xff1a; 用python实现树的构造和几种遍历算…

.NET性能系列文章二:Newtonsoft.Json vs System.Text.Json

微软终于追上了&#xff1f;图片来自 Glenn Carstens-Peters[1]Unsplash[2]欢迎来到.NET 性能系列的另一章。这个系列的特点是对.NET 世界中许多不同的主题进行研究、基准和比较。正如标题所说的那样&#xff0c;重点在于使用最新的.NET7 的性能。你将看到哪种方法是实现特定主…

android gpu平板 推荐,性能强的不像话,最强安卓平板华为平板M6上手

原标题&#xff1a;性能强的不像话&#xff0c;最强安卓平板华为平板M6上手你为什么买平板电脑&#xff1f;当这一问题问出以后&#xff0c;许多朋友的表情都很微妙&#xff0c;随后大概率的回答则相当统一&#xff1a;"我买平板干嘛&#xff1f;"。其实得到这样一个…

企业应用“数据优先”革命的下一个主战场:安全与运营

根据IDC发布的2015年全球CIO日程预测&#xff0c;80%的CIO将提供一个实现创新和改善业务决策的新体系架构。 大数据时代&#xff0c;企业软件市场正在经历一次大迁移&#xff0c;数以十亿计的企业IT支出预算将投向“数据优先”应用&#xff0c;而不是长久以来以业务流程和工作流…

给Web开发人员的以太坊入坑指南

以太坊现在各种学习资料数不胜数&#xff0c;但由于以太坊正处于飞速发展阶段&#xff0c;有些学习资料很快就过时了。所以想找到有价值的资料无异于大海捞针。我费了很大功夫&#xff0c;才建立起对以太坊的整体认识&#xff0c;搞清楚它的工作机制。我相信很多跃跃欲试的开发…

一款简单的缩放拖拽图片控件

本文介绍一个针对 .NET 桌面应用程序的独立图片缩放拖拽显示控件 SQPhoto[1]。SQPhoto 是一个 Windows 桌面应用的组件&#xff0c;支持 .NET6 和 .NET Framework 4.6 。基于 PictureBox 的图片展示工具&#xff0c;增加了拖动和缩放功能&#xff0c;便于在某些场景下的图片展…

HTML怎么让div全透明,设置div为透明 怎样才让div里面的div不透明?

#a{ background:#FFCC33; filter:alpha(opacity:0); width: 300px; heig#a{background:#FFCC33; filter:alpha(opacity50); /*支持 IE 浏览器*/-moz-opacity:0.50; /*支持 FireFox 浏览器*/opacity:0.50; /*支持 Chrome, Opera, Safari 等浏览器*/width: 300px;height:300px;}还…

html overflow 样式,css样式之overflow-x属性样式

overflow-x是overflow子花样&#xff0c;平日也很少用的。overflow-x设置匿伏溢出过宽模式(比如过宽图片)、设置对象底部转折条等重要。overflow-x语法与根本懂得1、overflow-x可设置值overflow-x : visible | auto | hidden| scroll值与解释引见&#xff1a;visible :  不剪切…

C# WPF GridControl用法举例

概述GridControl是Dev中的表格控件&#xff0c;类似于Winfrom中的DataGridView&#xff0c;以及WPF中的DataGrid&#xff0c;但是这个控件功能比原生的功能要强大很多&#xff0c;下面用实例举例说明此控件的用法.代码前台XAML&#xff1a;<UserControl x:Class"Calibu…

js中关于Blob对象的介绍与使用

js中关于Blob对象的介绍与使用 blob对象介绍 一个 Blob对象表示一个不可变的, 原始数据的类似文件对象。Blob表示的数据不一定是一个JavaScript原生格式 blob对象本质上是js中的一个对象&#xff0c;里面可以储存大量的二进制编码格式的数据。 创建blob对象 创建blob对象本质上…

20170102-文件处理

文件处理 正常文件处理 python 文件处理 编码 f open(file"兼职白领学生空姐模特护士练习方式.txt",mode"r",encoding"utf-8")#把值附给变量f &#xff08;路径file"文件是兼职白领学生空姐模特护士练习方式.txt"&#xff0c;文本模式…

创建.NET程序Dump的几种姿势

当一个应用程序运行的有问题时&#xff0c;生成一个 Dump 文件来调试它可能会很有用。在 Windows、Linux 或 Azure 上有许多方法可以生成转储文件。Windows 平台dotnet-dump (Windows)dotnet-dump 全局工具[1]是一种收集和分析.NET 核心应用程序 Dump 的方法。安装 dotnet-dump…

自然语言处理怎么最快入门?

2019独角兽企业重金招聘Python工程师标准>>> 本文整理自知乎上的一个问答&#xff0c;分享给正在学习自然然语言处理的朋友们&#xff01; 一、自然语言处理是什么&#xff1f; 自然语言处理说白了&#xff0c;就是让机器去帮助我们完成一些语言层面的事情&#xff…

dotnet-exec 0.8.0 released

dotnet-exec 0.8.0 releasedIntrodotnet-exec 是一个 C# 程序的小工具&#xff0c;可以用来运行一些简单的 C# 程序而无需创建项目文件&#xff0c;而且可以自定义项目的入口方法&#xff0c;支持但不限于 Main 方法Install/Updatedotnet-exec 是一个 dotnet tool&#xff0c;可…

Siamese Network理解

提起siamese network一般都会引用这两篇文章&#xff1a; 《Learning a similarity metric discriminatively, with application to face verification》和《 Hamming Distance Metric Learning》。 本文主要通过论文《Learning a Similarity Metric Discriminatively, with A…

HashMap是如何工作的

2019独角兽企业重金招聘Python工程师标准>>> 1 HashMap在JAVA中的怎么工作的&#xff1f; 基于Hash的原理 2 什么是哈希&#xff1f; 最简单形式的 hash&#xff0c;是一种在对任何变量/对象的属性应用任何公式/算法后&#xff0c; 为其分配唯一代码的方法。 一个真…

如何做到十五分钟领略PowerBI的DAX精华及框架

有小伙伴问&#xff0c;如何最快速理解整套 DAX 精华体系&#xff0c;例如&#xff1a;15分钟。这虽然是个不可能完成的任务&#xff0c;但在借助脑图PPT的强大能力下&#xff0c;还是可以做到的。如果你从没接触过 DAX&#xff0c;但未来要学习 DAX&#xff0c;以下视频值得看…

springboot配置允许跨域访问

2019独角兽企业重金招聘Python工程师标准>>> 因springboot框架通常用于前后端分离项目&#xff0c;因此需配置后台允许跨域访问&#xff08;具体看注释&#xff09;&#xff0c;配置类如下&#xff0c;将该类加入工程中即可。 import org.springframework.context.a…

机器视觉传感器选型,交互作用决定取舍

目前&#xff0c;如何选择机器视觉传感器在当代的应用可谓是越来越广泛&#xff0c;如何选择机器视觉传感器是值得我们好好学习的&#xff0c;现在我们就深入了解如何选择机器视觉传感器。 相机是机器视觉系统的眼睛&#xff0c;而相机的心脏是图像传感器。传感器的选择取决于准…