0%

前言

复习下Dictionary的一些知识点。

源码地址

正文

定义


泛型的哈希表, 通过把关键码值映射到表中一个位置来访问数据,以加快查找的速度,是一种键值对关系的集合,底层为两个数组bucketsentries

成员变量


修饰符 类型 变量名 作用简述
private int数组 buckets hashkey映射数组
private Entry数组 entries 存储所有要查找的值的数组
private int count 容器内当前的元素数量
private int version 数据版本,用于校验对数组的操作是否合法
private int freeList 当前entries中指向的空节点
private int freeCount 空节点数量
private IEqualityComparer comparer 用于查找时比较hashkey
private KeyCollection keys 键集合,惰性生成
private ValueCollection values 值集合,惰性生成
private Object _syncRoot 多线程下使用的同步对象,不过现在基本废弃了, 多线程建议使用ConcurrentDictionary

Entry结构


1
2
3
4
5
6
private struct Entry {
public int hashCode; // Lower 31 bits of hash code, -1 if unused
public int next; // Index of next entry, -1 if last
public TKey key; // Key of entry
public TValue value; // Value of entry
}

虽然数据都是存储在一个名为entries的数组中,但是entry之间的逻辑结构实际上为链表结构, 即相同bucket映射位置的entry会通过next“指针”串联起来。至于为什么不直接用链表,个人猜测是使用数组能够保证一段内存的连续性,而如果使用单一指针指向的不同内存块来构建链表,内存数据会变得更碎片化,更容易cache miss,从而导致访问效率变低。

构造函数


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
        public Dictionary(): this(0, null) {}

public Dictionary(int capacity): this(capacity, null) {}

public Dictionary(IEqualityComparer<TKey> comparer): this(0, comparer) {}

public Dictionary(int capacity, IEqualityComparer<TKey> comparer) {
if (capacity < 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
if (capacity > 0) Initialize(capacity);
this.comparer = comparer ?? EqualityComparer<TKey>.Default;

#if FEATURE_CORECLR
if (HashHelpers.s_UseRandomizedStringHashing && comparer == EqualityComparer<string>.Default)
{
this.comparer = (IEqualityComparer<TKey>) NonRandomizedStringEqualityComparer.Default;
}
#endif // FEATURE_CORECLR
}

public Dictionary(IDictionary<TKey,TValue> dictionary): this(dictionary, null) {}

public Dictionary(IDictionary<TKey,TValue> dictionary, IEqualityComparer<TKey> comparer):
this(dictionary != null? dictionary.Count: 0, comparer) {

if( dictionary == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.dictionary);
}

foreach (KeyValuePair<TKey,TValue> pair in dictionary) {
Add(pair.Key, pair.Value);
}
}

可以看到关键的构造函数主要为以下两个函数,一个预分配容量,一个以另一个键值对容器初始化。

1
2
3
4
public Dictionary(int capacity, IEqualityComparer<TKey> comparer)

public Dictionary(IDictionary<TKey,TValue> dictionary,IEqualityComparer<TKey> comparer)

预分配构造函数将bucketentries两个数组以capacity大小初始化,而以容器作为参数的构造函数则不停迭代,将参数容器的元素依次Add到新创建的Dictionary当中。comparer为hashkey的查找比较函数,传入null即使用默认比较函数EqualityComparer<TKey>.Default

扩容机制


扩容的时机与List雷同,即在Insert插入新元素时如果容器元素数量达到上限触发,关键代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private void Resize() {
Resize(HashHelpers.ExpandPrime(count), false);
}

private void Resize(int newSize, bool forceNewHashCodes) {
Contract.Assert(newSize >= entries.Length);
int[] newBuckets = new int[newSize];
for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1;
Entry[] newEntries = new Entry[newSize];
Array.Copy(entries, 0, newEntries, 0, count);
if(forceNewHashCodes) {
for (int i = 0; i < count; i++) {
if(newEntries[i].hashCode != -1) {
newEntries[i].hashCode = (comparer.GetHashCode(newEntries[i].key) & 0x7FFFFFFF);
}
}
}
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;
}

其中扩容调用的是Resize()函数,在通过ExpandPrime函数确定新容量大小后,则会分配重新分配buckets和entries的数组大小,并将数据Copy过去。

减少哈希冲突的优化


在扩容时优化算法来降低哈希冲突: 分配的新容器大小为与 2倍于目前容器容量大小 最接近的最小素数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static readonly int[] primes = {
3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369};

/*......Other Code......*/

// Returns size of hashtable to grow to.
public static int ExpandPrime(int oldSize)
{
int newSize = 2 * oldSize;

// Allow the hashtables to grow to maximum possible size (~2G elements) before encoutering capacity overflow.
// Note that this check works even when _items.Length overflowed thanks to the (uint) cast
if ((uint)newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize)
{
Contract.Assert( MaxPrimeArrayLength == GetPrime(MaxPrimeArrayLength), "Invalid MaxPrimeArrayLength");
return MaxPrimeArrayLength;
}

return GetPrime(newSize);
}

public static int GetPrime(int min)
{
if (min < 0)
throw new ArgumentException(Environment.GetResourceString("Arg_HTCapacityOverflow"));
Contract.EndContractBlock();

for (int i = 0; i < primes.Length; i++)
{
int prime = primes[i];
if (prime >= min) return prime;
}

//outside of our predefined table.
//compute the hard way.
for (int i = (min | 1); i < Int32.MaxValue;i+=2)
{
if (IsPrime(i) && ((i - 1) % Hashtable.HashPrime != 0))
return i;
}
return min;
}

为什么每次扩容的容量大小都为素数?

关于这个问题可以参考stackoverflow上的讨论以及这篇博客

结论就是: 在大多数情况下,对于一组数列a(n),如果它数列长度的公因数数量越少,那么该数列中的数字经散列后形成的新数字在哈希表上分布就越均匀,即发生哈希冲突的概率就越小。而素数的公因数为1和它本身,公因数最少,所以每次扩容都尽可能地选择素数作为新容量大小。

哈希冲突过多的优化

Insert时发生了过多哈希冲突(超过100次)则会调用Resize来重新获取所有hashcode

1
2
3
4
5
if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer)) 
{
comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
Resize(entries.Length, true);
}

关键函数


主要就查找、添加、删除几个操作相关的函数,并辅以图像说明下。

假设先初始化了一个容量大小为5的哈希表

1
var dict = new Dictionary<string,int>(5);

那么它的初始结构如下图所示

structure

插入

不管是Add还是操作符[]赋值, 最终都会调用一个Insert函数,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
        private void Insert(TKey key, TValue value, bool add) {

if( key == null ) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}

if (buckets == null) Initialize(0);
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int targetBucket = hashCode % buckets.Length;

#if FEATURE_RANDOMIZED_STRING_HASHING
int collisionCount = 0;
#endif

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);
}
entries[i].value = value;
version++;
return;
}

#if FEATURE_RANDOMIZED_STRING_HASHING
collisionCount++;
#endif
}
int index;
if (freeCount > 0) {
index = freeList;
freeList = entries[index].next;
freeCount--;
}
else {
if (count == entries.Length)
{
Resize();
targetBucket = hashCode % buckets.Length;
}
index = count;
count++;
}

entries[index].hashCode = hashCode;
entries[index].next = buckets[targetBucket];
entries[index].key = key;
entries[index].value = value;
buckets[targetBucket] = index;
version++;

#if FEATURE_RANDOMIZED_STRING_HASHING

#if FEATURE_CORECLR
// In case we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing
// in this case will be EqualityComparer<string>.Default.
// Note, randomized string hashing is turned on by default on coreclr so EqualityComparer<string>.Default will
// be using randomized string hashing

if (collisionCount > HashHelpers.HashCollisionThreshold && comparer == NonRandomizedStringEqualityComparer.Default)
{
comparer = (IEqualityComparer<TKey>) EqualityComparer<string>.Default;
Resize(entries.Length, true);
}
#else
if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer))
{
comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
Resize(entries.Length, true);
}
#endif // FEATURE_CORECLR
#endif

}

可以看到如果之前并没有预分配容量(bucketsnull),那么会先通过Intialize函数给bucketsentries数组分配内存,之后就是调用comapre.GetHashCode分配hashcode,注意这里得到的hashcode要和最大正整数数0x7FFFFFFF进行位且运算来去掉符号位(不能是负数),这样之后通过和buckets的容量大小length进行模除运算就能得到hashcodebuckets数组上映射的位置。之后就是分为两种情况:

  1. 更新操作, 如果相同bucket位置的entry存在,并且entryhashcode与计算后的hashcode一致,而且也不是Add操作,那么就更新entry存储的值,否则就会抛出重复添加元素的异常。
  2. 插入操作, 会先检查有没有空的entry节点(FreeCount > 0 ,Remove后留下的所有空节点会串连成一个缓存链表), 如果有空节点那么会优先使用空节点freeList(freeList指向的其实就是缓存链表的头节点),然后使用头插法将其插入对应bucket位置指向的链表,否则就将count加1,然后选用entries[count]位置的节点作为新数据存放节点。之后就是上文提到的判断哈希冲突是否过多从而进行重新哈希的逻辑。

接下来画图举例说明, 假如我们对上文的dict指向多次插入操作,而且它们的hashcode都是21,那么在bucket上映射的位置为1

1
2
3
dict.Add("A", 1);
dict.Add("B", 2);
dict.Add("C", 3);

执行上述操作后,结构如下图

2

删除

删除的逻辑比较简单, 先将key转换为hashcode,然后再转换为buckets中的位置,之后就是遍历bucket位置指向的链表来比较hashcode,如果相等则清除对应位置的entry数据,并将其放入到freelist链表上缓存起来以方便下次Insert操作复用。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public bool Remove(TKey key) {
if(key == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}

if (buckets != null) {
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int bucket = hashCode % buckets.Length;
int last = -1;
for (int i = buckets[bucket]; i >= 0; last = i, i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
if (last < 0) {
buckets[bucket] = entries[i].next;
}
else {
entries[last].next = entries[i].next;
}
entries[i].hashCode = -1;
entries[i].next = freeList;
entries[i].key = default(TKey);
entries[i].value = default(TValue);
freeList = i;
freeCount++;
version++;
return true;
}
}
}
return false;
}

紧接着上文操作,假设我们执行下列删除操作。

1
2
dict.Remove("A");
dict.Remove("B");

那么结构变化如下图

删除A之后

3

删除B之后

4

如果之后又调用了

1
dict.Add("D", 4)

那么会将freeList指向的列表头节点复用起来,如果DHashCode模除不为1,而是在其他位置,比如假设D的HashCode18,即bucket位置为3。那么操作后的结构如下图

5

查找

函数GetOrDefaultContainsTryGetValue等内部都会用到FindEntry来查找对应元素。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private int FindEntry(TKey key) {
if( key == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
}

if (buckets != null) {
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
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;
}

核心逻辑就是通过对应的bucket位置找到链表,遍历比较hashcode即可。
所有操作大部分情况下时间复杂度皆为O(1)

注意ContainsValue未使用映射关系查找,而是从头遍历,因此时间复杂度为O(n)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public bool ContainsValue(TValue value) {
if (value == null) {
for (int i = 0; i < count; i++) {
if (entries[i].hashCode >= 0 && entries[i].value == null) return true;
}
}
else {
EqualityComparer<TValue> c = EqualityComparer<TValue>.Default;
for (int i = 0; i < count; i++) {
if (entries[i].hashCode >= 0 && c.Equals(entries[i].value, value)) return true;
}
}
return false;
}

使用总结


  1. foreach循环内不要去增删元素。
  2. 如果要批量删除元素,用一个List存储好要删除的key,然后在List的循环中执行Remove操作。
  3. List类似的扩容机制,所以在确定容量的情况下尽可能地预分配好空间。
  4. 避免频繁增删元素,频繁的增删操作会导致entries的链表逻辑结构顺序变化,foreach的顺序也可能会因此不同。在这种情况下建议换用SortedDictionary

前言

复习下List的一些知识点。

源码地址

正文

定义


可通过下标索引访问的强类型列表,连续性存储结构,底层为数组,可动态扩容。

成员变量


修饰符 类型 变量名 作用简述
private 泛型数组 items 存储目标数据类型的数组
private int size 当前数组内的条目数量
private int _version 数据版本,用于校验对数组的操作是否合法
private Object syncRoot 多线程下使用的同步对象,不过现在基本废弃了,多线程下最好用ConcurrentBag
private static 泛型数组 _emptyArray 默认构造函数内_items初始化指向的只读数组

构造函数


1. 默认构造函数

不会分配新的数组内存,而是指向一个默认的全局只读空数组,之后进行元素添加操作时才会触发扩容机制。

2. 带容量的构造函数

会预分配参数capacity指定大小的数组。

3. 使用一组数据的构造函数

如果传入的本身是一个容器ICollection<T>, 那么会使用这个容器的大小来初始化数组, 然后将容器数据元素拷贝到分配好的数组中去; 如果仅仅是IEnumrable<T>, 那么使用迭代器不停地将一个个元素Add,这其中会触发多次扩容。因此如果可以的话,尽量传入容器来初始化List

扩容机制

在向List中添加新元素时(调用AddInsertAddRange等),会先调用一次EnsureCapacity来保证数组的容量足够容纳新的元素。如果是空数组,那么会以初始为4的容量来初始化,否则的话就会分配一个当前数组的长度2倍的新数组,然后再将当前数组的所有元素拷贝过去,时间复杂度为O(n),因此在编写代码时,如果能预估List使用容量的话,那么最好是提前预分配容量,减少频繁扩容带来的开销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
   private void EnsureCapacity(int min) {
if (_items.Length < min) {
int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;
if ((uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength;
if (newCapacity < min) newCapacity = min;
Capacity = newCapacity;
}
}

public int Capacity {
get {
Contract.Ensures(Contract.Result<int>() >= 0);
return _items.Length;
}
set {
if (value < _size) {
ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.value, ExceptionResource.ArgumentOutOfRange_SmallCapacity);
}
Contract.EndContractBlock();

if (value != _items.Length) {
if (value > 0) {
T[] newItems = new T[value];
if (_size > 0) {
Array.Copy(_items, 0, newItems, 0, _size);
}
_items = newItems;
}
else {
_items = _emptyArray;
}
}
}
}

关键函数


Insert

先检测是否需要扩容,然后朝指定下标位置插入一个元素,会使用Array.Copy将目标位置之后的元素往后挪一位,因此不建议频繁在列表前段进行插入操作。

1
2
3
4
5
6
7
8
public Insert(int index, Object item)
{
\*....*\
if (index < _size) {
Array.Copy(_items, index, _items, index + 1, _size - index);
}
\*....*\
}

InsertRange

先检测扩容,然后尝试转换为ICollection<T>,如果不是ICollection<T>(即IEnumrable<T>),那么会使用迭代器循环不停地去调用Insert(数量很大的话会有一定性能开销)。
之后与Insert类似,将目标位置后元素往后挪,将插入的元素拷贝过去,拷贝时会生成一个临时数组来存放要拷贝的元素( 这里有一个优化点:如果被拷贝元素的容器和目标容器本来就是同一个,那么不需要分配新的临时数组,而是复用当前操作的数组)。

1
2
3
4
5
6
7
8
9
10
11
12
   // If we're inserting a List into itself, we want to be able to deal with that.
if (this == c) {
// Copy first part of _items to insert location
Array.Copy(_items, 0, _items, index, index);
// Copy last part of _items back to inserted location
Array.Copy(_items, index + count, _items, index*2, _size-index);
}
else {
T[] itemsToInsert = new T[count];
c.CopyTo(itemsToInsert, 0);
itemsToInsert.CopyTo(_items, index);
}

Add

先检测扩容,然后在当前列表末尾增添一个元素。还有一个非泛型的版本,传入的元素为Object,会尝试使用强制转换来添加新元素。

1
2
3
4
5
6
try { 
Add((T) item);
}
catch (InvalidCastException) {
ThrowHelper.ThrowWrongValueTypeArgumentException(item, typeof(T));
}

AddRange

直接调用的InsertRange,传入的index为当前数组长度,即在数组末尾插入一组新的元素。

RemoveAt

将指定下标位置的元素移除,然后调用Array.Copy将后续元素往前挪

Remove

遍历List找到该元素的下标,如果存在则调用```RemoveAt``。

RemoveRange

从指定index位置开始移除count个元素,核心代码如下.

1
2
3
4
5
6
7
8
9
if (count > 0) {
int i = _size;
_size -= count;
if (index < _size) {
Array.Copy(_items, index + count, _items, index, _size - index);
}
Array.Clear(_items, _size, count);
_version++;
}

RemoveAll

这里算法有点意思,用了双指针的思路,先找到第一个需要被移除的元素下标,这里称为freeindex, 然后开始遍历循环这个List,每次循环都找到与之对应的不需要移除的第一个元素,这里称为current,然后将current元素移动到freeindex元素的位置, current走到末尾循环结束,算法时间复杂度为O(n), 之后就是将多余的元素Clear掉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public int RemoveAll(Predicate<T> match) {
if( match == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
}
Contract.Ensures(Contract.Result<int>() >= 0);
Contract.Ensures(Contract.Result<int>() <= Contract.OldValue(Count));
Contract.EndContractBlock();

int freeIndex = 0; // the first free slot in items array

// Find the first item which needs to be removed.
while( freeIndex < _size && !match(_items[freeIndex])) freeIndex++;
if( freeIndex >= _size) return 0;

int current = freeIndex + 1;
while( current < _size) {
// Find the first item which needs to be kept.
while( current < _size && match(_items[current])) current++;

if( current < _size) {
// copy item to the free slot.
_items[freeIndex++] = _items[current++];
}
}

Array.Clear(_items, freeIndex, _size - freeIndex);
int result = _size - freeIndex;
_size = freeIndex;
_version++;
return result;
}

IndexOf

找到目标元素在List中的第一次出现的下标位置, 有三个重载版本, 默认是整个数组(Range[0, array.length]),如果传入index的话则是Range[index, array.length],额外传入第三个参数count的话,则是从下标index开始后的count的元素(Range[index, index + count])

LastIndexOf

找到目标元素在List中的最后一次出现的下标位置,逻辑和参数与IndexOf类似

Find

遍历List, 返回第一个满足条件函数的元素。

FindIndex

Find类似,返回第一个匹配的元素的下标, 可以向IndexOf一样传入额外两个参数indexcount指定查找范围。

FindLast

遍历List, 返回最后一个满足条件函数的元素,注意既然是最后一个元素,那么可以从数组末尾向首部查找。

FindLastIndex

返回最后一个匹配的元素的下标

Contains

遍历整个数组,判断某个元素是否在List

Exists

FindIndex的二次包装,判断List是否存在符合条件函数的元素。

Sort

排序,具体的排序算法这里不赘述。

Reverse

颠倒整个数组,两个重载版本,一个范围是整个数组,另一个版本的范围为Range[index, index + count]

TrueForAll

顾名思义,判断所有元素是否都符合条件函数。

Clear

清空整个List

TrimExcess

将数组容量(Capacity)裁剪到与当前元素数量(Count)一致,在数量达到容量90%的情况下不会生效。

        public void TrimExcess() {
            int threshold = (int)(((double)_items.Length) * 0.9);             
            if( _size < threshold ) {
                Capacity = _size;                
            }
        }

使用总结


  1. 尽量避免频繁在头部增删元素。
  2. 不要在正向循环中去增删元素,否则会导致迭代的下标错位和越界。
  3. new一个新的List时,预估好容量,避免多次扩容的开销。另外使用一组数据元素初始化List时,避免使用IEnumrable<T>,尽可能地使用ICollection<T>
  4. 在确定List不会再扩容的情况下,可以使用TrimExcess裁剪一部分数组空位置

前言

去年搭的用于Svn、Jenkins以及个人博客用的服务器快到期了,本来之前用学生身份一百多元买的,一看续费要1000多就想着能不能找点便宜的方式。
正好看到最近新购服务器的活动还在进行,新服务器3年只需要264¥,就想着能不能把旧服务器备份下然后把数据全部转到新服务器,变相地“便宜续一波”,最后操作下来是可行的,记录下。

步骤

1.进入旧服务器操作面板,制作新的个人镜像。

pic1
pic2

2.买个新服务器,选择的区域一定要是和旧服务器在同一地域(比如旧服务器是上海那么新服务器也选上海的),因为不同地域的镜像是无法跨域使用。
pic3

3.在新服务器操作面板,使用之前创建的旧服务器镜像重装系统。
pic4

4.关掉旧服务器,在域名解析界面把ip定向到新的服务器公网ip上,另外记得在新服务器的防火墙界面把相关的端口也开放下。
pic5

5.登录新服务器测试了相关的功能,除了像jenkins、unity这种需要重新登下账号外,无其他问题。

前言

摸索了大半天终于弄好了。

有关github webhookjenkins的配置可以参考这篇文章

踩坑

SSL验证的问题


使用的https来传递信息触发构建事件,结果push上去后github webhook抛了个错误信息。

错误

原因是没有配置正确的SSL证书验证,于是从对应的云服务商(我用的腾讯云)那下载了之前申请好的SSL证书,下载格式选择为jks

证书下载

将下载好的证书放到jenkins的根目录,然后在配置文件jenkins.xml中的arguments节点项里添加如下参数。

1
--httpsKeyStore="%JENKINS_HOME%\证书名.jks" --httpsKeyStorePassword="证书校验密码"

argument

之后重启jenkins,然后让repo对应的webhook页面redelivery一下就行。

redelivery

批处理脚本命令无法执行的问题


构建时可以让jenkins调用本地bat脚本

build_bat

逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
echo  update blog
cd ./repo

if exist node_modules\ (
echo skip install npm_module
) else (
call npm install
)

call hexo clean

call hexo g

但是会报错找不到npmhexo等可执行程序。

原因是jenkins的运行环境是一个独立的沙盒环境,不会用到Windows系统自带的环境变量,所以这些可执行文件的PATH路径需要添加在全局配置当中。该配置在.jenkins文件夹下的config.xml文件里。

config

也可以通过jenkins 图形化控制台的Dashboard -> ConfigureSystem -> Global properties-> Environment variables配置。

gui_config

前言

《代码整洁之道》的第二章读书笔记整理,示例代码为C#, 逻辑大部分仿照原文使用的Java代码,加入了一些自己思考后的示例。

正文

1. 名副其实


要让别人一看变量、函数或类的名称就该知道它是啥、它做了什么事、它应该怎么用。

假设有一个与时间相关的变量

1
int d; 

如果直接这么命名,第一次看到这变量的人不容易理解,更好的命名方式如下:

1
2
3
int elapsedTimeInDays;
int daysSinceCreation;
int fileAgeInDays;

这样变量所代表的含义一目了然。

以下代码同理,函数名含义模糊不清,另外列表list1 、常量4、迭代变量x等的意义未知。

1
2
3
4
5
6
7
8
9
10
public List<int[]> getThem()
{
List<int[]> list1 = new List<int[]>();
foreach(int[] x in theList)
{
if(x[0] == 4)
list1.Add(cell);
}
return list1;
}

以下为重构后的代码,使用一个类来封装相关的变量与逻辑。

1
2
3
4
5
6
7
8
9
10
public List<Cell> getFlaggedCells()
{
var flaggedCells = new List<Cell>();
foreach(var cell in gameboard)
{
if(cell.isFlagged())
flaggedCells.Add(cell);
}
return flaggedCells;
}

2. 避免误导


  • 避免使用与本意相悖的单词,比如 UniX平台下的 hpaixsco 等专有名称不应作为变量名。
  • 避免使用形如accountList 的名称,除非该变量本身就是List类型,考虑到未来该容器的类型可能会发生改变(比如变成了DictionaryHashSet之类的),这个时候accountList的命名就会引起其他开发人员错误的认知,他第一次看到该变量被调用时会认为这是一个List
  • 更好的命名是用 accountGroupbunchofAccounts,甚至直接用accounts都行。

3. 命名要有区分


  • 在同一作用范围内的命名不能太相似。比如

    1
    2
    3
    4
    5
    public static void copyChars(char[] a1, char[] a2)
    {
    for(int i = 0; i < a1.Length; ++i)
    a2[i] = a1[i];
    }

    上述代码的参数名a1a2完全可以用sourcedestination替代,如此的话函数使用者就知道他是将第一个数组的数据拷贝到第二个数组当中。

  • 避免意义相近的名称,如ProductDataProuctInfo这种。

4. 可读性良好的命名


  • 使用英文单词对变量进行命名,不要用各种简写或者拼音,以下为不好的命名示例。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class DtaRcrd102
    {
    private DateTime genymdhms;
    private DateTime modymdhms;
    private readonly string pszqint = "102";
    };

    class Wuqi
    {
    private float chongnengshijian; //充能时间
    private int danjiarongliang; //弹夹容量
    }
  • 纠正过后的良好命名如下。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class CustomerRecord
    {
    private DateTime generationTime;
    private DateTime modificationTime;
    private readonly string recordId = "102";
    };

    class Weapon
    {
    private float chargeTime;
    private int clipSize;
    }

5. 可搜索命名


尽可能地使用常量定义和单词变量去替代纯数字或者纯字符串,这样其他人想修改值的时候只需要搜索到定义的地方修改就行,不用一个个去找(比如下面这段代码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Player
{
private List<Weapon> m_Weapons = new List<Weapon>();

public void AddWeapon(Weapon newWeapon)
{
if(m_Weapons.Count >= 5)
return;
m_Weapons.Add(newWeapon);
}

public Weapon GetWeaponAt(int idx)
{
if(idx < 0 || idx >= 5)
return;
return m_Weapons[idx];
}
}

可以优化为更容易搜索和维护的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Player
{
private List<Weapon> m_Weapons = new List<Weapon>();

private const int MAX_WEAPON_NUM = 5;

public void AddWeapon(Weapon newWeapon)
{
if(m_Weapons.Count >= MAX_WEAPON_NUM)
return;
m_Weapons.Add(newWeapon);
}

public Weapon GetWeaponAt(int idx)
{
if(idx < 0 || idx >= MAX_WEAPON_NUM)
return;
return m_Weapons[idx];
}
}

这样当后续想要修改最大武器数量只需要修改MAX_WEAPON_NUM处即可,也便于维护者按关键字max之类的搜索。

6. 避免使用多余的编码


  • 6.1 匈牙利语标记法
    • 即在变量名前添加类型字符。比如iCount表示int类型的Count变量、pUnit表示指向一个Unit类的指针变量等,详细参见百科上的说明
    • 起源是在Windows的C语言API的时代,编译器不做类型检查,程序员需要这种方法来区分句柄、长指针或者void指针等变量的类型。现代高级语言一般不需要这样来做,作者也不建议这么来命名了。
    • 个人认为对于LuaPython 这种非强类型的脚本语言来说,这种命名还是可以采取的。
  • 6.2 成员前缀
    • 在类的成员变量(member variables)前添加 m_ 后缀, 如上文的Player 里定义的m_Weapons
    • 原文作者不建议使用成员前缀。这一点个人不是很赞同, 对于C#来说,在类的成员变量和索引器较多的情况下,还是应该引入前缀来区分。简而言之,视使用环境而定,我个人偏好使用成员前缀。
  • 6.3 接口命名
    • 作者也不建议在接口命名前添加大写字母I,比如比起IShapreFactory作者更青睐ShapeFactory。这一点个人也不是很赞同,私以为用这种方式区分好接口类和普通类还是有必要的。
  • 6.4 中文(个人补充)
    • 一般来说命名也不要用中文,不同机器如果默认编码不同会经常导致中文乱码。

7. 避免思维映射


不要让代码阅读者在脑中把你的命名翻译为他们熟悉的名称,通俗点来说就是不要让命名产生不必要的联想。

8. 命名的词性选择


  • 类名、对象名应使用名词或者名词短语,比如 CustomerWikiPageAccount
  • 方法、函数使用动词或者动词短语,比如postPaymentdeletePageSave等。

9. 不要扮可爱

不要取一些自己认为很cool的名字,不要抖机灵。比如一个简单的DeleteItem函数不要写成WipeOutTheUseless之类的。

10. 命名概念明确单一


  • 不要混用相同概念的词,每个抽象概念选一个词,并且一以贯之。比如不要在项目中同时有xxxManagerxxxControllerxxxDriver这类相同意义的名词,选其中一种就行了。
  • 不要在不合适的情况下使用双关语。比如 Add 函数这种,在一个类当中它可能表示将两个值相加,也可能表示将一条数据条目添加到指定集合当中,如果出现了这种情况可以考虑换成Append或者Insert

11. 使用项目开发人员都熟悉的名称


  • 使用解决方案领域的名称。
  • 使用源自所涉问题领域的名称。

12. 符合语境


  • 添加有意义的语境

    • 一个函数中出现了state变量,如果代码阅读者最开始理解不了这个变量的意义,他就得去定向到这个state的定义处,结合定义所在行的上下文来理解,导致花费更多的时间来熟悉代码。
    • 比如在一个名为Player的类里,state可能是指AnimationState,也可能是GameState,那么最好按照逻辑功能将使用的变量命名为对应的animStatecurrentGameState之类的,以此避免他人花费更多的时间去理解上下文。
  • 避免无意义的语境

    • 避免在某些类前面加上应用名称的前缀,比如一个加油站豪华版应用(Gas Station Deluxe)中的每个类都加了GSD前缀,类似GSDAccountAddress这种,本来只是一个表示邮件地址的类,加了前缀后会让其他人误以为是用来什么特殊处理逻辑。
    • 这个问题个人也在实际开发过程中遇见过:在前东家游戏项目内阅读相关的C++源码时发现了很多带ES前缀的类(比如ESArrayESString之类的),实际代码逻辑是一些针对项目本身需求优化过的STL容器,但一直很疑惑ES是啥特殊含义,还跑去问了上级老大,结果被告知ES是公司的英文简写。 这种容易造成误解的情况还是尽可能避免,实在需要的话,可以用命名空间包一层来处理。

后记

整个第二章读下来,发现大部分坑已经在以前做项目的过程中趟过了,不过还是学到了新的东西,可见理论和实践还是应该相辅相成才能对个人给予最大的成长。

概念

  • 寻找计算两点之间的最短路径,基本思想源于BFS,是Dijkstra寻路的进阶版,相比DijkStra引入了目标点到终点的预估消耗值。

算法伪逻辑代码

算法伪码图
其中OpenList可以考虑使用最小二叉堆或者优先级队列,这样每次取最小值时间复杂度为O(1)。


之前在旧博客的实现


Unity逻辑实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
using System;
using System.Collections;
using System.Collections.Generic;
using KeyBoard.Util;
using UnityEngine.Profiling;

namespace KeyBoard.Logic.Map
{
public abstract class IAstarNode<T> : IComparable
{
public float G_Value { get; set; }

public float H_Value { get; protected set; }

public abstract bool IsWalkable {get;}

public float F_Value => G_Value + H_Value;

public IAstarNode<T> Parent { get; set; }

public T NodeData { get; private set; }

public IAstarNode(T data)
{
NodeData = data;
}

public abstract float GetDistance(IAstarNode<T> target);


public static bool TryFindPath<Node>(List<Node> nodes, Node start, Node end, int Width, int Length, out List<Node> path) where Node:IAstarNode<T>
{
#if UNITY_EDITOR
Profiler.BeginSample("CustomAstar:" + typeof(Node));
// var watch = System.Diagnostics.Stopwatch.StartNew();
#endif
path = null;
if(!nodes.Contains(start) || !nodes.Contains(end))
return false;
if(Width < 0 || Length < 0)
return false;
var openNodes = new SortedSet<Node>();
HashSet<Node> closeNodes = new HashSet<Node>();
path = new List<Node>();
openNodes.Add(start);
while(openNodes.Count > 0)
{
var curNode = openNodes.Min;
openNodes.Remove(curNode);
closeNodes.Add(curNode);

if(curNode == end) break;

var idx = nodes.IndexOf(curNode);
var neighbor = UIUtil.GetNeighbors(idx, (int)Width, (int)Length);
SearchNeighbor(curNode, nodes.SafeGet(neighbor.left), end, openNodes, closeNodes);
SearchNeighbor(curNode, nodes.SafeGet(neighbor.right), end, openNodes, closeNodes);
SearchNeighbor(curNode, nodes.SafeGet(neighbor.up), end, openNodes, closeNodes);
SearchNeighbor(curNode, nodes.SafeGet(neighbor.down), end, openNodes, closeNodes);
}

while(end != null)
{
path.Add(end);
end = end.Parent as Node;
}
#if UNITY_EDITOR
// watch.Stop();
// UnityEngine.Debug.Log($"自定义寻路用时{watch.ElapsedMilliseconds}ms");
Profiler.EndSample();
#endif
return path.Count > 0;
}

private static void SearchNeighbor<Node>(Node curNode, Node neighborNode, Node end, ISet<Node> openNodes, ISet<Node> closeNodes) where Node:IAstarNode<T>
{
if(neighborNode == null || !neighborNode.IsWalkable || closeNodes.Contains(neighborNode))
return;
float newCost_G = curNode.G_Value + curNode.GetDistance(neighborNode);
if(newCost_G < neighborNode.G_Value || !openNodes.Contains(neighborNode))
{
neighborNode.G_Value = newCost_G;
neighborNode.H_Value = neighborNode.GetDistance(end);
neighborNode.Parent = curNode;
if(!openNodes.Contains(neighborNode))
{
openNodes.Add(neighborNode);
}
}
}


public int Compare(IAstarNode<T> x, IAstarNode<T> y)
{
if(x.H_Value < y.H_Value)
return -1;
if(x.H_Value > y.H_Value)
return 1;
return 0;
}

public int Compare(object x, object y)
{
var a = x as IAstarNode<T>;
var b = y as IAstarNode<T>;
if(a != null && b != null)
return a.CompareTo(b);
return 0;
}

public int CompareTo(object obj)
{
if (obj == null) return 1;

var other = obj as IAstarNode<T>;
if (other != null)
return F_Value.CompareTo(other.F_Value);
else
throw new ArgumentException("Object is not an IAstarNode");
}
}
}


信息就是位 + 上下文

  • 系统中所有的信息——包括磁盘文件、内存中的程序、内存中存放的用户数据以及网络上传送的数据,都是由一串比特表示的。区分不同数据对象的唯一方法是我们读到这些数据对象时的上下文
  • 大部分的现代计算机系统都使用 ASCII 标准来表示文本字符,这种方式实际上就是用一个唯一的单字节大小的✦整数值✦来表示每个字符

GCC的编译过程

  • 编译分为四个阶段
    1. 预处理阶段。读取预处理包含的文件数据,将内容插入原始的C程序文件,生成*.i文件。
    2. 编译阶段。将*.i文件转换为*.s文件并转换为汇编语言。
    3. 汇编阶段。将汇编语言转换生成为机器指令,并将结果保存在*.o文件当中。
    4. 链接阶段。将其他*.o文件合并为可执行程序

编译过程示例图


文本测试

Hexo 是一个快速、简洁且高效的博客框架。Hexo 使用 Markdown(或其他渲染引擎)解析文章,在几秒内,即可利用靓丽的主题生成静态网页。

代码测试

1
2
3
4

var dateTime = DateTime.Now;
Console.WriteLine(dateTime);

1
2
3
4

local t = os.time();
print(t)