banner
魔法少女秋小日

Qiuxiaori

github
email

Go 言語の型システム

値レシーバとポインタレシーバ#

レシーバが指定された関数はメソッドと呼ばれます。レシーバは値レシーバAnimalまたはポインタレシーバ*Animalのいずれかにすることができます:

type Animal struct {
	name string
}

func (a Animal) valueFn() {} // 値レシーバ
func (a *Animal) ptrFn() {} // ポインタレシーバ

メソッドを呼び出す際、呼び出し元は値型またはポインタ型のいずれかであることができます。

  • 値レシーバメソッドは、呼び出し元が値型またはポインタ型であっても、常に呼び出し元のコピーを渡します。メソッド内では呼び出し元自体を変更しません。
// 例1
func (a Animal) valueFn() {
	a.name = "new_" + a.name
}
func main() {
	a := Animal{"cat"} // 値型
	a.valueFn()
	println("this is name", a.name)
	// => this is name cat
	
	b := &Animal{"dog"} // ポインタ型
	b.valueFn()
	println("this is name", b.name)
	// => this is name dog
}
  • ポインタレシーバメソッドは、呼び出し元の参照を渡すため、呼び出し元を変更することができます。
// 例2
func (a *Animal) ptrFn() {
	a.name = "new_" + a.name
}
func main() {
	a := Animal{"cat"} // 値型
	a.ptrFn()
	println("this is name", a.name)
	// => this is name new_cat
	
	b := &Animal{"dog"} // ポインタ型
	b.ptrFn()
	println("this is name", b.name)
	// => this is name new_dog
}

これはコンパイラが行ういくつかの作業によるものです。以下の表を参照してください:

-値レシーバポインタレシーバ
値型の呼び出し元メソッドは呼び出し元のコピーを使用し、値の参照を使用してメソッドを呼び出します。例 2 は実際には (&a).ptrFn() となります。実際には値を渡すため、呼び出し元に影響を与える操作がメソッド内で行われます。ポインタのコピーが作成されます。
ポインタ型の呼び出し元ポインタは値に解除されます。例 1 は実際には (*b).valueFn() となります。実際には値を渡すため、呼び出し元に影響を与える操作がメソッド内で行われます。ポインタのコピーが作成されます。

型の本質#

新しい型を宣言し、その型のメソッドを宣言する前に、次の質問に答える必要があります:この型の本質は何ですか?この型の値を追加または削除する場合、新しい値を作成する必要がありますか、それとも現在の値を変更する必要がありますか?新しい値を作成する場合、その型のメソッドは値レシーバを使用します。現在の値を変更する場合、ポインタレシーバを使用します。この答えは、この型の値をプログラム内で渡す方法にも影響します。値を渡すか、ポインタを渡すかの一貫性を保つことが重要です。この背後にある原則は、メソッドが値をどのように処理するかではなく、値の本質に焦点を当てることです。

値レシーバを使用するか、ポインタレシーバを使用するかは、受け取る値が変更されたかどうかで決定すべきではありません。この決定は、型の本質に基づいて行うべきです。このルールの例外は、型の値が特定のインターフェースを満たす必要がある場合です。この場合、型の本質が非プリミティブであっても、値レシーバを使用してメソッドを宣言することができます。これにより、インターフェース値のメソッド呼び出しメカニズムが完全に満たされます。

  • 《Go 语言实战 5.3》
  • 組み込みの型:数値型、文字列型、ブール型。これらの型は本質的にプリミティブな型です。したがって、これらの値を追加または削除する場合、新しい値が作成されます。したがって、これらの型の値をメソッドや関数に渡す場合は、対応する値のコピーを渡す必要があります。
  • プリミティブな型:スライス、マップ、チャネル、インターフェース、関数型。後半は理解できませんでした 😅
  • 構造体型:一連の値を記述するために使用されます。変更がある場合はポインタを渡すようにします。型の値がプリミティブでない本質を持つ場合は、共有されるべきであり、コピーされるべきではありません。メソッドが受け取り者の値を変更しない場合でも、ポインタレシーバを使用する必要があります。

このセクションは理解できませんでしたが、おそらく非構造体型の場合は標準ライブラリの慣習に従うべきであり、構造体型の場合は通常、呼び出し元を変更する必要がない場合は値を渡し、変更が必要な場合はポインタを渡すべきだということを意味していると思われます。

インターフェース#

異なる型の呼び出し元に対して、値レシーバとポインタレシーバのメソッドの両方を呼び出すことができますが、インターフェースの実装ではわずかに異なります。値型のレシーバを実装すると、ポインタ型のレシーバも暗黙的に実装されますが、ポインタ型のレシーバを実装する場合は、値型のレシーバは自動的に実装されません。

// 例3. コンパイルは成功します
type IAnimal interface {
	valueFn()
	ptrFn()
}

func (a Animal) valueFn() {}
func (a *Animal) ptrFn() {}

func main() {
    var a IAnimal = &Animal{"cat"}
	a.valueFn()
	a.ptrFn()
}
// 例4. コンパイルエラー
func main() {
    var a IAnimal = Animal{"cat"} // 値型に変更
	a.valueFn()
	a.ptrFn()
}
// => .\main.go:22:18: cannot use Animal{…} (value of type Animal) as IAnimal value in variable declaration: Animal does not implement IAnimal (method ptrFn has pointer receiver)

メソッドセット#

インターフェースを実装するためにポインタレシーバを使用する場合、値型の値はそのインターフェースを実装することができません。これは、メソッドセットに関連しています。メソッドセットは、特定の型の値またはポインタに関連付けられた一連のメソッドです。メソッドを定義する際に使用するレシーバの型によって、そのメソッドが値に関連付けられるか、ポインタに関連付けられるか、または両方に関連付けられるかが決まります。

  • 仕様で定義されたメソッドセット:
valuesmethods receivers
T(t T)
* T(t T) and (t * T)

これは、T 型の値のメソッドセットには値レシーバで宣言されたメソッドのみが含まれることを意味します。一方、T 型のポインタのメソッドセットには値レシーバで宣言されたメソッドとポインタレシーバで宣言されたメソッドの両方が含まれます。

  • レシーバの視点から見たメソッドセットの規格:
methods receiversvalues
(t T)T and * T
(t * T)* T

これは、値レシーバを使用してインターフェースを実装する場合、値型の値とポインタの両方が対応するインターフェースを実装できます。ポインタレシーバを使用してインターフェースを実装する場合、その型のポインタのみが対応するインターフェースを実装できます。

したがって、eg4 の値型 AnimalIAnimal インターフェースを実装していないため、エラーが発生します。

埋め込み型#

埋め込み型は、既存の型を新しい構造体型に直接宣言することです。埋め込まれた型は、新しい外部型の内部型として知られています。

type Cat struct {
	Animal
	color string
}

func main() {
	c := Cat{Animal{"miao"},"yellow"}
	c.Animal.valueFn()
}

内部型のフィールドとメソッドは、外部型から直接アクセスすることもできます:

func main() {
	//...
	c.valueFn()
	println("this is c name", c.name)
}

参考文献#

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。