質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Go

Go(golang)は、Googleで開発されたオープンソースのプログラミング言語です。

Q&A

解決済

1回答

575閲覧

[Go] スライスのshift関数を実装したい

gorimori

総合スコア7

Go

Go(golang)は、Googleで開発されたオープンソースのプログラミング言語です。

0グッド

0クリップ

投稿2018/03/11 18:51

RubyにおけるArray#shiftと同様の関数を実装したい

素朴に以下のように書いたところ期待とは異なる挙動をしました。

shiftの実装

go

1func shift(ls []int) int { 2 x := ls[0] 3 ls = ls[1:] 4 return x 5}

実行結果

もとのスライスが変更されていませんでした。

go

1func main() { 2 ls := []int{1, 2, 3} 3 x := shift(ls) 4 fmt.Println(x, ls) 5}

sh

1$ go run main.go 21 [1 2 3]

また、こちらの質問を参考にappendを使うようにしても思うような結果が得られませんでした。

appendを使った場合

go

1func shift(ls []int) int { 2 x := ls[0] 3 ls = append(ls[:0], ls[1:]...) 4 return x 5}

実行結果

もとのスライスの最後の要素が重複しているようでした。

sh

1$ go run main.go 21 [2 3 3]

質問

  1. 最初の実装ではなぜもとのスライスが変更されないのでしょうか?
  2. appendを使った実装ではなぜ最後の要素が重複しているのでしょうか?
  3. 期待する結果を得るにはどのように実装したらいいのでしょうか?

なお、以下のように関数を定義せずに同様のことができることは存じておりますので、この方向性でやるとしたらどのようにしたらよいかをお伺いしたいです。

go

1ls := []int{1,2,3} 2x := ls[0] 3ls = ls[1:]

環境

sh

1$ go version 2go version go1.10 linux/amd64

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

1.

スライスは参照用の値と実体の配列の2段構造です。

go

1ls := []int{1, 2, 3}

とスライスを作ると、[1 2 3]という配列がメモリに確保され、
その参照用の値

Slice{ptr:<配列の参照先ポインタ>,len:3, cap:3}

というのに等価な構造体がスライスなのです。

なので、スライスを引数に載せたり、返値を受け渡す際に
構造体のコピーが発生します。
なので、引数に渡す前と受け取ったものは複製されており別物です。
(指してる配列は同じですが)
別物をいくら加工しようが元のスライスは影響ありません。
(配列値を変更すればもちろん影響あります)

詳しくはこちら

2.

appendではスライス値の再代入を行っているのでちゃんとスライス構造の変化が反映されます。
これけっこう説明ややこしいですね。あとで書き直します。

https://play.golang.org/p/veZLmIk1kGT

go

1func shift(ls []int) int { 2 x := ls[0] 3 ls = append(ls[:0], ls[1:]...) 4 fmt.Printf("ls: %[1]v(%[1]p)\n", ls) 5 return x 6} 7func main() { 8 s := []int{1, 2, 3} 9 x := shift(s) 10 fmt.Printf("s: %[1]v(%[1]p)\n", s) 11 fmt.Println("x:",x) 12}

詳しくは・・・のページを読んだあとで、
上記ようにポインタアドレスを表示するようにすると見えてくるかもしれません。
xとlsの配列へのポインタは同じ値です。

go

1ls = append(ls[:0], ls[1:]...)

この時何が行われているのかというと、

  • (元配列は[1 2 3])
  • lsの配列ポインタの先にls[1:]の内容をコピーする
  • (元配列は[2 3 3] に変更される)
  • lsというスライスは元配列の先頭から長さ2のスライスになる
  • このlsを表示すると「[]int{2 3}」
  • shit関数呼び出し元のsというスライスは変更されないので元配列を指す長さ3のスライスのまま
  • このsを表示すると「[]int{2 3 3}」となる

3.

以下の形であれば動作すると思います。

https://play.golang.org/p/ITqY6PmOFiA

sh

1func shift(in []int) (int, []int) { 2 return in[0], in[1:] 3} 4func main() { 5 s := []int{1,2,3} 6 x, s := shift(s) 7 fmt.Println(x, s) 8}

もう一つの方法はスライスをポインタ渡しとします。

https://play.golang.org/p/N2lNYs4eYRc

sh

1func shift(in *[]int) int { 2 res := (*in)[0] 3 *in = (*in)[1:] 4 return res 5} 6func main() { 7 s := []int{1, 2, 3} 8 x := shift(&s) 9 fmt.Println(x, s) 10}

投稿2018/03/11 23:54

編集2018/03/12 02:06
nobonobo

総合スコア3367

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

gorimori

2018/03/12 12:53 編集

nobonoboさん とても詳細な解説ありがとうございました。具体的な実装コードまで提示していただけてとても勉強になります。 1は、 - スライスは配列へのポインタと長さ、キャパシティで構成されており、引数としてスライスを渡した(コピーされた)場合でももとの配列へのポインタを持っているため配列の値を変更できる。 - つまりコピー元のスライスを変更できるが、「コピーされたスライスが持っている」もとの配列へのポインタを変更しても、「コピー元のスライスが持っている」もとの配列へのポインタは影響を受けない というふうに理解しました。 [ご提示いただいた資料](https://blog.golang.org/go-slices-usage-and-internals)の > Slicing does not copy the slice's data. It creates a new slice value that points to the original array. という部分と解説がとてもわかりやすかったです。 2の解説もよく理解できました。何が起きているのかまったくわからなかったので感動しました。コピーされたスライスを使って背後の配列の値を変えたとしても、もとのスライスが持っている`len`の情報が更新されないため`[]int{2,3,3}`となってしまうのですね。 3の例は、前者が「コピーされた新しいスライスをもとのスライスに代入してしまう」という作戦で、後者は「`ptr`, `len`を直接書き換えてしまう」という作戦みたいな感じでしょうか。 丁寧で詳しい解説をどうもありがとうございました。こういったことをちゃんと勉強したことがありませんでしたがとてもおもしろかったです。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問