mirror of
https://github.com/gopl-zh/gopl-zh.github.com.git
synced 2024-12-01 02:28:56 +00:00
133 lines
5.0 KiB
Markdown
133 lines
5.0 KiB
Markdown
## 13.3. 示例: 深度相等判斷
|
|
|
|
來自 reflect 包的 DeepEqual 對兩個值進行深度相等判斷. DeepEqual 使用內建的 `==` 操作符對基礎類型進行相等判斷, 對於複合類型則遞歸變量每個基礎類型然後做類似的比較判斷. 因爲它工作在任意的類型上, 甚至對一些不支持 `==` 操作符的類型也可以工作, 因此在一些測試代碼中被廣泛地使用. 比如下面的代碼是用 DeepEqual 比較兩個字符串數組是否等價.
|
|
|
|
```Go
|
|
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 也不相等.
|
|
|
|
```Go
|
|
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 slice(map類似) 和非 nil 的空 slice 視作相等的值. 基礎部分的比較可以基於反射完成, 和 12.3 章的 Display 實現方法類似. 同樣, 我們頂一個一個內部函數 equal, 用於內部的遞歸比較. 目前不用關心 seen 參數. 對於每一對需要比較的 x 和 y, equal 函數 首先檢測它們是否都有效(或都無效), 然後檢測它們是否是相同的類型. 剩下的部分是一個大的 switch 分支, 用於擁有相同基礎類型的比較. 因爲頁面空間的限製, 我們省略了一些類似的分支.
|
|
|
|
```Go
|
|
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.Value 類型.
|
|
|
|
```Go
|
|
// 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] 的是否進行過了.
|
|
|
|
```Go
|
|
// 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 函數的使用的例子:
|
|
|
|
```Go
|
|
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"
|
|
```
|
|
|
|
它甚至可以處理類似12.3章中導致Display陷入死循環的數據.
|
|
|
|
```Go
|
|
// 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: 編寫一個函數, 報告其參數是否循環數據結構.
|
|
```
|
|
|