gopl-zh.github.com/ch13/ch13-03.md

5.2 KiB
Raw Blame History

13.3. 示例: 深度相等判斷

來自reflect包的DeepEqual函數可以對兩個值進行深度相等判斷。DeepEqual函數使用內建的==比較操作符對基礎類型進行相等判斷,對於複合類型則遞歸該變量的每個基礎類型然後做類似的比較判斷。因爲它可以工作在任意的類型上,甚至對於一些不支持==操作運算符的類型也可以工作因此在一些測試代碼中廣泛地使用該函數。比如下面的代碼是用DeepEqual函數比較兩個字符串數組是否相等。

func TestSplit(t *testing.T) {
	got := strings.Split("a:b:c", ":")
	want := []string{"a", "b", "c"};
	if !reflect.DeepEqual(got, want) { /* ... */ }
}

盡管DeepEqual函數很方便而且可以支持任意的數據類型但是它也有不足之處。例如它將一個nil值的map和非nil值但是空的map視作不相等同樣nil值的slice 和非nil但是空的slice也視作不相等。

var a, b []string = nil, []string{}
fmt.Println(reflect.DeepEqual(a, b)) // "false"

var c, d map[string]int = nil, make(map[string]int)
fmt.Println(reflect.DeepEqual(c, d)) // "false"

我們希望在這里實現一個自己的Equal函數用於比較類型的值。和DeepEqual函數類似的地方是它也是基於slice和map的每個元素進行遞歸比較不同之處是它將nil值的slicemap類似和非nil值但是空的slice視作相等的值。基礎部分的比較可以基於reflect包完成和12.3章的Display函數的實現方法類似。同樣我們也定義了一個內部函數equal用於內部的遞歸比較。讀者目前不用關心seen參數的具體含義。對於每一對需要比較的x和yequal函數首先檢測它們是否都有效或都無效然後檢測它們是否是相同的類型。剩下的部分是一個鉅大的switch分支用於相同基礎類型的元素比較。因爲頁面空間的限製我們省略了一些相似的分支。

gopl.io/ch13/equal
func equal(x, y reflect.Value, seen map[comparison]bool) bool {
	if !x.IsValid() || !y.IsValid() {
		return x.IsValid() == y.IsValid()
	}
	if x.Type() != y.Type() {
		return false
	}

	// ...cycle check omitted (shown later)...

	switch x.Kind() {
	case reflect.Bool:
		return x.Bool() == y.Bool()
	case reflect.String:
		return x.String() == y.String()

	// ...numeric cases omitted for brevity...

	case reflect.Chan, reflect.UnsafePointer, reflect.Func:
		return x.Pointer() == y.Pointer()
	case reflect.Ptr, reflect.Interface:
		return equal(x.Elem(), y.Elem(), seen)
	case reflect.Array, reflect.Slice:
		if x.Len() != y.Len() {
			return false
		}
		for i := 0; i < x.Len(); i++ {
			if !equal(x.Index(i), y.Index(i), seen) {
				return false
			}
		}
		return true

	// ...struct and map cases omitted for brevity...
	}
	panic("unreachable")
}

和前面的建議一樣我們併不公開reflect包相關的接口所以導出的函數需要在內部自己將變量轉爲reflect.Value類型。

// Equal reports whether x and y are deeply equal.
func Equal(x, y interface{}) bool {
	seen := make(map[comparison]bool)
	return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen)
}

type comparison struct {
	x, y unsafe.Pointer
	treflect.Type
}

爲了確保算法對於有環的數據結構也能正常退出我們必鬚記録每次已經比較的變量從而避免進入第二次的比較。Equal函數分配了一組用於比較的結構體包含每對比較對象的地址unsafe.Pointer形式保存和類型。我們要記録類型的原因是有些不同的變量可能對應相同的地址。例如如果x和y都是數組類型那麽x和x[0]將對應相同的地址y和y[0]也是對應相同的地址這可以用於區分x與y之間的比較或x[0]與y[0]之間的比較是否進行過了。

// cycle check
if x.CanAddr() && y.CanAddr() {
	xptr := unsafe.Pointer(x.UnsafeAddr())
	yptr := unsafe.Pointer(y.UnsafeAddr())
	if xptr == yptr {
		return true // identical references
	}
	c := comparison{xptr, yptr, x.Type()}
	if seen[c] {
		return true // already seen
	}
	seen[c] = true
}

這是Equal函數用法的例子:

fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3}))        // "true"
fmt.Println(Equal([]string{"foo"}, []string{"bar"}))      // "false"
fmt.Println(Equal([]string(nil), []string{}))             // "true"
fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true"

Equal函數甚至可以處理類似12.3章中導致Display陷入陷入死循環的帶有環的數據。

// Circular linked lists a -> b -> a and c -> c.
type link struct {
	value string
	tail *link
}
a, b, c := &link{value: "a"}, &link{value: "b"}, &link{value: "c"}
a.tail, b.tail, c.tail = b, a, c
fmt.Println(Equal(a, a)) // "true"
fmt.Println(Equal(b, b)) // "true"
fmt.Println(Equal(c, c)) // "true"
fmt.Println(Equal(a, b)) // "false"
fmt.Println(Equal(a, c)) // "false"

練習 13.1 定義一個深比較函數,對於十億以內的數字比較,忽略類型差異。

練習 13.2 編寫一個函數,報告其參數是否循環數據結構。