2022/05/29

[golang] 配列の比較とスライスの比較

似てるようで似ていない golang の配列とスライス。

 

配列同士の比較

package main

import "fmt"

func main() {
    array1 := [...]int{1, 2, 3}
    array2 := [...]int{2, 2, 3}
    fmt.Printf("array1 == array2: %v\n", array1 == array2)
    array2[0] = 1
    fmt.Printf("array1 == array2: %v\n", array1 == array2)
}

$ go run .
array1 == array2: false
array1 == array2: true

 

配列は宣言時の要素数がそのまま型として扱われると考えておくと良いだろう。
なので、[3]int と [4]int は別の方だから実行時に失敗するのではなくコンパイルエラーになる。

    array3 := [...]int{1, 2, 3, 4}
    fmt.Printf("array1 == array3: %v\n", array1 == array3)

invalid operation: array1 == array3 (mismatched types [3]int and [4]int)

 


スライスは直接の比較ができない。

    slice1 := array1[:]
    slice2 := array2[:]
    fmt.Printf("slice1 == slice2: %v\n", slice1 == slice2)

invalid operation: slice1 == slice2 (slice can only be compared to nil)

 

よく出てくるのが reflect.DeepEqual() を使う方法だった。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    array1 := [...]int{1, 2, 3}
    array2 := [...]int{2, 2, 3}
    array3 := [...]int{1, 2, 3, 4}

    slice1 := array1[:]
    slice2 := array2[:]
    slice3 := array3[:]
    fmt.Printf("slice1 == slice2: %v\n", reflect.DeepEqual(slice1, slice2))
    fmt.Printf("slice1 == slice3: %v\n", reflect.DeepEqual(slice1, slice3))
    array2[0] = 1
    fmt.Printf("slice1 == slice2: %v\n", reflect.DeepEqual(slice1, slice2))
    slice1 = append(slice1, 4)
    fmt.Printf("slice1 == slice3: %v\n", reflect.DeepEqual(slice1, slice3))
}

$ go run .
slice1 == slice2: false
slice1 == slice3: false
slice1 == slice2: true
slice1 == slice3: true

配列をスライスに変換しているのは、単に前のコードを使い回していただけで意味はない。
いま気付いたが、スライスに変換しているといっても、その場で値をコピーしてまるまる別物になるわけではないのだな。
なんでかというと、array2[0] に代入したら slice1 と slice2 が同じ値と判定されているからだ。
COW(Copy On Write)かな?

package main

import (
    "fmt"
)

func main() {
    array2 := [...]int{2, 2, 3}
    slice2 := array2[:]

    slice2[0] = 1
    fmt.Printf("array2[:]: %p\n", &array2)
    fmt.Printf("slice2: %p\n", slice2)
    slice2 = append(slice2, 4)
    fmt.Printf("slice2: %p\n", slice2)
}

$ go run .
array2[:]: 0xc0000ae000
slice2: 0xc0000ae000
slice2: 0xc0000a8030

さすがに明示的に変数への代入をしない限りは書き換わらないか。

 

[]int 型なので reflect.DeepEqual() を使ったが、 []byte 型の場合は bytes.Equal() がある。

package main

import (
    "bytes"
    "fmt"
)

func main() {
    array1 := [...]byte{1, 2, 3}
    array2 := [...]byte{2, 2, 3}
    array3 := [...]byte{1, 2, 3, 4}

    slice1 := array1[:]
    slice2 := array2[:]
    slice3 := array3[:]
    fmt.Printf("slice1 == slice2: %v\n", bytes.Equal(slice1, slice2))
    fmt.Printf("slice1 == slice3: %v\n", bytes.Equal(slice1, slice3))
    array2[0] = 1
    fmt.Printf("slice1 == slice2: %v\n", bytes.Equal(slice1, slice2))
    slice1 = append(slice1, 4)
    fmt.Printf("slice1 == slice3: %v\n", bytes.Equal(slice1, slice3))
}

$ go run .
slice1 == slice2: false
slice1 == slice3: false
slice1 == slice2: true
slice1 == slice3: true

 

シンプルなスライスだったら reflect.DeepEqual() を使うのにちゅうちょしないのだが、構造体のスライスとかになると心配になって使いづらい。説明が長いだけで使うのに不安を感じる。

理解して使えばよいのだが、それくらいだったら自分でループ回して比較した方が安心だと考えてしまう。比較するメソッドを作ればそこまで苦痛ではないだろう。

DeepEqual の実装は説明文に比べるとかなり短い。 go1.18.2 だとこうなっていた。

func DeepEqual(x, y any) bool {
    if x == nil || y == nil {
        return x == y
    }
    v1 := ValueOf(x)
    v2 := ValueOf(y)
    if v1.Type() != v2.Type() {
        return false
    }
    return deepValueEqual(v1, v2, make(map[visit]bool))
}

・片方でも nil があるなら単純比較
・型が違えば false
deepValueEqual()

短いのは条件だけだからだった......

単純比較は nil 同士なら true になるかと思ったのだがそうではなかった。

package main

import (
    "fmt"
    "reflect"
)

func main() {
    type MyType1 = struct {
        Value int
    }
    type MyType2 = struct {
        Value float32
    }
    var val1 *MyType1
    var val2 *MyType2
    val1 = nil
    val2 = nil
    fmt.Printf("val1=%v, val2=%v, compare=%v\n", val1 == nil, val2 == nil, any(val1) == any(val2))
    fmt.Printf("val1 == val2: %v\n", reflect.DeepEqual(val1, val2))
}

$ go run .
val1=true, val2=true, compare=false
val1 == val2: false

なお、型が違うので val1 == val2 と書くとコンパイルエラーになる。

 

DeepEqual() で期待している比較の処理は deepValueEqual() で行われていて、こちらは長い。
あまり見ていないが、型によって比較の仕方が違うから処理に時間がかかりそうだ。
パフォーマンスを気にするシーンだったら自前で書いた方がよさそう。