文法的な観点から言えば、それは可能です。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.4000000000000000000000001
math.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