takuroooのブログ

勉強したこととか

GoのArrayとSliceの違い

先週からGoを勉強し始めた。
blog.golang.org
ArrayとSliceがいまいちわからなかったが、この記事を読んでArrayとSliceの違いを学んだので自分なりにまとめてみる。

目次

ArrayとSliceの違い(記事を読む前の認識)

  • Array
    • 固定長の配列
    • C言語の配列と同じように変数は配列の先頭アドレスを保持する。
  • Slice
    • 可変長の配列。

と思っていたけど調べてみると間違っている部分があった。

以下ArrayとSliceが何者なのかをまとめていく。

Array

Array型とはなにか

Array型は配列全体を示すデータ型とのこと。これは配列の長さまでを含めて型として定義しているということ。*1

なので同じArray型でも長さが異なれば違う型として判定される。 つまり、以下のコードはエラーになる。

package main

import (
    "fmt"
)

func doSomething(arr [4]int) {
    fmt.Println(arr)
}

func main() {
    arr := [3]int{1, 2, 3}

    // [3]int と [4]intは型が違うためエラーになる
    doSomething(arr) 
}

arrの型は、int型の値を持つ配列ではなく「int型の値を持つ長さ3の配列」という意味になる。関数の引数の型は「int型の値を持つ長さ4の配列」なので型が一致せずエラーになる。

Array型の変数は何を意味しているのか

Array型の変数は、C言語の配列のように先頭のアドレスを保持しているわけではない。 固定長の配列という観点ではC言語の配列と同じだが、この点が違う。

以下のコードではArray型の変数の値をアドレス表記としてプリントしようとしているが、正しくプリントされない。
Array型の変数はC言語の配列のように配列の先頭アドレスを保持しているわけではないので、変数をアドレスとしてプリントしようとすると意図しない表示になる。

package main

import (
    "fmt"
)

func main() {
    arr := [3]int{1, 2, 3}
    // アドレスが表示されるのが期待だけど...
    fmt.Printf("%p\n", arr) // %!p([3]int=[1 2 3])
}

一方、Sliceは同じやり方でアドレスが正しく表示される。詳細は後述する。

Array型の変数は値渡し

Array型の変数は先頭のアドレスを保持した変数ではないため関数に私たりや変数の代入すると実態のコピーが発生する。

package main

import (
    "fmt"
)

func main() {
    arr1 := [3]int{1, 2, 3}
    arr2 := arr1 // arr2のために新しいメモリを確保してarr1の値がコピーされる
    arr2[0] = 0

    // arr1とarr2が同じ配列を共有していないためarr2を変更してもarr1に影響がない
    fmt.Println(arr1, arr2) // [1 2 3] [0 2 3]
}

関数の引数としてArrayを渡した場合もコピーが渡される。

package main

import (
    "fmt"
)

func doSomething(arr [3]int) {
    arr[0] = 0 // Arrayのコピーの値を変えているためarr1には影響がない
}

func main() {
    arr1 := [3]int{1, 2, 3}
    doSomething(arr1)
    fmt.Println(arr1) //[1 2 3]
}

もちろんArrayのアドレスを引数として渡せば、Arrayのコピーは発生せずにdoSomethingの中で行なった処理はarr1に影響するようになる。

Slice

Slice型とはなにか

Slice型は配列を指すデータ型である。どの型のArrayを指すかだけを定義している。なのでSlice型の宣言には配列の長さを含めない。

package main

import (
    "fmt"
)

func main() {
    slice := []int{1, 2, 3}
    arr := [3]int{1, 2, 3}
    fmt.Printf("slice %T\n", slice) # slice []int
    fmt.Printf("arr   %T\n", arr)   # arr   [3]int
}

%Tで型を表示してみるとSlice型は長さの情報が表示されないが、Array型は長さが表示されているのがわかる。

Slice型の変数は何を意味しているのか

Slice型の変数はArrayの先頭アドレスを保持している構造体みたいなもの。これはC言語の配列を保持する変数と似ている。
Arrayにもポインタがあるがこれは固定長の配列を指すアドレスである。サイズが異なる配列を指すことはできない。Slice型の変数はArrayの長さに制限されない。

package main

import (
    "fmt"
)

func main() {
    slice := []int{1, 2, 3}
    fmt.Printf("%p\n", slice) // 16進数で配列アドレスが表示される
}

Slice型の変数の値を%pで表示するとArray型のときとは異なりエラーしないでアドレスが表示される。
slice := []int{1, 2, 3}この宣言ではsliceにArrayの先頭アドレスが保持される。このとき背後で長さが3の配列が生成されている。

下記コードではArrayからSliceを生成しているが、Sliceが保持しているアドレスがArrayのアドレスを保持していることがわかる。同じアドレスを見ているので、arrもしくはsliceの変更はお互いに影響しあう。

package main

import (
    "fmt"
)

func main() {
    arr := [3]int{1, 2, 3} // Array型
    var slice []int // Slice型

    slice = arr[:] // arr[:]はArrayからSliceを生成する

    // 同じアドレスが表示される
    fmt.Printf("%p %v\n", slice, slice) // 0xc0000141a0 [1 2 3]
    fmt.Printf("%p %v\n", &arr, arr) // 0xc0000141a0 [1 2 3]

    arr[0] = 0

    // 同じアドレスを指しているのでarrの変更はsliceにも反映される
    fmt.Printf("%p %v\n", slice, slice) // 0xc0000141a0 [0 2 3]
    fmt.Printf("%p %v\n", &arr, arr) // 0xc0000141a0 [0 2 3]
}

Slice型の変数も値渡し

Slice型はArray型の先頭アドレスを保持している構造体のようなもの。これを関数の引数に渡すとスライスが持つ情報はコピーされる(参照渡しにならない)。しかしスライスが持つ配列のアドレスはそのまま同じものが関数に渡されるので、関数内でスライスを操作すると(スライスが指している配列が操作されると)、関数の呼び出し元のスライス(が指している配列)にも影響がでる。

package main

import (
    "fmt"
)

func doSomething(slice []int) {
    slice[0] = 0
    fmt.Printf("%p %d\n", slice, slice) // 0xc0000141a0 [0 2 3]
    fmt.Printf("%p\n", &slice) // スライスの情報を保持しているアドレスは関数外のものと異なる

}
func main() {
    slice := []int{1, 2, 3}
    doSomething(slice)
    fmt.Printf("%p %d\n", slice, slice) // 0xc0000141a0 [0 2 3]
    fmt.Printf("%p\n", &slice) //  // スライスの情報を保持しているアドレスは関数内のものと異なる
}

*1:GoのブログではArrayはインデックス付きのフィールドを持った構造体としても考えられると書いてある。