GO言語インタビューエッセンス - float型はマップのキーとして使用できますか?

文法的な観点から言えば、それは可能です。Go 言語では、同等の型をキーとして使用できます。スライス、マップ、関数型を除き、他の型でも問題ありません。具体的には、ブール値、数値、文字列、ポインタ、チャネル、インターフェイス タイプ、構造体、および上記のタイプのみを含む配列が含まれます。これらの型の共通の特徴は、k1 と k2 が同じキーと見なされる場合に、==and!=演算子をサポートしていることです。k1 == k2構造体の場合、ハッシュ値とリテラル値のみが等しく、それらは同じキーとみなされます。等しいリテラル値の多くについては、参照などのハッシュ値が等しくない場合があります。

ちなみに、値としてはマップ型を含む任意の型を使用できます。

例を見てみましょう:

func main() {
	m := make(map[float64]int)
	m[1.4] = 1
	m[2.4] = 2
	m[math.NaN()] = 3
	m[math.NaN()] = 3

	for k, v := range m {
		fmt.Printf("[%v, %d] ", k, v)
	}

	fmt.Printf("\nk: %v, v: %d\n", math.NaN(), m[math.NaN()])
	fmt.Printf("k: %v, v: %d\n", 2.400000000001, m[2.400000000001])
	fmt.Printf("k: %v, v: %d\n", 2.4000000000000000000000001, m[2.4000000000000000000000001])

	fmt.Println(math.NaN() == math.NaN())
}

プログラム出力:

[2.4, 2] [NaN, 3] [NaN, 3] [1.4, 1] 
k: NaN, v: 0
k: 2.400000000001, v: 0
k: 2.4, v: 2
false

この例では、キー タイプが float のマップが定義され、4 つのキー (1.4、2.4、NAN、NAN) がそこに挿入されます。

印刷時には 4 つのキーも印刷されますが、NAN != NAN とわかっていれば、驚くことではありません。それらの比較結果は等しくないため、当然ながら、マップから見ると、これらは 2 つの異なるキーになります。

次に、いくつかのキーをクエリしたところ、NAN も 2.400000000001 も存在しませんでしたが、2.400000000000000000000001 は存在することがわかりました。

なんだか変ですね。

そして、組み立てを通じて以下の事実を発見しました。

float64 をキーとして使用する場合は、まず uint64 型に変換してからキーに挿入する必要があります。

Float64frombits具体的には、これは次の関数を通じて行われます。

// Float64frombits returns the floating point number corresponding
// the IEEE 754 binary representation b.
func Float64frombits(b uint64) float64 { return *(*float64)(unsafe.Pointer(&b)) }

つまり、浮動小数点数は IEEE 754 で指定された形式で表現されます。代入ステートメントなど:

0x00bd 00189 (test18.go:9)      LEAQ    "".statictmp_0(SB), DX
0x00c4 00196 (test18.go:9)      MOVQ    DX, 16(SP)
0x00c9 00201 (test18.go:9)      PCDATA  $0, $2
0x00c9 00201 (test18.go:9)      CALL    runtime.mapassign(SB)

"".statictmp_0(SB)変数は次のようなものです。

"".statictmp_0 SRODATA size=8
        0x0000 33 33 33 33 33 33 03 40
"".statictmp_1 SRODATA size=8
        0x0000 ff 3b 33 33 33 33 03 40
"".statictmp_2 SRODATA size=8
        0x0000 33 33 33 33 33 33 03 40

もう一度何かを出力してみましょう:

package main

import (
	"fmt"
	"math"
)

func main() {
	m := make(map[float64]int)
	m[2.4] = 2

    fmt.Println(math.Float64bits(2.4))
	fmt.Println(math.Float64bits(2.400000000001))
	fmt.Println(math.Float64bits(2.4000000000000000000000001))
}
4612586738352862003
4612586738352864255
4612586738352862003

16 進数に変換すると、次のようになります。

0x4003333333333333
0x4003333333333BFF
0x4003333333333333

以前のものと比較すると"".statictmp_0、非常に明確です。関数による変換2.4後の結果は同じです。当然、マップの観点からは、2 つは同じキーを持ちます。2.4000000000000000000000001math.Float64bits()

NAN (数値ではありません) を見てみましょう。

// NaN returns an IEEE 754 ``not-a-number'' value.
func NaN() float64 { return Float64frombits(uvnan) }

uvnan は次のように定義されます。

uvnan    = 0x7FF8000000000001

NAN() を直接呼び出しFloat64frombits、ハードコーディングされた const 変数を渡し0x7FF8000000000001、NAN 値を取得します。NAN は定数から解析されるのに、マップに挿入されるときに別のキーとみなされるのはなぜですか?

これは、その型のハッシュ関数によって決まります。たとえば、64 ビット浮動小数点数の場合、そのハッシュ関数は次のようになります。

func f64hash(p unsafe.Pointer, h uintptr) uintptr {
	f := *(*float64)(p)
	switch {
	case f == 0:
		return c1 * (c0 ^ h) // +0, -0
	case f != f:
		return c1 * (c0 ^ h ^ uintptr(fastrand())) // any kind of NaN
	default:
		return memhash(p, h, 8)
	}
}

2 番目のケースf != fはこの目的のためのものでNAN、ここに乱数が追加されます。

このようにして、すべてのパズルが解決されます。

NAN の特性により:

NAN != NAN
hash(NAN) != hash(NAN)

したがって、マップ内で検索したキーが NAN の場合は何も見つかりませんが、そこに 4 つの NAN を追加すると、トラバーサルによって 4 つの NAN が取得されます。

最後に結論ですが、float 型もキーとして使用できますが、精度の問題でおかしな問題が発生するため、使用には注意してください。


キーが参照型の場合、2 つのキーが等しいかどうかを判断するには、ハッシュ値が等しく、キーのリテラルが等しい必要があります。@WuMingyu によって追加された例:

func TestT(t *testing.T) {
    
    
	type S struct {
    
    
		ID	int
	}
	s1 := S{
    
    ID: 1}
	s2 := S{
    
    ID: 1}

	var h = map[*S]int {
    
    }
	h[&s1] = 1
	t.Log(h[&s1])
	t.Log(h[&s2])
	t.Log(s1 == s2)
}

テスト出力:

=== RUN   TestT
--- PASS: TestT (0.00s)
    endpoint_test.go:74: 1
    endpoint_test.go:75: 0
    endpoint_test.go:76: true
PASS

Process finished with exit code 0

おすすめ

転載: blog.csdn.net/zy_dreamer/article/details/132799925