メニュー

2013年12月25日

Go言語の関数とメソッドのちょいネタ

Go Advent Calendar 2013-12-25 の記事です。

Go言語の関数とメソッドについてです。
前半はおさらい的な内容で、後半でちょっとしたネタを紹介します。

関数

まず普通の関数です。
func hello() {
    fmt.Println("Hello, Gophers")
}

関数を変数に代入

関数を変数に代入することも出来ます。
f := hello
f();    // "Hello, Gophers"

匿名関数

関数リテラルで匿名関数を定義することが出来ます。
f := func() {
    fmt.Println("Hello, Gophers")
}
f();    // Hello, Gophers

メソッド

レシーバーと関連付けた関数をメソッドと言います。
type Test struct {
    Name string
}

func (t Test) hello() {
    fmt.Printf("Hello, %s\n", t.Name)
}

func main() {
    t := Test{Name: "Gophers"}
    t.hello()    // "Hello, Gophers"
}

メソッドを変数に代入

メソッドも関数と同じように変数に代入することができます。
t := Test{Name: "Gophers"}
f := t.hello
f();    // "Hello, Gophers"
このとき、レシーバーの値はメソッドと関連付けて保持されます。

レシーバーの型

このあたりから本番です。

メソッドを変数に代入したあとに元の値を書き換えると……
t := Test{Name: "Gophers"}
f := t.hello
f();    // "Hello, Gophers"

t.Name = "Go Rangers"

f();    // "Hello, Gophers" になる。"Hello, Go Rangers" ではない!
おや、"Hello, Go Rangers" にならずに "Hello, Gophers" となりました。

これは`hello`メソッドのレシーバーがポインタではなく値だからです。

変数`f`のレシーバーは、メソッドを変数に代入した時にコピーされており、`Name`の中身はGophersになっています。その後で行われた変数`t`への操作の影響は受けません。

レシーバーの型をポインタにしてみると
func (t *Test) helloPtr() {
    fmt.Printf("Hello, %s\n", t.Name)
}
f := t.helloPtr
f();    // "Hello, Gophers"

t.Name = "Go Rangers"

f();    // "Hello, Go Rangers"
メソッドを変数に代入したあとの、Nameへの変更が反映されています。変数`f`にバインドされているのはポインタになっているからです。

レシーバーの型が値なのかポインタなのかは、メソッド内でレシーバーの中身を書き換える場合にも関係しますので、意識しておきましょう。

nilに対してメソッドを呼び出す

さて、レシーバーがポインタの場合にnilに対してメソッドを呼び出してみましょう。

以下の例だと変数`c`はnilです。その変数`c`からメソッド`hello`を呼び出します。
type Cat struct {
    Name string
}

func (c *Cat) hello() {
    fmt.Println("myaa")
}

func main() {
    var c *Cat    // c は nil
    c.hello()    // myaa
}
ぬるぽ……にならず、この例だとpanicも起きずに動作します。

なぜならば、nilポインタとなっている変数`c`の中身が使われないので、不正なアドレスへのアクセスが起きないためです。

変数`c`の中身はnilですが、変数`c`自体は実態を持っていますのでメソッドが呼び出せるわけです。

ちなみに変数`c`の中身を使おうとするとpanicします。
func (c *Cat) hello2() {
    fmt.Printf("myaa, %s\n", c.Name)    // panic: runtime error: invalid memory address or nil pointer dereference
}
これはnilのアドレスを解決しようとしているので、当然の動作です。

メソッドに違う方法でアクセスしてみる

さて、また違った例を見てみましょう。
type Sample struct {
    Name string
}

func (s Sample) hello() {
    fmt.Printf("Hi %s\n", s.Name)
}

func main() {
    s := Sample{"Gophers"}
    Sample.hello(s)    // Hi Gophers
}
変数`s`を通してではなく、`Sample.hello`という関数名を通してメソッド呼び出しています。

`Sample.hello`の型は`func(Sample)`となっていて、第一引数に`Sample`を受け取るので、上記の例のように呼び出すこともできます。

レシーバーがポインタの場合は以下のようになります。
func (s Sample) helloPtr() {
    fmt.Printf("Hi %s\n", s.Name)
}
sp := &Sample{"Gophers"}
(*Sample).helloPtr(sp)    // Hi Gophers
こちらもレシーバー型とメソッド名から、メソッドを取得できています。`(*Sample).helloPtr`の型は`func(*Sample)`です。

もちろん、変数に代入することも出来ます。
fv := Sample.hello
fp := (*Sample).helloPtr

処理の対象となるオブジェクトが定まっていないけれど、メソッドを変数に入れておいて使いたい、という時に使えそうです。

interfaceでも同様のコードを書くことが出来ます。
type Duck struct {}

type Sounder interface {
    Sound()
}

func (d *Duck) Sound() {
    fmt.Println("quack")
}

func main() {
    var f func(Sounder) = Sounder.Sound
    d := &Duck{}
    f(d)    // quack
}

感想など

メソッドの型は、第一引数に構造体を受け取る関数と同じになっています。変数を通してメソッドを呼び出せますが、ある種のシンタックスシュガーと言えるかもしれません。

もちろんinterfaceの定義を満たしているかどうかによるサブタイピングがあるので、メソッドを定義する意味はありますし、使わない手はありません。

以上、ちょっとしたネタでしたが、知っておくといつか役に立つと思います。

参考

- http://golang.org/ref/spec#Function_literals
- http://golang.org/ref/spec#Method_expressions
- http://golang.org/ref/spec#Method_values

0 件のコメント:

コメントを投稿