Skip to content

Instantly share code, notes, and snippets.

@yyoshiki41
Last active October 5, 2016 19:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save yyoshiki41/7d9e60d7aaa4668fbee77fd6cc04e2c4 to your computer and use it in GitHub Desktop.
Save yyoshiki41/7d9e60d7aaa4668fbee77fd6cc04e2c4 to your computer and use it in GitHub Desktop.

6.1 メソッドの宣言

メソッドは、関数名の前に特別なパラメータを置いて宣言されます。 そのパラメータは、関数にその型のパラメータを付与します。

私達の最初のメソッドを見ていきましょう。

https://github.com/adonovan/gopl.io/blob/master/ch6/geometry/geometry.go

func (p Point) Distance(q Point) float64

特別なパラメータであるpは、メソッドのレシーバと呼ばれ、初期のオブジェクト志向言語からの遺物では、"オブジェクトにメッセージを送る(sending a message to an object)"と呼ばれます。

c.f. OCaml

Goでは、thisselfのような特別な名前を使いません。私たちは、他のパラメータと同じようにレシーバの名前を選べます。

レシーバの名前は頻繁に使われるので、なるべく短く、かつ全てのメソッドで統一されているのが良いでしょう。一般的なものは、最初の文字を取るものです。例えば、Pointであれば、pのように。

メソッド呼び出しにおいて、メソッド名の前にレシーバがおかれます。これは、レシーバがメソッド名の前に現れるという点で、メソッドの宣言と似ています。

p := Point{1, 2}
q := Point{4, 5}
fmt.Println(Distance(p, q))
fmt.Println(q.Distance(q))

上の2つに、コンフリクトはありません。1つ目は、パッケージレベルのgepmetry.Distanceという関数の呼び出しで、2つ目は、Pointという型のメソッドの呼び出しです。

p.Distanceという表現は、selector と呼ばれます、それはPoint型のpの為に、適切なDistanceというメソッドを選択しているからです。セレクターは、p.Xのような構造体のフィールドを選択することとしても使われます。メソッドとフィールドは、同じ名前空間に存在していて、Point型の構造体のXというメソッドを宣言することは、不明瞭でコンパイラがリジェクトします。

それぞれの型はメソッドの為のそれぞれの名前空間を持っており、他の型に属している限り、Distanceという名前を使うことが出来ます。

type Path []Point

// Distance returns the distance traveled along the path.
func (path Path) Distance() float64 {
	sum := 0.0
	for i := range path {
		if i > 0 {
			sum += path[i-1].Distance(path[i])
		}
	}
	return sum
}

Pathはslice型で、Pointのような構造体ではない為、メソッドを定義することが出来ます。どんな型に対してもメソッドを紐付けれる点において、Goは他の多くのオブジェクト志向言語とは異なります。それは時々、numbers,strings,slices,mapsなどのようなシンプルな型に対して、付加的な振る舞いを定義出来て便利です。メソッドは、同じパッケージ内で定義されているいかなる型にも宣言できます、基底型がポインターやインターフェイスであるものではない限り。

perim := Path{
  {1, 1},
  {5, 1},
  {5, 4},
  {1, 1},
}
fmt.Println(perim.Distance())

特定の型に対する全てのメソッドはユニークでないといけません、しかし、型が異なる場合、同じ名前のメソッドが使えます。曖昧さを避けるために、例えばPathDistanceのように修飾する必要はありません。ここに、通常の関数を使う以上にメソッドを使う一つ目の利益があります: メソッドの名前は短い。この利益は、パッケージの外を出ると更に拡大します、短い名前でかつ、パッケージ名を省略できます。

6.2 ポインタをレシーバーとするメソッド

関数を呼び出すと各引数のコピーを作成するので、もし変数を更新する必要やコピーを出来るなら避けたいぐらい大きい引数の場合、ポインタを使って変数のアドレスを渡すべきです。レシーバの値を更新する必要のあるメソッドの場合も同様です。*Pointのようにポインタにメソッドをアタッチします。

func (p *Point) ScaleBy(factor float64) {
  p.X *= factor
  p.Y *= factor
}

このメソッドの名前は、(*Point).ScaleByです。 括弧()は必要です。これがなければ、*(Point.ScaleBy)と解釈されてしまうからです。

現実世界のプログラムでは、慣習的にPointのメソッドがポインタをレシーバとしているなら、全てのPointのメソッドをポインタをレシーバとすべきです、たとえ一つでも厳格にそれが必要ではないとしても。

(*Point).ScaleByメソッドは*Pointをレシーバとして以下のように、呼び出せます。

r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r)
p := Point{1, 2}
pptr := &p
pptr.ScaleBy(2)
fmt.Println(p)
p := Point{1, 2}
(&p).ScaleBy(2)
fmt.Println(p)

しかし、2,3は不格好である。幸運にもここでは言語がサポートしてくれているが。レシーバーのpPointという型の値だったとしても、以下の短い呼び出しで使うことが可能である。

p.ScaleBy(2)

そして、コンパイラは暗黙的に変数を&pと解釈して振る舞う。 これは、p.Xのような構造体のフィールドやperim[0]のような配列やスライスを含む、値のときのみ作用します。 私たちは*PointのメソッドをレシーバーのPointをアドレス指定が出来ない場合に呼び出すことが出来ません、それは一時変数のアドレスを確保することが出来ないためです。

Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal

しかし、私たちはPoint.Distanceという形で*Pointをレシーバとするメソッドを呼び出し可能です、それは変数からアドレスを知る方法があるからです。 コンパイラは暗黙的に*という修飾子を挿入してくれます。下記の2つの関数呼び出しは等価です。

pptr.Distance(q)
(*pptr).Distance(q)

もし全てのメソッドが、*TではなくTという型をレシーバとしていた場合、その型のインスタンスのコピーを安全に行い、全てのメソッド呼び出しでコピーが必要となります。 例えば、time.Durationは、大量にコピーされています、関数の引数として。

https://golang.org/src/time/time.go

しかし、もしポインタをレシーバとするメソッドがあるなら、Tというインスタンスをコピーすることを避けるべきです。それは内部の不変条件(internal invariants)に反するためです。

6.2.1 Nil は、レシーバとして有効な値

幾つかの関数は、、それらをレシーバとするメソッドのために、nilポインタを引数として認めています。特にnilはmapsやslicesのゼロバリューを意味する場合です。

下の例では、nilは空のlistを表します。

// An IntList is linked list of intergers.
// A nil *IntList represents the empty list.
type IntList Struct {
	Value int
	Tail *IntList
}

func (list *IntList) Sum() int {
	if list == nil {
		return 0
	}
	return list.Value + list.Tail.Sum()
}

レシーバーの値としてnilを許可するメソッドをもつ型を定義したなら、上のように明示的にドキュメンテーションコメントとして書いておくべきです。

net/urlパッケージのValuesもこのように定義されています。

https://golang.org/src/net/url/url.go

https://github.com/adonovan/gopl.io/blob/master/ch6/urlvalues/main.go

6.3 構造体埋め込みによる型の委譲

type Point struct{ X, Y float64 }

type ColoredPoint struct {
	Point
	Color color.RGBA
}

ColoredPointPointを埋め込むことで、X,Y`を含む、3つのフィールを持つ構造体を定義できます。

Pointと明示的に書かずとも、Pointのフィールドを扱うことが出来ます。

var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X)
cp.Point.Y = 2
fmt.Println(cp.Y)

同じメカニズムはPointのメソッドに対しても適応出来ます。ColoredPointをレシーバとして、埋め込んだPointのもつメソッドを呼び出し可能です。

https://github.com/adonovan/gopl.io/blob/master/ch6/coloredpoint/main.go

オブジェクト志向言語に親しんだ読者の型なら、Pointをベースクラスとして、ColoredPointがサブクラス、派生クラスと考えたり、まるでColoredPointPointであるかのように解釈するでしょう。しかしそれらは間違いです。上で、Distanceを呼び出した時に注目してください。DistancePoint型をパラメータとしています、そして、qPoint型ではなく、埋め込みとしてPointのフィールドを持っています、私たちは明示的にそれを指定しなければいけません。qを渡すとエラーが発生します。

p.Distance(q) compile error: cannnot use q (ColoredPoint) as Point

ColoredPointPointではありませんが、Pointを持っています。DistanceScaleByというPointから割り当てられた2つのメソッドを持ちます。もし、あなたが実装の観点から考えるなら、委譲によるフィールドはコンパイラに追加の下記と等価のラッパーメソッドを追加させるよう命令します。

func (p ColoredPoint) Distance(q Point) float64 {
	return p.Point.Distance(q)
}

func (p *ColoredPoint) ScaleBy(factor float64) {
	p.Point.ScaleBy(factor)
}

一つ目のラッパーメソッド内で、Point.Distanceが呼ばれたとき、レシーバーの値はPではなくp.Pointなので、Pointが埋め込まれたColoredPointにアクセスする術はありません。無名フィールドの型はポインタであるべきでしょう、このようなケースでフィールドやメソッドがオブジェクトを指し示すことで、直接参照できるようになるので。 共通の構造体を共有して、オブジェクト間を直接変更することも出来ます。

type ColoredPoint struct {
	*Point
	Color color.RGBA
}

p := ColoredPoint{Point{1, 1}, red}
q := ColoredPoint{Point{5, 4}, blue}
q.Point = p.Point // p and q now share the same Point

構造体は無名フィールドを一つ以上持つことも出来ます。

type ColoredPoint struct {
	Point
	color.RGBA
}

上の型は、PointRGBAの全てのメソッドをもち、追加のメソッドもColoredPointから直接、宣言できます。 もし、同じランクから2つのメソッドが割り当てられた場合、セレクターが曖昧なので、コンパイラはエラーを返します。

埋め込みのおかげで、無名の構造体を使ってメソッドを実装することは時々便利です。 以下では、mapとそれをガードするmutexの良い例を示します。

var cache = struct {
	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 := Point{1, 2}
q := Point{4, 6}

distanceFromP := p.Distance // method value
fmt.Println(distanceFromP(q)) // "5"
var origin Point {0, 0}
fmt.Println(distanceFromP(origin)) // "2.23606797749979", √5

メソッド変数は、パッケージAPIとして関数変数を呼び出す際に便利です、クライアントは特定のレシーバのメソッドを関数のように振る舞うことを期待しています。

type Rocket struct { /* .... */ }
func (r *Rocket) Launch() { /* ... */ }

r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() })

メソッドバリューシンタックスはもっと短い。

time.AfterFunc(10 * time.Second, r.Launch)

メソッド変数に関係しているものとして、メソッド式があります。メソッドを呼び出した際、通常の関数とは対象的に、セレクターのシンタックスを用いてレシーバーを指定する必要があります。

p := Point{1, 2}
q := Point{4, 6}

distance := Point.Distance // method expression
fmt.Println(distance(p, q)) // "5"
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"

scale := (*Point).ScaleBy
scale(&p, 2)
fmt.Println(p) // "{2, 4}"
fmt.Printf("%T\n", scale) // "func(*Point, float64)"

同じ型に属している複数のメソッドの中から、メソッドを選択して変数として表す必要がある際に便利です。

6.5 ビットベクタータイプ

https://github.com/adonovan/gopl.io/blob/master/ch6/intset/intset.go

6.6 カプセル化

もしクライアントからアクセス不可にしたい場合、オブジェクトの変数やメソッドは、カプセル化すべきだと言われます。カプセル化は、時々_情報隠ぺい_と言われ、オブジェクト志向の重要項目です。

Goはそのメカニズムを命名で制御します。:大文字で始まる識別子はパッケージ外に持ち出すことが出来、大文字で始まらないものはそうではない。同じメカニズムでパッケージ内の構造体のフィールドやメソッド等のメンバへのアクセスも制御しています。結果的にオブジェクトをカプセル化するために、構造体を作らなければならない。

このため、先程のIntSetは一つのフィールドしか持たないが、構造体として定義されている。

type IntSet struct {
	words []uint64
}
type IntSet []uint64

上記のように定義しても本質的には同じだが、これは他のパッケージのクライアントから、sliceを直接変更することを許可する。

ネームベースのメカニズムの違う側面として、カプセル化のまとまりはパッケージ単位で、他の多くの言語と違い型単位ではない。構造体のフィールドは同じパッケージ内からは見えている。

カプセル化には3つの利点がある。 一つ目は、クライアントが直接オブジェクトの値を変更できないので、変数の変更される可能性を理解出来少ないステートメントステートメントの検査で済む。 二つ目は、実装を隠蔽することで、クライアントが実装に依存してしまうことを防ぎ、デザイナーにAPIの互換性を壊すことなく内部の実装を進化させる自由をもたらす。 三つ目は、多くのケースで最も重要で、クライアントが任意でオブジェクト変数を設定することを防ぐ。オブジェクトの変数は同じパッケージ内の関数によって設定されるので、パッケージの作成者がオブジェクトの不変性をこれらの関数によって保持できる。

getterメソッドに命名する際、私たちは大抵Getプレフィックスを省略する。これらの参照は簡潔で、全てのメソッドに拡張でき、フィールドへのアクセサーだけでなく、Fetch, FindLookupのような他の冗長なプレフィックスでもそうである。

https://golang.org/src/log/log.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment