読者です 読者をやめる 読者になる 読者になる

No Programming, No Life

新しいNPNLです。http://d.hatena.ne.jp/fumokmm/ から引っ越してきました。

Groovyのクロージャ生成時のパラメータ指定について

Groovy Tips

はじめに

クロージャって便利ですよね。私はクロージャと出会ったことで処理とイテレーションの分離の考え方を身に付けることができました。さて、今回はそのクロージャ生成時のパラメータ指定についてです。普段何気なく使ってしまっているクロージャですが、パラメータの受け渡し方などちょっとだけ奥が深いところがあった*1のでまとめてみました。間違いなどがありましたらご指摘ください。
(動作確認: Groovy Version: 1.8.0-rc-1 JVM: 1.6.0_22)

パターン1:パラメータ省略

def clos = { ... }

パラメータを省略した場合は第1引数を暗黙変数「it」で受け取れる。

使用例
def map = [a:1, b:2, c:3]
map.each{
  println "${it.key}/${it.value}"  // key/value形式で出力
}

パターン2:パラメータ指定

def clos = { arg1, arg2 -> ... }

クロージャを受け取るメソッドが望む分だけパラメータを指定してクロージャを生成する方法。

使用例
def map = [a:1, b:2, c:3]
map.each{ k, v ->
  println "$k/$v"  // key/value形式で出力
}

また複数パラメータを受け取る場合でも、少なめに受け取るように省略して宣言しても問題ない。
たとえば、String#eachLine()メソッドはクロージャに

  • 第1パラメータ:改行で区切った各行の文字列
  • 第2パラメータ:行番号(0オリジン) *2

というパラメータを渡すように実装されているようですが、第2パラメータは受け取らなくてもよいようです。

使用例
// 第1引数のみ受け取る
'''aaa
bbb
ccc'''.eachLine{ line ->
  println line // 行の内容を出力
}

// 第2引数も受け取る
'''aaa
bbb
ccc'''.eachLine{ line, no ->
  println "$no: $line" // 「行番号: 行の内容」 の形式で出力
}

パターン3:パラメータなし

def clos = { -> ... }

パターン3はあまり見ない形かもしれない。-> の左側を空にすることで明示的にパラメータがないことを示すことができる。
よって、パターン1と同じ感覚で使おうとするとエラーになる。間違ってパラメータを渡してくるようなメソッドにクロージャが渡された場合はエラーとなるので便利です。

使用例(エラーとなる)
def map = [a:1, b:2, c:3]
map.each{ -> // エラー発生 => #each() はクロージャにパラメータを渡してきてしまうため
  println 'パラメータなんて必要ないんだからねっ!'
}

なので、「引数は受け取らないよ!絶対受け取らないんだからねっ!」という時に使うと良い。

使用例
5.times { no ->
  Thread.start{ -> // スレッドだからパラメータはいらないよ!
    int sleepTime = 1000 * Math.random() as int
    sleep(sleepTime)
    println "I'm No.${no} thread (slept ${sleepTime}ms)."
  }
}

// 結果
// I'm No.0 thread (slept 130ms).
// I'm No.2 thread (slept 472ms).
// I'm No.1 thread (slept 552ms).
// I'm No.4 thread (slept 642ms).
// I'm No.3 thread (slept 997ms).

まとめ

ということで、パターン別にまとめてみましたがいかがでしたでしょうか。クロージャはGroovyの中でも奥が深い部分でもありますが、規則を覚えて使いこなしていけばこれほど便利な機能はないと思います。

*1:パターン3の使い方がよく分かってなかったので、まとめてみようと思ったのが今回の記事を書こうとしたきっかけです。

*2:これはString#eachLine(n, clos)のnにintを渡すことでオリジンを変更可能です。