Groovy + Spockでユニットテストを書いているが、Groovyの名前付き引数・名前付きパラメータを知らないメンバーがいた。
なかなか特殊な仕様なのでメモ。
環境
サクッとGroovy Web Consoleで確認。
println System.getProperty('java.version') println GroovySystem.version
してみたところ、
1.8.0_181-google-v7 2.5.7
だった。
仕様
ドキュメントにはサラッとした記述しかない。
昔は Named argument
と呼んでいたが、今見ると Named parameters
と呼ばれている。
いつから変わったのかと思ったら、 v2.5.3までは Named argument
、 v2.5.4で Named parameters
になった模様。
コンストラクタの名前付き引数
デフォルトコンストラクタが存在するか、第1引数にMapを取るコンストラクタが存在する場合、フィード名をキーとしたMapを渡すことで、名前付き引数として扱える。
また、GroovyでのMapリテラルは [key: value]
だが、名前付き引数呼び出し時であれば、 [
と ]
で囲む必要がなくなる。
// デフォルトコンストラクタの場合 @groovy.transform.ToString class ExampleClass1 { Integer id String name } assert new ExampleClass1().toString() == 'ExampleClass1(null, null)' assert new ExampleClass1(name: 'name').toString() == 'ExampleClass1(null, name)' assert new ExampleClass1(id: 1).toString() == 'ExampleClass1(1, null)' assert new ExampleClass1(name: 'name', id: 1).toString() == 'ExampleClass1(1, name)' // Mapを引数に取るコンストラクタの場合 @groovy.transform.ToString class ExampleClass2 { Integer id String name // デフォルト値として空のMapを指定するか、Nullセーフ演算子を使わないと、引数省略時にNullPointerExceptionが発生する ExampleClass2(Map parameters) { this.id = parameters?.id this.name = parameters?.name } } assert new ExampleClass2().toString() == 'ExampleClass2(null, null)' assert new ExampleClass2(id: 1, name: 'name').toString() == 'ExampleClass2(1, name)'
メソッドやクロージャの名前付き引数
メソッドやクロージャの場合も、第1引数にMapを取れば名前付き引数となる。
名前付き引数呼び出し時であれば、 [
と ]
が省略できるのもコンストラクタと同様。
// デフォルト値として空のMapを指定するパターン def ExampleMethod(Map parameters = [:]) { return [parameters.id, parameters.name] } assert ExampleMethod() == [null, null] assert ExampleMethod(id: 1) == [1, null] assert ExampleMethod(name: 'name') == [null, 'name'] assert ExampleMethod(id: 1, name: 'name') == [1, 'name']
注意点
第1引数にMapを取る場合、デフォルト値として空のMapを指定しても、明示的に null
を渡されると参照時に NullPointerException
が発生する。
null
が渡される可能性があれば、Mapの値の参照は、常にNullセーフ演算子 ?.
で行っておくのが無難。
名前付き引数のデフォルト値の設定
第1引数にMapを取る場合、デフォルト値としてMapを設定し、そこにkeyとvalueを指定すると、引数省略時はそのMapおよびkeyとvalueが使用される。
だが、引数の一部を指定した場合、未指定の引数はnullになる。
@groovy.transform.ToString class ExampleClass { Integer id String name ExampleClass(Map parameters = [id: 0, name: 'default']) { this.id = parameters.id this.name = parameters.name } } // 引数省略時はうまくいったように見える assert new ExampleClass().toString() == 'ExampleClass(0, default)' // 引数をすべて指定した場合も問題なし assert new ExampleClass(id: 1, name: 'name').toString() == 'ExampleClass(1, name)' // 引数の一部を指定した場合、未指定の引数はnullになる assert new ExampleClass(id: 1).toString() == 'ExampleClass(1, null)' assert new ExampleClass(name: 'name').toString() == 'ExampleClass(null, name)'
引数の一部を指定した場合でも、未指定の引数にデフォルト値を設定したい場合、コンストラクタやメソッド内でデフォルト値を設定したMapを用意し、そこに第1引数のMapをputAllするのがよさそう。
def ExampleMethod(Map parameters) { def params = [id: 0, name: 'default'] + (parameters ?: [:]) return [params.id, params.name] } assert ExampleMethod() == [0, 'default'] assert ExampleMethod(id: 1) == [1, 'default'] assert ExampleMethod(name: 'name') == [0, 'name'] assert ExampleMethod(id: 1, name: 'name') == [1, 'name']
名前付き引数と通常の引数の組み合わせ
第2引数以降に、通常の変数を宣言することも可能。
その場合、 key: value
形式で指定した引数は第1引数のMapに、それ以外の引数は第2引数以降に指定順に渡される。
必須としたい引数は第2引数以降で宣言し、必須ではない引数は名前付き引数にする、といったことが可能。
ただし、第1引数のMapにはデフォルト値を指定しておかないと、名前付き引数が1つも渡されていない場合、 MissingMethodException
が発生する。
Groovyの仕様では、引数のデフォルト値を指定すると、通常はそれ以降の引数にもデフォルト値を設定する必要があるが、この名前付き引数と通常の引数の組み合わせの場合、第2引数以降はすべて必須パラメータとなり、デフォルト値を指定できない(指定しても、省略してメソッドを呼び出せない)模様。
そのため、第2引数以降を省略したければ、メソッドをオーバーロードする必要がある。
def ExampleMethod(Map parameters = [:], int id, String name) { return [id, name, parameters.address, parameters.email] } def ExampleMethod(Map parameters = [:], int id) { return ExampleMethod(parameters, id, 'default') } assert ExampleMethod(1) == [1, 'default', null, null] assert ExampleMethod(1, 'name') == [1, 'name', null, null] assert ExampleMethod(address: 'tokyo', email: 'mail@example.com', 1) ==[1, 'default', 'tokyo', 'mail@example.com'] // 名前付き引数の宣言順は不定 assert ExampleMethod( email: 'mail@example.com', 1, 'name', address: 'tokyo' ) == [1, 'name', 'tokyo', 'mail@example.com']
ここの挙動について、ドキュメントが見つけられなかった。動きから逆算しているので、仕様とは説明が異なるかもしれない。
振り返り
いつの間にやら「名前付き引数と通常の引数の組み合わせ」を使っていたが、どういった経緯でこの方法を知ったかは思い出せない。
機能名としては「名前付き引数」で通したが、ドキュメントの英語からだと「名前付きパラメータ」が正式名称になるのかな?