1

Go でポインター、スライス、およびインターフェイスがどのように相互作用するかについて頭を悩ませています。これは私が現在コーディングしているものです:

type Loader interface {
  Load(string, string)
}

type Foo struct {
  a, b string
}

type FooList []Foo

func (l FooList) Load(a, b string) {
  l = append(l, Foo{a, b})
  // l contains 1 Foo here
}

func Load(list Loader) {
  list.Load("1", "2")
  // list is still nil here
}

この設定を前提として、次のことを試みます。

var list FooList
Load(list)
fmt.Println(list)

ただし、リストは常にnilここにあります。私の FooList.Load 関数は要素をlスライスに追加しますが、それは可能な限りです。listin Load はのままnilです。スライスへの参照を渡して、それに追加することができるはずだと思います。ただし、それを機能させる方法について明らかに何かが欠けています。

4

4 に答える 4

3

( http://play.golang.org/p/uuRKjtxs9Dのコード)

メソッドに変更を加えたい場合は、ポインター レシーバーを使用することをお勧めします。

// We also define a method Load on a FooList pointer receiver.
func (l *FooList) Load(a, b string) {
    *l = append(*l, Foo{a, b})
}

ただし、これには、FooList 値自体がLoaderインターフェイスを満たさないという結果があります。

var list FooList
Load(list)      // You should see a compiler error at this point.

ただし、FooList 値へのポインターLoaderは、インターフェイスを満たします。

var list FooList
Load(&list)

以下の完全なコード:

package main

import "fmt"

/////////////////////////////
type Loader interface {
  Load(string, string)
}

func Load(list Loader) {
    list.Load("1", "2")
}
/////////////////////////////


type Foo struct {
  a, b string
}

// We define a FooList to be a slice of Foo.
type FooList []Foo

// We also define a method Load on a FooList pointer receiver.
func (l *FooList) Load(a, b string) {
    *l = append(*l, Foo{a, b})
}

// Given that we've defined the method with a pointer receiver, then a plain
// old FooList won't satisfy the Loader interface... but a FooList pointer will.

func main() {
    var list FooList
    Load(&list)
    fmt.Println(list)
}
于 2013-09-11T02:36:29.453 に答える
1

分かりやすいように問題を単純化します。そこで行われていることはこれと非常に似ていますが、これも機能しません (ここで実行できます):

type myInt int

func (a myInt) increment() { a = a + 1 }
func increment(b myInt)    { b.increment() }

func main() {
    var c myInt = 42
    increment(c)
    fmt.Println(c) // => 42
}

これが機能しない理由は、ドキュメントで説明されているように、Go がパラメーターを値で渡すためです。

関数呼び出しでは、関数の値と引数は通常の順序で評価されます。それらが評価された後、呼び出しのパラメーターは値によって関数に渡され、呼び出された関数が実行を開始します。

実際には、これは、上記の例のab、およびのそれぞれcが異なる int 変数を指しており、aおよびが初期値bのコピーであることを意味します。c

これを修正するには、同じメモリ領域を参照できるようにポインタを使用する必要があります (ここで実行可能):

type myInt int

func (a *myInt) increment() { *a = *a + 1 }
func increment(b *myInt)    { b.increment() }

func main() {
    var c myInt = 42
    increment(&c)
    fmt.Println(c) // => 43
}

現在a、 とは両方ともvariableのアドレスbを含むポインターであり、それぞれのロジックが元の値を変更できるようになっています。文書化された動作はここでも保持されることに注意してください:とは元の値のコピーですが、関数のパラメーターとして提供される元の値は のアドレスです。cabincrementc

スライスの場合もこれと同じです。これらは参照ですが、参照自体は値によるパラメーターとして提供されるため、参照を変更すると、それらは異なる変数であるため、呼び出しサイトは変更を監視しません。

ただし、それを機能させる別の方法もあります。それは、標準append関数の API に似た API を実装することです。再び簡単な例をincrement使用すると、代わりに変更された値を返すことで、元の値を変更せずに、またポインターを使用せずに実装できます。

func increment(i int) int {  return i+1 }

strconv.AppendInt関数など、標準ライブラリのさまざまな場所でこの手法が使用されていることがわかります。

于 2013-09-11T02:48:38.983 に答える
0

Go のデータ構造がどのように実装されているかのメンタル モデルを維持することは価値があります。これにより、通常、このような動作について簡単に推論できます。

http://research.swtch.com/godataは、高レベルのビューを紹介するのに適しています。

于 2013-09-11T04:00:32.053 に答える
0

Go は値渡しです。これは、パラメーターとレシーバーの両方に当てはまります。スライス値に割り当てる必要がある場合は、ポインターを使用する必要があります。

次に、スライスへのポインターは既に参照されているため、ポインターを渡してはならないことをどこかで読みました

これは完全に真実ではなく、話の一部が欠けています。

マップ タイプ、チャネル タイプなどを含む「参照タイプ」とは、実際には内部データ構造へのポインタであることを意味します。たとえば、マップ タイプは基本的に次のように定義されていると考えることができます。

// pseudocode
type map *SomeInternalMapStructure

したがって、連想配列の「内容」を変更するために、マップ変数に代入する必要はありません。マップ変数を値で渡すことができ、その関数はマップ変数が指す連想配列の内容を変更でき、呼び出し元に表示されます。これは、何らかの内部データ構造へのポインターであることに気付いたときに意味があります。どの内部連想配列を指すようにするかを変更したい場合にのみ、マップ変数に割り当てます。

ただし、スライスはより複雑です。これは、(内部配列への) ポインターに、長さと容量、2 つの整数を加えたものです。したがって、基本的には、次のように考えることができます。

// pseudocode
type slice struct {
    underlyingArray uintptr
    length int
    capacity int
}

したがって、「単なる」ポインタではありません。これは、基になる配列に対するポインターです。ただし、長さと容量はスライス タイプの「値」部分です。

したがって、スライスの要素を変更する必要があるだけの場合は、はい、スライスを値で渡し、関数に要素を変更させて呼び出し元に表示できるという点で、参照型のように機能します。

ただし、あなたappend()(質問であなたがしていること)の場合は異なります。まず、追加はスライスの長さに影響し、長さはポインターの後ろではなく、スライスの直接部分の 1 つです。第 2 に、追加によって別の基になる配列が生成される場合があります (元の基になる配列の容量が十分でない場合は、新しい配列が割り当てられます)。したがって、スライスの配列ポインター部分も変更される可能性があります。したがって、スライス値を変更する必要があります。(これがappend()何かを返す理由です。) この意味で、参照型と見なすことはできません。スライスを直接変更しています。

于 2013-09-12T10:31:15.037 に答える