Scala重载apply方法时需显式转换方法为函数的原因及优化方案
apply需要显式写foo _,第一个却不用? 我来给你拆解这个Scala里的典型问题,核心在于Scala的ETA展开(也就是方法自动转函数的机制)的触发条件。
先搞懂第一个apply为啥没问题
看第一个apply的签名:
def apply(s: String, x: Int, y: Int, z: Int, foo: Int => Int) = new A(...)
当你调用A("a1", 1, 2, 3, foo)时,Scala一眼就看出来最后一个参数需要的是Int => Int类型的函数。这时候它会自动帮你把foo这个方法(def foo(x: Int): Int)转换成对应的函数值——相当于偷偷帮你加了foo _,所以你不用手动写。
第二个apply为啥报错?
再看第二个apply的签名:
def apply(s: String, x: Int, y: Int, z: Int, foo: Int => Int, l: List[String]) = new A(...)
当你写A("a1", 1, 2, 3, foo, List("b1", "b2"))时,Scala解析参数的顺序是从左到右。当它遇到foo的时候,后面还有个List(...)参数,这时候它会犯嘀咕:你是想调用foo方法(但没传参数啊)?还是想把foo转成函数?
因为Scala没法立刻确定你的意图,它会优先把foo当作未调用的方法处理,而方法必须传参数才能调用,所以就抛出了那个错误——提示你用foo _或者foo(_)明确告诉它:“我要把这个方法转成函数,不是要调用它”。
怎么修改才能直接用foo?
最直接的办法就是调整参数顺序,把函数类型的参数放到参数列表的最后,这样Scala就能明确知道你要的是函数,自动触发ETA展开:
修改后的伴生对象
object A { // 原第一个apply保持不变 def apply(s: String, x: Int, y: Int, z: Int, foo: Int => Int) = new A(s, x, y, z, foo, None) // 把foo移到最后一位 def apply(s: String, x: Int, y: Int, z: Int, l: List[String], foo: Int => Int) = new A(s, x, y, z, foo, Some(l)) }
现在调用第二个apply就可以直接用foo了:
val a2 = A("a1", 1, 2, 3, List("b1", "b2"), foo) // 完美运行,不用加下划线
更符合Scala风格的替代方案:用默认参数代替重载
其实对于这种“可选参数”的场景,Scala更推荐用默认参数来替代多个重载的apply方法,这样能减少歧义:
// 给l参数加默认值None case class A(s: String, x: Int, y: Int, z: Int, foo: Int => Int, l: Option[List[String]] = None) object A { // 提供一个接受List[String]的辅助方法,同样把foo放最后 def apply(s: String, x: Int, y: Int, z: Int, l: List[String], foo: Int => Int): A = A(s, x, y, z, foo, Some(l)) }
这样调用起来更统一,也不会有参数解析的歧义问题。
一句话总结
Scala只会在明确需要函数类型的位置自动把方法转成函数。当函数参数后面还有其他参数时,Scala没法确定你的意图,所以需要手动显式转换;把函数参数移到最后,就能让Scala自动帮你完成转换啦。
内容的提问来源于stack exchange,提问作者toni057

