「メソッド」
書籍:プログラミング言語Go
第6章 「メソッド」 の要点と思われる箇所を自分のメモ用にまとめました。
6.1 メソッド宣言
- メソッドは特定の型に関連付けられた関数。オブジェクトはメソッドを持つ単なる値あるいは変数。
- メソッドは普通の関数宣言の変形で宣言され、そこでは関数名の前に追加のパラメータが書かれる。そのパラメータが当のパラメータの型へ関数を結びつける。
.go
type Point struct { X, Y float64 }
func Distance(p, q Point) float64 { // 昔ながらの関数
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
func (p Point) Distance(q Point) float64 // Point型のメソッド
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(p.Distance(q)) // "5", メソッド呼び出し。
- 上記メソッドの p をレシーバと呼ぶ。Goではレシーバに対し、this や self などの特別な名前は使わない。
- レシーバ名は頻繁に使われるので短くメソッド間で一貫するのが望ましく、Pointに対する p など、型名の頭文字などよく利用される。
- メソッドとフィールドは同じ名前空間に存在するため、メソッド名とフィールド名が同一のものを定義すると曖昧となりコンパイルエラーとなる。
- **スライス型などもtype宣言で名前を付けることができ、メソッドを関連付けることができる。**この点は他言語と異なる点。
.go
type Path []Point
func (path Path) Distance() float64 { // pathに沿って進んだ距離を返す。
sum := 0.0
for i := range path {
if i > 0 {
sum += path[i-1].Distance(path[i])
}
}
return sum
}
- マップや関数も名前付き型にすればメソッド定義できる。基底型がポインタかインタフェースでない限り同一パッケージ内の名前付き型にはメソッド宣言できる。
- 関数でなくメソッドを使う利点の一つは型の名前があるのでメソッド名を短くできる点。パッケージ外からの呼び出しでこの利点は増大する。
6.2 ポインタレシーバを持つメソッド
- *Pointなどのポインタ型にもメソッドを結び付けられる。以下のメソッドの名前は(*Point).ScaleBy ("()"をつけないと*(Point.ScaleBy)と解釈されるので注意)。
.go
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
- 慣習として、Pointのどれかのメソッドがポインタレシーバを持つ場合、Pointの すべて のメソッドはポインタレシーバを持つべき。
- 名前付き型 (Point) とその型へのポインタ (*Point) は、レシーバ宣言に書くことができる。曖昧さを避けるため、それ自身がポインタの名前付き型はメソッド宣言できない。
.go
type P *int
func (P) f() { ... } // コンパイルエラー: 不正なレシーバ型
- ポインタのレシーバの利用方法は以下。最後の呼び出しもOKで、コンパイラが暗黙で&pを行う。Pointのメソッドを*Pointで呼び出すことも可能。
.go
r := &Point{1, 2}
r.ScaleBy(2)
p := Point{1, 2}
pptr := &p
pptr.ScaleBy(2)
(&p).ScaleBy(2)
p.ScaleBy(2) // コンパイラが暗黙で&pを利用してくれる。変数 (アドレス化可能) の場合のみ暗黙の変換をしてくれる。
- アドレス化可能でない Point レシーバに対して *Point を呼び出すことはできない、
.go
Point{1, 2}.ScaleBy(2) // コンパイルエラー: Pointリテラルのアドレスは得られない。
- 以下が成立すればメソッド呼び出しが可能。
- レシーバ引数がレシーバパラメータと同じ型。
- レシーバ引数が型 T の変数で、レシーバパラメータが型 *T である。(コンパイラが暗黙でその変数のアドレスを得る。)
- レシーバ引数の型が *T で、レシーバパラメータは型 T である。(コンパイラは暗黙的にレシーバの指す値を参照する。)
- 名前付き型 T のすべてのメソッドが T (*Tでなく)のレシーバを持つならその型のインスタンスをコピーすることは安全。メソッド呼び出しではコピーが行われるから。
- どれかのメソッドがポインタレシーバをもっていると、その名前付き型のインスタンスコピーは避けるべき。内部の不変式を破る可能性があるから。(コピーしたインスタンスがコピー元のインスタンス操作で変更されうるので。)
6.2.1 nil は正当なレシーバ値
- 特にマップやスライスのように nil がその型のゼロ値を意味する場合などあるため、nil をレシーバとして利用することは許されている。
.go
func (list *IntList) Sum() int {
if list == nil {
return 0
}
...
- レシーバ値として nil を許すメソッドを持つ型を定義する場合、ドキュメンテーションコメントで明示すべき。
■6.3 構造体埋め込みによる型の合成
- 構造体を埋め込んだ(無名フィールドを利用)構造体は、埋め込まれた構造体のフィールドに直接アクセスできる。埋め込まれた構造体のメソッドは埋め込んだ構造体に 格上げ される。
.go
type Point struct{ X, Y float64 }
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
type ColoredPoint struct {
Point // 無名フィールドとして Point を利用。
Color color.RGBA
}
var cp ColoredPoint
cp.X = 1 // OK
fmt.Println(cp.Point.X) // "1"
cp.ScaleBy(2) // OK
- **ColoredPoint と Point の関係は "is a"(継承関係) ではなく "has a"。**Point を引数にする関数やメソッドに ColoredPoint は使えない。
- 無名フィールドの型は名前付き型へのポインタでもよい。その場合もフィールドとメソッドは格上げされる。
- 埋め込みのおかげで名前なし構造体型がメソッドを持つことができる。以下の例は機能的に同じ実装だが、後者のほうが可読性が高くなる。
.go
var (
mu sync.Mutex
mapping = make(map[string]string)
)
func Lookup(key string) string {
mu.Lock()
v := mapping[key]
mu.Unlock()
return v
}
var cache = struct { // 名前なし構造体
sync.Mutex // sync.Mutex を埋め込み。
mapping map[string]string
} {
mapping: make(map[string]string)
}
func Lookup(key string) string {
cache.Lock()
v := cache.mapping[key]
cache.Unlock()
return v
}
6.4 メソッド値とメソッド式
- p.ScaleByなどは特定のレシーバ p に結びついたメソッド値と呼ぶ。メソッド値を利用するとレシーバ値なしで関数呼び出しできる。
.go
distanceFromP = p.Distance
fmt.Println(distanceFromP(q)) // p.Distance(q) と同じ呼び出し。
- 名前付き構造体 T があり、fというメソッドを持つ場合、T.f または (*T).f をメソッド式と呼ぶ。レシーバの代わりをする普通の第一パラメータを持つ関数値を生成する。
.go
distance := Point.Distance
fmt.Println(distance(p, q)) // p.Distance(q) と同じ呼び出し。
6.5 例:ビットベクタ型
- ビットベクタは符号なし整数値のスライス「複数ワード」を使う。個々のビットはセット内の可能な要素を表す。そのセットは i 番目のビットが設定されていれば i を含む。
- 演習問題 6.5 「64で割る代わりにunitの」は uint の間違い??
6.6 カプセル化
- Goは名前の可視性を制御する仕組みは一つしか持たない。大文字始まりだとパッケージ外へ公開され、大文字で始まっていない場合は公開されない。
- カプセル化は三つの利点を提供する。
- クライアントがオブジェクトの変数を直接修正できないためオブジェクトの変数が取り得る値を理解するために調べるコードが少なくなる。
- 詳細な実装の隠蔽によりクライアントは変更されるかもしれない事柄に依存しなくなり、APIの互換性を破ることなく実装を発展させる大きな自由を設計者へ与える。
- クライアントがオブジェクトの変数を勝手に設定するのを防ぐ。関数経由で変数を設定させるので、オブジェクトの内部的な不変式を維持することが保証できる。
- **フィールドが一旦公開されたらAPIに対する互換性を失うことなく非公開にすることはできない。**そのため最初の選択は慎重に検討されるべき。
練習問題解答例