现在的位置: 首页 > 综合 > 正文

Scala 2.8的新特性:具名参数

2014年02月20日 ⁄ 综合 ⁄ 共 4440字 ⁄ 字号 评论关闭

首先先简单的说一下什么是具名参数(named argument),然后我们再谈Scala 2.8中引入的这个新特性。

 

什么是具名参数(Named Argument)

 

假设我们现在有这样的一种情况:在我们的程序中有一个方法,它有两个类型一样的参数,比如int sub(int minuend, int subtrahend),很明显,这是一个用来计算减法的方法,第一个参数是被减数,第二个参数是减数,而返回值就是两个数相减的结果。现在我们调用sub(100,32)来计算100减32,应该得到结果68。但是如果我们在调用方法的时候将被减数与减数搞混了,这样就成了sub(32, 100),得到的结果就是-68了。因为两个参数的类型是一样的,编译器不会对这种调用报任何的错误信息,这种错误一旦发生了,寻找起来会非常的困难。

当然了,我们可能不会在调用一个sub方法时像我一样犯上面这么幼稚的错误。但是想一想,如果我们的方法拥有很多相同类型的参数呢?如果我们传入的值是来自于其它的方法呢?只要任何的一个小差错就会造成bug。这个时候,具名参数(named argument)就非常有用了。

具名参数(named argument),就是指可以在调用方法时将值传入到指定名字的参数上。以上面的减法方法为例,为了避免搞混被减数和减数,我们可以指定被减数和减数这两个参数的名字:sub(minuend=100, subtrahend=32),这个就是具名参数(named argument)。使用具名参数(named argument)不但不会产生上面提到的bug,而且可以有效的提高代码的可读性。

 

Scala 2.8的具名参数(Named Argument)

 

说了什么是具名参数(named argument),接下来就该看看在Scala 2.8中的这个新特性了。

*以下这些大部分来自官方的PDF
,有兴趣的人可以去看原文。

在Scala 2.8中使用具名参数很简单,就像上面那样就可以。比如我们有一个方法:

def f[T](a: Int, b: T)

在调用的时候指定参数名就可以了:

f(b = getT(), a = getInt())

需要知道的是,像b=getT()这类的参数表达式是按照调用的顺序来执行的,所以在上面的例子中,getT()会在getInt()之前被执行。另外,具名参数还可以和普通的参数传入方式(这里称为位置参数)混合使用,比如:

f(0, b = "1") // 有效
f(a=0, "1") // 有效
f(b = "1", a = 0) // 有效
// f(b = "1", 0) // 无效,位置参数放到了错误的位置上
// f(0, a = 1) // 无效,参数a指定了两次

那么,如果我们在使用"x=expr"这样的具名参数时,x不是参数名的话,Scala会将"x=expr"这个参数看作是对x变量的赋值表达式,且该参数的类型为Unit。例如:

def twice(op: => Unit) = { op; op }
var x = 1
twice(x = x + 1) // 直接给参数赋值,不作为具名参数处理
twice(op=(x=x+1)) //具名参数

上面的语句执行过后,x的值等于5。

当x既是参数名又是变量名的时候,上面的这种用法就会发生错误。如果将"x=expr"这种表达式用花括号或者圆括号括上的话,Scala就不会将其作为具名参数来处理。同样,如果参数是一个块表达式的话(如f{arg}),也不会作为具名参数来处理。

def twice(op: => Unit) = { op; op }
var op = 1
// twice(op = op + 1) // 错误,op的引用不明确
twice((op = op + 1)) // 直接给参数赋值,不作为具名参数处理
twice({op = op + 1}) // 同上
twice{ op = op + 1 } // 同上

上面的这个例子说明了一个问题,就是如果在作用域内有变量和参数名相同的时候,该参数名就无法作为具名参数来使用了。这个问题在日常的应用中一定要注意。

 

具名参数与其它特性结合

 

在这里我们讨论一下Scala 2.8中具名参数和其它特性结合时会是怎样的:

  • By-Name Parameters
这个特性我觉得大家应该都知道,实在是不知道该如何翻译。从上面的例子我们也可以看出,By-Name Parameters与具名参数互不影响,只要作用域内没有和参数名相同的变量名即可。
  • Repeated Parameters

又一个不知道该如何翻译的特性。从官方的文档来看,Repeated Parameters是可以被指定的,但是只能指定一次,不能多次指定。但是就我现在的实验来看,貌似Repeated Parameters只能和它的定义一样,在具名参数时出现在最后,如:

def method(a:String, b:Int*)
method(a="ss", b=1,2,3,4,5,6) //OK
method(b=1,2,3,4,5, a="ss") //错误

  • Functional values
好吧,索性不翻译了,但是得解释一下吧。Scala中的functional value就是一个实现了apply方法的类的实例。我们可以使用apply方法参数的名字来对functional value使用具名参数。不过,因为functional value的静态类型实际上是scala.FunctionN,所以在指定具名参数时,使用的名字应该是scala.FunctionN的apply方法的参数名。

val f1 = new { def apply(x: Int) = x + 1 } //这个的类型是java.lang.Object{def apply(x: Int): Int}
val f2 = (x: Int) => x + 1 // Function1[Int, Int]的实例
f1(x = 2) // OK
// f2(x = 2) // 错误。
f2(v1 = 2) // 可以,因为v1是Function1中apply方法的参数名

  • Overriding

当一个抽象被子类实现或者一个方法被子类覆盖时,参数的名字不必和父类的相同。对类型检查的程序使用具名参数,方法的静态类型会决定应该使用哪个参数名。

trait A { def f(a: Int): Int }
class B extends A { def f(x: Int) = x }
val a1: A = new B
a1.f(a = 1) // OK
a1.f(x = 1) // 错误,找不到值x
val a2: B = new B
a2.f(a = 2) // 错误,找不到值a
a2.f(x = 2) // OK

  • Overloading Resolution

好吧,我承认对于重载来说,使用具名参数会有N多的限制。所以没事干还是少给自己找麻烦,下面我写完大家就明白了。

重载就意味着会有许多名字一样但是参数不一样的方法。当对这些方法使用具名参数时,Scala首先会找出所有合适的候选方法,然后再从中选出最合适的。

至于如何寻找合适的候选方法,给出的参数就很重要了。这些参数不但个数得对,而且对应的类型也必须匹配,比如:

def f() // #1
def f(a: Int, b: String) // #2
f(b = "someString", a = 1) // 选#2

从多个合适的候选方法中确定最合适的方法的过程就和具名参数没有什么关系了,仅仅取决于方法签名。

但是,在下面的这个例子中,两个候选方法就无法区别了,因为参数的类型不是基于参数名来比较的:

def f(a: Int, b: String) // #1
def f(b: Object, a: Int) // #2
f(a = 1, b = "someString") // 错误。对重载方法的引用不明确

不过,对于上面的这个例子我一直想不通,官方的原文是:

In the following example, both alternatives are applicable, but none of them is more specific than the other because the argument types are compared 
based on their position

, not on the argument name.

真的是不太明白这里所说的 "based on their position" 确切的意思到底是什么,所以也就不敢妄加揣测了。但是我觉得对于上面的例子来说是不应该报错的,看看下面的例子:

def f(a: Int, b: String) // #1
def f(a: Int, b: Object) // #2
f(a=1, b="something") // OK,选择#1
f(b=List(1,2,3,4), a=1) // OK,选择#2

让我来大胆的猜想一下:对比这两个例子来看,例子1出现问题的原因可能就是因为参数定义的位置。对于成熟的JVM来说,区分String和Object还是不难的,但是这个是在运行时加以区分的。例子1的问题出现在编译时,也就是说,对于例子1来说,Scala编译器知道要调用名为f的方法,但是从参数类型上来说,Object和String都可以引用"something",也就是说两个f都可以,但是两个f的参数位置是完全不同的,调用哪一个在编译时就需要决定了;但是对于例子2来说,Scala的编译器仅仅确定调用的方法名是f就可以,至于是哪个f就交给JVM动态的来解决吧。

  • Anonymous functions

最后一个,匿名函数(方法)。

从官方的文档来看,占位符语法是可以和具名参数一起使用的,但是,按照它的例子,我始终没有成功过:

def f(x: Int, y: String) = x
val g1: Int => Int = f(y = "someString", x = _) // 错误,位置参数放到了错误的位置上
val g2 = f(y = "someString", x = _: Int) // 错误同上
val g3: Int=>Int = f(_ , y="someString") //OK
val g4: Int=>Int = f(x=_ , y="someString") //错误

不知道是不是我的理解有误了,反正只有第三种情况是可以用的。如果有知道的牛人希望能帮我指正一下。

 

实现

使用具名参数的时候,在调用时的参数顺序不需要和方法定义时的参数顺序一致。如果你传入的参数不是值,而是一些需要先计算的表达式的话,那么Scala会按照调用的顺序来执行它们,下面的例子很好的解释了这个:

class A {
    def f(a: Int, b: Int)(c: Int)
}
(new A).f(b = getB(), a = getA())(c = getC())
// 上面的代码可以转换成这样:
// {
//     val qual$1 = new A()
//     val x$1 = getB()
//     val x$2 = getA()
//     val x$3 = getC()
//     qual$1.f(x$2, x$1)(x$3)
// }




总结

 

以上涉及到的就是具名参数了。实话实说,这是一个非常诱人也非常有用的特性,尤其是构造函数和对于一些API的调用什么的。但是,和其它特性结合的时候还是需要小心的,至少上面列出的重载和匿名函数就在我这里出现了问题。如果不了解背后机制的话,这些问题还真是闹不住啊。



抱歉!评论已关闭.