2.5 オブジェクト指向
前の2つの章で関数とstructをご紹介しました。関数をstructのフィールドとして処理したくなったんじゃないですか?今日は関数のもう一つの形態についてご説明します。受け取り手のいる関数で、我々がmethod
とよんでいるものです。
method
今、このような状況にいると仮定します。あなたは長方形というstructを定義してこの面積を求めようとしています。我々の一般的な思考回路に基づけば下のような方法で実現するでしょう。
package main
import "fmt"
type Rectangle struct {
width, height float64
}
func area(r Rectangle) float64 {
return r.width*r.height
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
fmt.Println("Area of r1 is: ", area(r1))
fmt.Println("Area of r2 is: ", area(r2))
}
このコードは長方形の面積を求めることができますが、area()はRectangleの(一般的なオブジェクト指向でいうような)メソッドで実現されたものではありません。Rectangleのオブジェクト(ここではr1,r2)を引数に面積を計算する関数に渡しているだけです。
このように実現してももちろん構わないのですが、図形が増えてきて、正方形、五角形ついには多角形になってきた頃、これらの面積も求めようとするとどうでしょう?この場合関数を増やすしかなくなってしまいます。関数名はそれぞれ用意しなければなりません。area_rectangle, area_circle, area_triangle...
といった具合に。
下の図で示すように、楕円が関数を表しています。これらの関数はstructに属していない(オブジェクト指向用語で言い換えるとclassに属していない)ので、structの外側に単独で存在しており、概念上どのstructにも属していないことになります。
図2.8 メソッドとstructの関係図
明らかにこのような実現方法はエレガントではありません。それに概念からしても"面積"は"形状"の一属性です。これはある特定の形状に属しています。長方形の縦と横と同じようなものです。
このような理由からmethod
の概念が生まれました。method
はある型に属しています。この文法と関数の宣言の文法はほとんど同じです。ただ、func
の後にreceiver(methodがくっついているということです)を追加します。
上で述べた形状の例からすると、method area()
はある形状(たとえばRectangle)に由来して発生しています。Rectangle.area()の主語はRectangle、area()はRectangleに属するメソッドで外側の関数ではありません。
より具体的に述べると、Rectangleにはフィールドlengthとwidthが存在します。同時にarea()メソッドが存在します。これらのフィールドとメソッドは共にRectangleに属しています。
Rob Pikeの言葉を借りると:
"A method is a function with an implicit first argument, called a receiver."
methodの文法は以下のとおりです:
func (r ReceiverType) funcName(parameters) (results)
はじめの例をとってmethodを実現してみます:
package main
import (
"fmt"
"math"
)
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
func (r Rectangle) area() float64 {
return r.width*r.height
}
func (c Circle) area() float64 {
return c.radius * c.radius * math.Pi
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
c1 := Circle{10}
c2 := Circle{25}
fmt.Println("Area of r1 is: ", r1.area())
fmt.Println("Area of r2 is: ", r2.area())
fmt.Println("Area of c1 is: ", c1.area())
fmt.Println("Area of c2 is: ", c2.area())
}
methodを使う時にはいくつか注意が必要です。
- methodはまったく同じ名前でもレシーバが異なればmethodも異なります。
- methodはレシーバのフィールドにアクセスすることができます。
- methodの呼び出しは
.
を通じて行います。structがフィールドにアクセスするのと同じです。
図解:
図2.9 異なるstructのmethodは異なる。
上の例では method area() はそれぞれRectangleとCircleに属します。この時これらの Receiver は Rectangle と Circleになります。またはこのarea()メソッドはRectangle/Circleを主語とします。
特に、図中のmethodは破線で表示しています。これは、このメソッドのレシーバは値渡しであり、参照渡しではありません。そうです。レシーバはポインタでもいいのです。両者の違いはポインタはレシーバがエンティティの内容に操作を行うことがあるのに対し、普通の型ではレシーバは操作するオブジェクトのコピーでしかありません。オリジナルのエンティティに対して操作が発生しないのです。詳細は後述します。
methodはstructの上でしか使用されないのでしょうか?当然違います。これはカスタム定義型、ビルトイン型、structなどあらゆる型でも定義することができます。ちょっとよくわからなくなってきましたか?何がカスタム定義型だ、カスタム定義型はstructじゃないのか。そういうわけではありません。structはカスタム定義型のなかでも比較的特殊な型であるだけです。下のような宣言で実現します。
type typeName typeLiteral
以下のカスタム定義型の宣言のコードをご覧ください。
type ages int
type money float32
type months map[string]int
m := months {
"January":31,
"February":28,
...
"December":31,
}
わかりましたか?簡単でしょう?このように自分のコードの中に意味のある型を定義することができるのです。実際はエイリアスを定義しているだけです。Cのtypedefに似たようなもので、例えば上のagesはintの代わりになっています。
それじゃあ、method
にもどりましょう。
カスタム定義型の中で任意のmethod
を定義することができます。次にちょっと複雑な例を見てみましょう。
package main
import "fmt"
const(
WHITE = iota
BLACK
BLUE
RED
YELLOW
)
type Color byte
type Box struct {
width, height, depth float64
color Color
}
type BoxList []Box //a slice of boxes
func (b Box) Volume() float64 {
return b.width * b.height * b.depth
}
func (b *Box) SetColor(c Color) {
b.color = c
}
func (bl BoxList) BiggestColor() Color {
v := 0.00
k := Color(WHITE)
for _, b := range bl {
if bv := b.Volume(); bv > v {
v = bv
k = b.color
}
}
return k
}
func (bl BoxList) PaintItBlack() {
for i, _ := range bl {
bl[i].SetColor(BLACK)
}
}
func (c Color) String() string {
strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
return strings[c]
}
func main() {
boxes := BoxList {
Box{4, 4, 4, RED},
Box{10, 10, 1, YELLOW},
Box{1, 1, 20, BLACK},
Box{10, 10, 1, BLUE},
Box{10, 30, 1, WHITE},
Box{20, 20, 20, YELLOW},
}
fmt.Printf("We have %d boxes in our set\n", len(boxes))
fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String())
fmt.Println("The biggest one is", boxes.BiggestColor().String())
fmt.Println("Let's paint them all black")
boxes.PaintItBlack()
fmt.Println("The color of the second one is", boxes[1].color.String())
fmt.Println("Obviously, now, the biggest one is", boxes.BiggestColor().String())
}
上のコードはconstでいくつかの定数を定義しています。その後カスタム定義型を定義しています。
- Colorはbyteのエイリアスです。
- struct:Boxを定義します。3つの縦横高さのフィールドと色プロパティを持っています。
- slice:BoxListを定義します。Boxを持っています。
次に上のカスタム定義型をレシーバとしてmethodを定義します。
- Volume()はレシーバをBoxとして定義します。Boxの体積を返します。
- SetColor(c Color)はBoxの色をcに変更します。
- BiggestColor()はBoxListに定義されており、listの中の体積が最大の色を返します。
- PaintItBlack()はBoxListのすべてのBoxの色を全部黒に変更します。
- String()はColorに定義されており、Colorの具体的な色を返します(文字列形式)
上のコードは文字で書くと非常に簡単に思えませんか?私達は問題を解決する場合問題の描写を通して、対応するコードを書くことで実現します。
ポインタとしてのreceiver
ではここで、SetColorのメソッドを見なおしてみましょう。このreceiverはBoxのポインタをさしています。そうです。*Boxを使えるのです。どうしてBox本体ではなくポインタを使うのでしょうか?
SetColorを定義した本当の目的はこのBoxの色を変更することです。もしBoxのポインタを渡さなければ、SetColorが受け取るのは実はBoxのコピーになってしまいます。つまり、メソッド内で色の変更を行うと、Boxのコピーを操作しているだけで、本当のBoxではないのです。そのため、ポインタを渡す必要があります。
ここではreceiverをメソッドの第一引数にしました。こうすれば前の関数で説明した値渡しと参照渡しも難しくなくなるでしょう。
もしかしたらSetColor関数の中で以下のように定義すべきじゃないかと思われたかもしれません。*b.Color=c
、ところがb.Color=c
でよいのです。ポインタに対応する値を読み込むことが必要ですから。
そのとおりです。Goの中ではこの2つの方法はどちらも正しいのです。ポインタを使って対応するフィールドにアクセスした場合(ポインタになんのフィールドがなかったとしても)、Goはあなたがポインタを通してその値を必要としていることを知っています。どうです。Goのデザインに魅了されてきたんじゃないですか?
注意深い読者はこのように思うかもしれません。PointItBlackの中でSetColorをコールした時、ひょっとして(&bl[i]).SetColor(BLACK)
と書かなければならないんじゃないかと。SetColorのreceiverは*Boxであり、Boxではありませんから。
ええ、その通りなんです。この2つの方法はどちらでもかまいません。Goはreceiverがポインタであることを知っています。こいつは自動的に解釈してくれるのです。
つまり:
もしメソッドのreceiverが*Tであれば、T型のエンティティの変数V上でこのメソッドをコールすることができます。&Vによってメソッドをコールする必要はありません。
同じように
もしメソッドのreceiverがTであれば、T型の変数P上でこのメソッドをコールすることができます。Pを使ってメソッドをコールする必要はありません。
ですので、コールしているポインタのメソッドがポインタのメソッドであるかどうかは気にする必要がありません。Goはあなたが行おうとしているすべてのことを知っているのです。C/C++でプログラムを経験されてこられた方にとっては、とてもとても大きな苦痛が解決されることでしょう。
method継承
前の章でフィールドの継承を学びました。するとあなたはGoの不思議なところに気がついたかもしれません。methodも継承できるのです。もし匿名フィールドが一つのメソッドを実現している場合、この匿名フィールドを含むsturctもこのメソッドをコールすることができるのです。例をお見せします。
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名フィールド
school string
}
type Employee struct {
Human //匿名フィールド
company string
}
//human上でメソッドを定義
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
methodの書き直し
上の例で、もしEmployeeにSayHiを実現したい場合はどうすればよいでしょうか?簡単です。匿名フィールドの衝突と同じ道理で、Employee上でもメソッドを定義することができます。匿名フィールドを書き直す方法は下の例をご確認ください。
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名フィールド
school string
}
type Employee struct {
Human //匿名フィールド
company string
}
//Humanでmethodを定義
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//EmployeeのmethodでHumanのmethodを書き直す。
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
上のコードのデザインはこのように絶妙です。Goのデザインに驚くことでしょう。
このように、基本的なオブジェクト指向のプログラムを設計することができます。ですが、Goのオブジェクト指向はこのように簡単です。プライベートやパブリックといったキーワードは出てきません。大文字と小文字によって実現しているのです(大文字で始まるものはパブリック、小文字で始まるものはプライベートです)、メソッドにも同じルールが適用されます。