Map の話

今回も覚え書き用の小ネタ。

map による連想配列の定義と基本操作

まず, map を使って以下の連想配列を考える。

type platnets map[string]float64

p := platnets{
    "Mercury": 0.055,
    "Venus":   0.815,
    "Earth":   1.0,
}

この連想配列の各要素を操作する記述は以下のような感じになる。

package main

import (
    "fmt"
)

type platnets map[string]float64

func main() {
    p := platnets{
        "Mercury": 0.055,
        "Venus":   0.815,
        "Earth":   1.0,
    }

    if v, ok := p["Mars"]; ok {
        fmt.Println("Mars =", v)
    } else {
        fmt.Println("Mars is not exist.")
    }

    p["Mars"] = 0.107 //add or set
    if v, ok := p["Mars"]; ok {
        fmt.Println("Mars =", v)
    } else {
        fmt.Println("Mars is not exist.")
    }

    delete(p, "Mars")
    if v, ok := p["Mars"]; ok {
        fmt.Println("Mars =", v)
    } else {
        fmt.Println("Mars is not exist.")
    }
}

このコードの実行結果は以下の通り。

Mars is not exist.
Mars = 0.107
Mars is not exist.

この連想配列の要素を全て取り出す。 これは for range 構文を使ってこんな感じに記述する。

package main

import "fmt"

type platnets map[string]float64

func main() {
    p := platnets{
        "Mercury": 0.055,
        "Venus":   0.815,
        "Earth":   1.0,
    }
    for k, v := range p {
        fmt.Println(k, v)
    }
}

結果は以下の通り。

Mercury 0.055
Venus 0.815
Earth 1

ちなみに for range 構文で map を使う場合,取り出しの順番は不定になる1。 例えば,アルファベット順に取り出したければ以下のようにキーをソートした配列を用意する。

package main

import (
    "fmt"
    "sort"
    "strings"
)

type platnets map[string]float64

func main() {
    p := platnets{
        "Mercury": 0.055,
        "Venus":   0.815,
        "Earth":   1.0,
    }

    keys := []string{}
    for k, _ := range p {
        keys = append(keys, k)
    }
    sort.Slice(keys, func(i, j int) bool {
        return strings.Compare(keys[i], keys[j]) < 0
    })
    for _ , k := range keys {
        fmt.Println(k, p[k])
    }
}

結果は以下の通り

Earth 1
Mercury 0.055
Venus 0.815

map は連想配列を参照するオブジェクトである

map連想配列への参照を属性として持つオブジェクトである。 したがって map インスタンスを引数として渡した場合は見かけ上「参照渡し」として機能する

package main

import "fmt"

type platnets map[string]float64

func (p platnets) print() {
    for k, v := range p {
        fmt.Println(k, v)
    }
}

func add(p platnets, k string, v float64) {
    p[k] = v
}

func main() {
    p := platnets{
        "Mercury": 0.055,
        "Venus":   0.815,
        "Earth":   1.0,
    }
    p.print()
    fmt.Println()

    add(p, "Mars", 0.107)
    p.print()
}

このコードの実行結果は以下の通り。

Mercury 0.055
Venus 0.815
Earth 1

Mercury 0.055
Venus 0.815
Earth 1
Mars 0.107

Nil に注意

当たり前の話ではあるが map では空の連想配列と nil では挙動が異なる。 たとえば nil map に要素を追加しようとすると

package main

type platnets map[string]float64

func main() {
    var p platnets
    p["Mars"] = 0.107 //add or set
}

以下のように実行時 panic になる。

panic: assignment to entry in nil map

したがって有効な map インスタンスを宣言する場合は make() 関数で初期化するか nil 以外の初期値を与える必要がある。

package main

type platnets map[string]float64

func main() {
    p := platnets{}
    p["Mars"] = 0.107 //add or set
}

ちなみに nil map に対して len() 関数を使っても実行時 panic にならない2

package main

import "fmt"

type platnets map[string]float64

func main() {
    var p platnets
    fmt.Println(len(p)) // 0
}

連想配列の複製

map 内の連想配列の複製を行う関数は用意されていない3 ため for range 構文で地道に処理する。

package main

import "fmt"

type platnets map[string]float64

func (p platnets) print() {
    for k, v := range p {
        fmt.Println(k, v)
    }
}

func (p platnets) copy() platnets {
    c := platnets{}
    for k, v := range p {
        c[k] = v
    }
    return c
}

func main() {
    p1 := platnets{
        "Mercury": 0.055,
        "Venus":   0.815,
        "Earth":   1.0,
    }

    p2 := p1.copy()
    p2["Mars"] = 0.107

    p1.print()
    fmt.Println()
    p2.print()
}

このコードの実行結果は以下の通り。

Mercury 0.055
Venus 0.815
Earth 1

Mercury 0.055
Venus 0.815
Earth 1
Mars 0.107

連想配列の比較

map インスタンス同士は == 演算子による比較ができない。 以下のコードはコンパイルエラーになる4

package main

import "fmt"

type platnets map[string]float64

func (p platnets) copy() platnets {
    c := platnets{}
    for k, v := range p {
        c[k] = v
    }
    return c
}

func main() {
    p1 := platnets{
        "Mercury": 0.055,
        "Venus":   0.815,
        "Earth":   1.0,
    }

    p2 := p1.copy()
    if p1 == p2 {
        fmt.Println("p1 == p2")
    } else {
        fmt.Println("p1 != p2")
    }
}
prog.go:23:8: invalid operation: p1 == p2 (map can only be compared to nil)

map が参照している連想配列の内容を比較したいのであれば reflect.DeepEqual() 関数が使える。

package main

import (
    "fmt"
    "reflect"
)

type platnets map[string]float64

func (p platnets) copy() platnets {
    c := platnets{}
    for k, v := range p {
        c[k] = v
    }
    return c
}

func main() {
    p1 := platnets{
        "Mercury": 0.055,
        "Venus":   0.815,
        "Earth":   1.0,
    }

    p2 := p1.copy()
    if reflect.DeepEqual(p1, p2) {
        fmt.Println("p1 == p2")
    } else {
        fmt.Println("p1 != p2")
    }
}

このコードの実行結果は以下の通り。

p1 == p2

map が参照している連想配列のインスタンスが同一であるかどうか調べるには reflect.ValueOf() 関数で値(=連想配列)を取得し,そのポインタ値を == 演算子で比較する。

package main

import (
    "fmt"
    "reflect"
)

type platnets map[string]float64

func (p platnets) copy() platnets {
    c := platnets{}
    for k, v := range p {
        c[k] = v
    }
    return c
}

func main() {
    p1 := platnets{
        "Mercury": 0.055,
        "Venus":   0.815,
        "Earth":   1.0,
    }

    p2 := p1
    if reflect.ValueOf(p1).Pointer() == reflect.ValueOf(p2).Pointer() {
        fmt.Println("p1 == p2")
    } else {
        fmt.Println("p1 != p2")
    }

    p2 = p1.copy()
    if reflect.ValueOf(p1).Pointer() == reflect.ValueOf(p2).Pointer() {
        fmt.Println("p1 == p2")
    } else {
        fmt.Println("p1 != p2")
    }
}

このコードの実行結果は以下の通り。

p1 == p2
p1 != p2

【2023-08-10 追記】 maps 標準パッケージを使う

Go 1.21 から maps 標準パッケージが追加された。 これは map の操作を Generics を使って定義したもので,たとえば map の複製や比較を行うメソッドは

// Clone returns a copy of m.  This is a shallow clone:
// the new keys and values are set using ordinary assignment.
func Clone[M ~map[K]V, K comparable, V any](m M) M {
    // Preserve nil in case it matters.
    if m == nil {
        return nil
    }
    return clone(m).(M)
}
// Equal reports whether two maps contain the same key/value pairs.
// Values are compared using ==.
func Equal[M1, M2 ~map[K]V, K, V comparable](m1 M1, m2 M2) bool {
    if len(m1) != len(m2) {
        return false
    }
    for k, v1 := range m1 {
        if v2, ok := m2[k]; !ok || v1 != v2 {
            return false
        }
    }
    return true
}

といった感じに定義されている。 これを使えば前節のコードは

package main

import (
	"fmt"
	"maps"
	"reflect"
)

type platnets map[string]float64

func main() {
	p1 := platnets{
		"Mercury": 0.055,
		"Venus":   0.815,
		"Earth":   1.0,
	}

	p2 := maps.Clone(p1)
	if reflect.ValueOf(p1).Pointer() == reflect.ValueOf(p2).Pointer() {
		fmt.Println("p1.Pointer == p2.Pointer")
	} else {
		fmt.Println("p1.Pointer != p2.Pointer")
	}
	if maps.Equal(p1, p2) {
		fmt.Println("p1 == p2")
	} else {
		fmt.Println("p1 != p2")
	}

	p2["mars"] = 0.107
	if maps.Equal(p1, p2) {
		fmt.Println("p1 == p2")
	} else {
		fmt.Println("p1 != p2")
	}
	// Output:
	// p1.Pointer != p2.Pointer
	// p1 == p2
	// p1 != p2
}

てな感じに書き直すことができる。 maps.Clone() 関数によって新たにコピーが生成されているのが確認できるだろう。 また maps.Equal() 関数が連想配列の内容(値)を比較している点にも注目してほしい。

ブックマーク

参考図書

photo
プログラミング言語Go
アラン・ドノバン (著), ブライアン・カーニハン (著), 柴田芳樹 (著)
丸善出版 2016-06-20 (Release 2021-07-13)
Kindle版
B099928SJD (ASIN)
評価     

Kindle 版出た! 一部内容が古びてしまったが,この本は Go 言語の教科書と言ってもいいだろう。感想はこちら

reviewed by Spiegel on 2021-05-22 (powered by PA-APIv5)


  1. 言語仕様上定義されていないという意味ではなく,意図的に乱択されるらしい。 ↩︎

  2. これは nil slice も同様で, len() 関数を使っても実行時 panic にならず 0 が返る。 ↩︎

  3. 代入構文では map インスタンスの複製ができるだけで,参照している連想配列は同じものになる。 ↩︎

  4. &p1 == &p2 ならコンパイルエラーにはならないが,やってることは map インスタンスのポインタ値を比較しているだけである。 ↩︎