Groovy 中提供了一个减少输入的特性叫做命名参数(Named Parameter)。GroovyBean 可以通过在构造器调用中传递冒号隔开的属性名称和值进行构建。如:
1 |
car = new Car(model : "BMW", color : "black"); |
其实类似的用法早已有之,这有如 C++ 中的初始化成员列表,VB、Python、Transact-SQL 中的命名参数,JavaScript 中的 JSON 写法,Ruby 中提供的 syntax sugar用 hash 模拟所谓的 keyword argument。
我们先详细介绍 Groovy 的命名参数的用法与本质,然而再附上其他一些语言类似用法的实例,纵向贯通以加深印象。
要说从外部表现上好像是先调用了空构造方法,然后是相应的 setter 方法进行设值。因此,我们所直接想像的应该相当于下列 Java 代码
1 2 3 |
Car car = new Car(); car.setModel("BMW"); car.setColor("black"); |
不过,假如你把 Groovy 生成的 class 文件反编译一下就会发现 Groovy 为上面那行生成了如下代码:
1 2 3 4 5 6 7 |
Class class1 = Car.class; Class class2 = groovy.lang.MetaClass.class; Car car = ((Car) (ScriptBytecodeAdapter.invokeNewN(class1, class1, ((Object) (new Object[] { ScriptBytecodeAdapter.createMap(new Object[] {//Groovy 把命名参数转换成一个对象数组 "model", "BMW", "color", "black" //然后放到 Map 中,通过相应的 setter 方法或属性名反射赋值 }) }))))); |
所以若再加以试验,就会知道那个 Car 必须要有一个空的构造方法,这是必要条件。但它们的属性值如果有相应的 setter 方法就用 setter 方法赋值,如果没有就直接通过反射进行设值。所以并不要求属性有相应的 setter 方法,甚至是私有属性而无相应的 setter 方法也不打紧。即使只有一个光头的 setter 方法,无对应属性也是可以的。--有点啰嗦,觉得有用的话,可尽力去理解。
Groovy 是通过 org.codehaus.groovy.runtime.ScriptBytecodeAdapter 来完成这一过程的,看到 Bytecode 就知道它大概做了一些不光明的事情。
前面看到了,一行代码可以完成多行代码的功能。迫不急切地,我们还是来点有实效性的东西,例如构造一个 JFrame 窗口:
1 2 3 4 5 6 |
JFrame frame = new JFrame( title:"Named Parameter", size:[400,300], location:new Point(300,200), defaultCloseOperation:JFrame.EXIT_ON_CLOSE, visible:true); |
对上面那样一个过程,我们可以一句话说完,简洁易懂。要领就在于你只要发现可用的属性(不管是私有的还是别的),或是 setXxx() 方法的那个 xxx (符合 JavaBean 规范即可) 就可以拿来作为命名参数的名字。
还应注意的是:在给 size 赋值时 size:[400,300] 使用到了隐式构造(Implicit constructors),size 原本接收的是一个 Dimension,而实质 [400,300] 就是隐式的调用了 new Dimension(400,300)。所以 location 属性也可以写成 [300,200]。隐式构造还常用于 Builder 中,如 SwingBuilder。
命名参数不仅可以应用于构造实例时,还能运用于普通方法调用上,而且这种机制了可以接受 Map 对象作为参数的方法:
例如,前面的那么代码可以写成:
1 |
car = new Car(["model":"BMW","color":"black"]) //小注:在 GroovyEclipse 中最后那个中括号 "]" 的输入有点难度 |
并且生成的字节码与原来完全一样,总之都是转换成对象数组,然后反射赋值。
再说一个接受 Map 参数的方法,以命名参数形式来调用的例子:
1 2 3 4 |
def desc(dog){ println dog.name; println dog.breed; } |
调用时可用以下两种形式,效果是完全一样的:
1 2 |
desc(name : "Lina", breed : "Labrador"); desc(["name" : "Lina", "breed" : "Labrador"]); |
为有助于理解,还是反编译出以上两行生成的 Java 代码(都是一样的):
1 2 3 4 5 6 7 |
Class class1 = Car.class; Class class2 = groovy.lang.MetaClass.class; ScriptBytecodeAdapter.invokeStaticMethodN(class1, class1, "desc", new Object[] { ScriptBytecodeAdapter.createMap(new Object[] { "name", "Lina", "breed", "Labrador" //不管怎么样,仍然是转换成对象数组,放到 Map 中,再逐个处理 }) }); |
用 Map 的形式只适用于键是字符串的情况。
前面例子中的属性值都是字符串,其实是可以接受任何的属性类型,例如:
1 |
car = new Car(manufactureDate: new Date(), height: 2); //假定 Car 有这两个属性 |
注意:前面的代码以都是在 Groovy 1.5.6 版本中测验过,以及这一版本生成的字节码;可能其他版本生成的字节码略有不同,但执行结果应不会有差异。
附录:(对比其他几个语言的命名参数用法)
这里只是说类似,但是千万要注意 Groovy 的命名参数不能像 VB/Python/Transact-SQL 那样,调用方法时指示哪个参数是什么。Groovy 的命名参数是针对于类的属性(或者是 setter 方法)或 Map 的 Key。
1) C++ 的初始化成员列表:
1 2 3 |
Child::Child(char* surname):Parent(surname),name(10),height(20) { //surname 同时传递给父构造方法,并初始化成员变量 name 和 height }; //似乎 C++ 的东西总是那么深不可测,这里只见识一下它的这一特性即可 |
2) VB 的命名参数:
1 2 3 4 5 6 7 |
Sub say(Optional code As String, Optional name As String) MsgBox "Hello " & code End Sub Private Sub Command1_Click() say code:="Unmi" End Sub |
3) Ruby 的做法:ruby 其实没有所谓的 keyword argument,而是提供一个syntax sugar用 hash 模拟。
1 2 3 4 5 |
def image(opt={}) default_opt = {:height => 25, :width => 10} default_opt.merge! opt #opt中同样key的內容会覆盖default_opt中key的value end |
4) Transact-SQL 中的命名参数:
1 |
my_proc @second = 2, @first = 1, @third = 3 |
其他像 Python、JavaScript 等各种语言的命名参数的用法就没必要继续列了。但据此我们能体验到这一特性确有其方便可取之处。
参考:1. 《Java 脚本编程,语言、框架与模式》第 4 章
2. 《Groovy in Action》第 7 章 Dynamic object orientation, Groovy style
3. C++初始化成员列表(member initialization list)
4. Ruby慣寫法
5. Python 使用可选参数和命名参数
本文链接 https://yanbin.blog/unmi-study-groovy-named-paramter/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
其实map的key如果是字符串的话可以不用加引号,所以
["name" : "Lina", "breed" : "Labrador"]
可以写成
[name : "Lina", breed : "Labrador"]
讲的好透彻亚,我之前完全没有注意到builder跟命名参数是一回事。
@Johnny
不加引号的做法我不太喜欢,有时要用变量时就会写成[name:name, breed:breed] 看起来总觉得怪怪的,思路要卡一下。
@Johnny
我基本能认可把 Map 写成
[name : "Lina", breed : "Labrador"]
形式,这样的话,也就能与
car = new Car(model : "BMW", color : "black");
中的写法达成一致
如果是在一个项目中,对于这种过于自由的脚本,还是必须制定一个规范的,如
Map 和命名参数中的命名的引号可以省去。因为[key:value] 这种写法的 Map 的 key 只允许为字符串。
现在还在对 Groovy 的研究阶段,也不知何时能真正能运用于项目中。从网上来看,Groovy 还是受不少人的关注,一起共同进步吧。
@Feng
[name:name, breed:breed]
不加引号是指 key,value 部分是一定要加上引号,或者别的明确类型。
写成
[name:"name", breed:"breed"]
的话,相信思路上不会卡一下的,这等同于通常的命名参数了。
@隔叶黄莺
如果key不是字符串的话,可以写成[(key):value],在我的博客中也有提过:
http://johnnyjian.javaeye.com/blog/196423
@Feng
可以看看Grails自动生成的controller的代码,它也是倾向于不加引号的key。
@Johnny
谢谢,长见识了,看那本书上却是说只支持字符串的 Key,它也没说明是用的哪个版本的 Groovy。
@Feng
后来慢慢一想,我还是觉得隐匿构造应该不属于命名参数的范畴。
隐式构造是指定一个参数列表,由 Groovy 去隐式的依据参数去调用相应的构造方法。它只负责构造实例。
隐式构造时,从上文需要明确知道构造的是什么类型的实例。例如:处在一个方法的参数位置上,这个参数要求的类型。或者是直接指定了类型。所以
Dimension dim = [200,300];
它就知道是把列表 [200,300]; 转换成一个 Dimension 实例,如果写成
def dim = [200,300];
的话,dim 仅仅是一个列表。
说白了隐式构造就是根据上下文环境,把一个列表转换成所要求的实例。
Groovy 的这种隐式构造倒是有点像 C/C++ 语言的结构的构造方式,如
OneStruct os = {200,300}
@隔叶黄莺
[name:name, breed:breed]
这样的写法,我是想说有变量叫name和breed的情况,而通常我也确实习惯使用这样的命名。这时候就需要特别注意到前一个name是字符串,而后一个name是变量名。
关于Builder,我想我是用错词了,应该是Markup。又去看了看文档,这个的确大量使用了命名参数,但是嵌套的方式就跟命名参数没关系了,不能说是一回事情。
非常感谢,学习到了,开始看grails中生成的controller
看得晕晕乎乎,知道闭包,再看了楼主的帖子,清晰了很多