导读
封装是计算机编程语言中最基本但也十分经典的思想(更严谨地说封装是面向对象设计中的一种思想),例如将一段频繁用到的逻辑写成一个函数的过程,其背后蕴含的其实就是封装的思想。与众多编程语言类似,Scala中也提供了方法和函数的功能,但在具体使用上又有很多特别之处,甚至一定程度上可以彰显Scala的设计理念。
本文旨在介绍Scala中方法和函数的常用用法,对一些少用而略显怪异的功能不予关注。主要行文目录如下:
- 方法的常用用法
- 标准定义
- 参数默认值、带名传参和不定长参数
- 参数列表缺省
- return缺省
- 返回值类型缺省
- 等号缺省
- 大括号缺省
- 函数的常用用法
- 标准定义
- 偏应用函数和偏函数
- 柯里化函数
- 高阶函数
- 二者的联系与区别
- 方法主要用于类和对象,函数主要用于传参和返回值
- 函数是一个对象,可以赋值给一个变量
- 二者可以部分转化
01 方法的常用用法
在多数编程语言中,方法其实属于广义上的函数:独立定义的叫做函数,定义在类中的函数一般称之为方法。然而在Scala中,二者的差异其实会更大,不仅有形式上的区别,更有用法上的不同。
方法的标准定义如下:
以上是一个标准的Scala方法定义程序,执行的是两个整数求和的操作,保留了方法定义中的每个要素,分别介绍如下:
- def:方法定义关键字,即define的缩写,这与Python中函数定义关键字一致
- fun:方法名,符合Scala中标识符的定义要求,一般采用小驼峰方式组织命名
- fun后接一对小括号,用于接收一组参数定义
- a:参数名,符合Scala中标识符的定义要求
- 参数名的Int:声明参数类型。与Python中可选声明参数类型不同的是,Scala中的参数类型声明是必须项,而且程序编译时会执行类型检查(Python中的参数类型声明就是个形式,仅用于提示使用者而不做实际检查,挂羊头卖狗肉是可行的);但值得指出的参数类型可以使用声明类型的子类和支持隐式转换,例如某方法中参数声明类型为Any,那么实际上可以接受任何类型;某方法参数声明为Double,那么传入Int也是可以的
- 方法参数小括号后的Int:返回值类型,多数情况下可以省略,此时由编译器执行类型推断得出;但当方法中存在递归调用时为必须项;
- 方法后的=:用于赋值操作,相当于把方法体中的返回值赋予给调用该方法的变量,特殊情况下可省略,此时无论方法体中是否实际有返回结果,该方法的返回值均为空
- 方法体中的大括号:在Scala中,大括号意味着将一组执行语句囊括为一个整体,并称之为代码块,代码块的最后一行代码的执行结果即是该方法的返回结果
- 方法体中return:与Python中必须显示使用return关键字来表达返回值,Scala中的return是可选项,一般仅在需提前返回方法执行结果时才需使用(否则,就是以方法体代码块中的最后一句代码执行结果作为返回值)
- 方法调用:使用方法名 相应参数即可,这与其他语言中类似
以上为Scala中方法的完整标准定义和调用,但在很多情况下可以省略其中的部分要素,例如:
1)当参数指定默认取值时,在调用时可缺省,这与Python中的带名参数调用方式是一致的。特别地,Scala中也支持类似Python的不定长参数,但具体形式与Python中略有区别,注意如下方法中参数nums声明类型Int后标注了*,代表nums是不定长的Int型参数:
代码语言:javascript复制scala> def fun1(nums:Int*):Int= nums.sum
def fun1(nums: Int*): Int
scala> fun1(1, 2)
val res7: Int = 3
2)方法调用时省略传参小括号。特殊情况下,当方法无需接收任何参数时,即参数为空,那么在调用该方法时则可省略小括号,直接写出方法名即可;更特殊地,如果一个方法无需接收任何参数,那么在定义方法时则可省略小括号的书写,此时在调用方法时则必须省略小括号:
代码语言:javascript复制scala> def sayHelloWorld : Unit = println("hello, world")
def sayHelloWorld: Unit
scala> sayHelloWorld
hello, world
scala> sayHelloWorld()
^
error: Unit does not take parameters
3)return缺省。绝大多数情况下可以省略return,此时方法体中的最后一句代码执行结果即为该方法的返回值,一般仅需在提前终止代码块执行并返回结果时才需使用,例如如下方法首先判断除数是否为0,若为0则提前返回:
代码语言:javascript复制scala> def fun2(a:Int, b:Int):Int={
| if(b==0) return -1
| a / b
| }
def fun2(a: Int, b: Int): Int
4)返回值类型缺省。Scala中的一个典型特性就是支持类型推断,包括方法的返回值类型推断。既然可以自动推断,所以一般可以省略,但当方法具有递归调用时必须显示声明返回值类型:
代码语言:javascript复制scala> def fun3(num:Int):Int = if(num==1) 1 else fun3(num-1)*num
def fun3(num: Int): Int
scala> def fun4(num:Int) = if(num==1) 1 else fun4(num-1)*num
^
error: recursive method fun4 needs result type
5)等号缺省。方法声明中的等号用于连接方法签名(即方法名和参数部分)和方法体(即大括号中的代码块),用以表示赋值。所以,当无需返回任何结果或者说返回值类型为空时(返回值类型为空用Unit表示),此时即可省略等号。省略等号意味着返回值类型一定为空,即使方法体中的代码块实际会产生非空的返回值。例如以下方法的返回结果与预期不一致:
代码语言:javascript复制scala> def fun4(a:Int, b:Int) {a b}
^
warning: procedure syntax is deprecated: instead, add `: Unit =` to explicitly declare `fun4`'s return type
def fun4(a: Int, b: Int): Unit
scala> fun4(1, 2) // 返回结果为空
6)大括号缺省。实际上这不是Scala特有的特性,即当方法体仅有单行代码时,无需显示写出大括号。这很容易理解:大括号的作用是将一组代码囊括为一个整体,而当代码块仅有单行代码时自然可以缺省。
注:等号和大括号不可同时缺省。
02 函数的常用用法
如果说Scala中的方法更像是其他语言中函数,那么Scala中的函数则更像是为实现函数式编程而特有的设计。在多数介绍Scala中函数的技术文章中,一般会提到这么一句:
函数是Scala中的一等公民。
实际上,称函数是一等公民,其实是相对于方法而言,即函数可以像任何其他对象那样赋值给一个变量,以参数或者返回值的身份作为方法的一部分,换句话说函数在Scala中具有和其他对象同等使用权限,而这是方法所不具备的。
与方法使用def作为关键字来声明不同,Scala中声明函数的关键字其实是“=>”,一个标准的函数声明如下:
在如上的函数声明中,仍然实现的是两个整数相加的功能,其中各要素介绍如下:
- 函数参数即参数类型,用法与方法中类似
- 建立参数与返回值映射,个人认为这是Scala中函数的一个标志性符号,作用类似于方法中的=,但不可缺省
- 函数体与方法中的用法类似
实际上,在完成方法介绍之后,函数的用法其实会更简单,但需把握以下区别:
- 函数可以没有函数名,此时即为匿名函数;
- 函数无需指定返回值类型,不是可以缺省,而是不支持;
另一方面,由于函数可以像其他对象一样赋值给变量,所以如上函数的定义可以用一个变量接收,而后该变量即可像方法一样完成功能调用、像变量一样作为参数供其他方法调用或作为返回值。实际上,在Scala中,函数的主要作用其实就是作为方法的参数或返回值,此时即对应高阶函数,体现的即为Scala的函数式编程思想。
代码语言:javascript复制scala> val add = (a:Int, b:Int) => a b
val add: (Int, Int) => Int = $Lambda$1225/569105801@27b3e5ed
scala> add(1, 2)
val res17: Int = 3
函数的用法有许多高级特性,这些在一定程度上丰富了Scala的语法特性,但也很容易对初学者造成很大困扰,下面仅就其中的几个简单展开介绍:
1)偏应用函数和偏函数。这是两个很容易搞混的概念,所以不妨首先给出英文原义:偏应用函数英文写法为partial applied function,偏函数为partial function。所以看到了英文写法,两个概念中的偏就很容易理解:与其翻译为"偏",实则表达的含义是"部分"。至于是否带应用的区别,则没那么直观:但表达的含义倒也算清晰:
- 偏应用函数的"偏"侧重于参数个数层面,即可以先传入部分参数,剩余参数交由后续再传入
- 偏函数的"偏"侧重于参数定义域层面,即仅对部分定义域范围内明确逻辑,而对其他则不予给出。比如在某些情况下有明显的业务逻辑,而在其他情况下则处于待定状态时,则可用偏函数实现
// 偏应用函数:先指定部分参数,再指定其余参数
scala> val add = (a:Int, b:Int) => a b
val add: (Int, Int) => Int = $Lambda$1225/569105801@27b3e5ed
scala> val add1 = add(1, _:Int)
val add1: Int => Int = $Lambda$1229/509809074@6fab6ea2
scala> add1(2)
val res18: Int = 3
// 偏函数:仅对参数定义域的部分范围明确逻辑
scala> val fun:PartialFunction[Int, Double] = {
| case x if(x>0) => x 1.0
| }
val fun: PartialFunction[Int,Double] = <function1>
PartialFunction[Int, Double]中,第一个Int表示输入参数为Int,第二个Double表示返回值类型为Double。
2)柯里化函数。对于Scala中含有多个参数的方法,可以通过调整书写形式实现各参数的逐步指定。例如:
代码语言:javascript复制scala> def add(a:Int, b:Int)=a b
def add(a: Int, b: Int): Int
scala> def addCurried(a:Int)(b:Int)=a b
def addCurried(a: Int)(b: Int): Int
scala> val add1 = addCurried(1) _
val add1: Int => Int = $Lambda$1272/601949467@1d4c39c2
scala> add1(2)
val res22: Int = 3
如上代码中,通过将add方法的书写形式调整为addCurried中的书写形式,在后续调用中可以先明确部分参数,并将明确了部分参数的函数作为返回结果赋值给一个新的变量add1,注意这里add1实际上是一个函数。
可见,对一个方法柯里化的过程,其效果与偏应用函数实际上是有些类似的,明确了部分参数的方法的返回结果就叫做柯里化函数。这也是将方法的柯里化特性放在这里讲述的原因。
3)高阶函数。实际上,上述的偏应用函数、柯里化函数背后对应的都属于Scala中高阶函数的特性,即函数以一个返回值的身份出现在其他方法中。对于Scala中的一个方法定义,但参数或返回值是一个函数类型时,那么就称之为高阶函数(或者更严谨的说,是一个高阶方法),这也是Scala中函数式编程的直接体现。
实际上,将函数作为另一个函数的参数或者返回值,这一特性在Python中也是有所体现的。
03 二者的联系与区别
作为编程语言中常用的封装技巧,函数是必不可少的语法特性。在很多编程语言中,例如Python,方法和函数本无实质区别,但在Scala中却有很大差异。这些差异一方面是出于Scala语法特性的需要,另一方面也成就了函数式编程的精髓。概括而言,方法和函数的主要联系与区别包括:
- 方法定义的关键字为def,函数定义的标志性符号则为=>
- 函数必须接受参数列表(参数可以为空,但小括号不可省略);而方法中则可以省略参数列表甚至小括号,此时仅用于完成部分固定功能
- 方法可以指定返回值类型,也可以缺省;而函数则不支持指定返回值类型
- 函数与其他对象一致(所谓的一等公民),可以赋值给一个变量,也可作为一个方法的参数或返回值,此时即为高阶函数
- 方法可以简单的通过"方法名 空格 _"转变化函数