第四章 Scala基础——函数及其几种形式

一、定义一个函数

Scala的函数定义以“def”开头,然后是一个自定义的函数名(推荐驼峰命名法),接着是用圆括号“( )”包起来的参数列表。在参数列表里,多个参数用逗号隔开,并且每个参数名后面要紧跟一个冒号以及显式声明的参数类型,因为编译器在编译期间无法推断出入参类型。写完参数列表后,应该紧跟一个冒号,再添加函数返回结果的类型。最后,再写一个等号“=”,等号后面是用花括号“{ }”包起来的函数体。例如:

用“def”开始函数定义
       | 函数名
       |   |  参数及参数类型
       |   |        |   函数返回结果的类型
       |   |        |          |  等号
       |   |        |          |   |
      def max(x: Int, y: Int): Int = {
        if(x > y)
          x
        else  |
          y   | 
      }       |
              |
       花括号里定义函数体

   Ⅰ、分号推断

在Scala的代码里,语句末尾的分号是可选的,因为编译器会自动推断分号。如果一行只有一条完整的语句,那么分号可写可不写;如果一行有多条语句,则必须用分号隔开。有三种情况句末不会推断出分号:①句末是以非法结尾字符结尾,例如以句点符号“.”或中缀操作符结尾。②下一行的句首是以非法起始字符开始,例如以句点符号“.”开头。③跨行出现的圆括号对“( )”或者方括号对“[ ]”,因为它们里面不能进行分号的自动推断,要么只包含一条完整语句,要么包含用分号显式隔开的多条语句。另外,花括号对“{ }”的里面可以进行分号的自动推断。为了简洁起见,同时不产生无意的错误和歧义,建议一行只写一条完整的语句,句末分号省略,让编译器自动推断。而且内层的语句最好比外一层语句向内缩进两个空格,使得代码层次分明。

   Ⅱ、函数的返回结果

在Scala里,“return”关键字也是可选的。默认情况下,编译器会自动为函数体里的最后一个表达式加上“return”,将其作为返回结果。建议不要显式声明“return”,这会引发warning,而且使得代码风格看上去像指令式风格。

返回结果的类型也是可以根据参数类型和返回的表达式来自动推断的,也就是说,上例中的“: Int”通常是可以省略的。

返回结果有一个特殊的类型——Unit,表示没有值返回。也就是说,这是一个有副作用的函数,并不能提供任何可引用的返回结果。Unit类型同样可以被推断出来,但如果显式声明为Unit类型的函数,则即使函数体最后有一个可以返回具体值的表达式,也不会把表达式的结果返回。例如:

scala> def add(x: Int, y: Int) = { x + y }
add: (x: Int, y: Int)Int

scala> add(1, 2)
res0: Int = 3

scala> def nothing(x: Int, y: Int): Unit = { x + y }
nothing: (x: Int, y: Int)Unit

scala> nothing(1, 2)

scala>

   Ⅲ、等号与函数体

Scala的函数体是用花括号包起来的,这与C、C++、Java等语言类似。函数体里可以有多条语句,并自动推断分号、返回最后一个表达式。如果只有一条语句,那么花括号也可以省略。

Scala的函数定义还有一个等号,这使得它看起来类似数学里的函数“f(x) = ...”。当函数的返回类型没有显式声明时,那么这个等号可以省略,但是返回类型一定会被推断成Unit类型,不管有没有值返回,而且函数体必须有花括号。当函数的返回类型显式声明时,则无论如何都不能省略等号。建议写代码时不要省略等号,避免产生不必要的错误,返回类型最好也显式声明。

   Ⅳ、无参函数

如果一个函数没有参数,那么可以写一个空括号作参数列表,也可以不写。如果有空括号,那么调用时可以写也可以不写空括号;如果没有空括号,那么调用时就一定不能写空括号。原则上,无副作用的无参函数省略括号,有副作用的无参函数添加括号,这提醒使用者需要额外小心。

二、方法

方法其实就是定义在class、object、trait里面的函数,这种函数叫做“成员函数”或者“方法”,与多数oop(object-oriented programming)语言一样。

三、嵌套函数

函数体内部还可以定义函数,这种函数的作用域是局部的,只能被定义它的外层函数调用,外部无法访问。局部函数可以直接使用外层函数的参数,也可以直接使用外层函数的内部变量。例如:

scala> def addSub(x: Int, y: Int) = {
         |     def sub(z: Int) = z - 10
         |     if(x > y) sub(x - y) else sub(y - x)
         | }
addSub: (x: Int, y: Int)Int

scala> addSub(100, 20)
res0: Int = 70

 四、函数字面量

函数式编程有两个主要思想,其中之一就是:函数是一等(first-class)的值。换句话说,一个函数的地位与一个Int值、一个String值等等,是一样的。既然一个Int值可以成为函数的参数、函数的返回值、定义在函数体里、存储在变量里,那么,作为地位相同的函数,也可以这样。你可以把一个函数当参数传递给另一个函数,也可以让一个函数返回一个函数,亦可以把函数赋给一个变量,又或者像定义一个值那样在函数里定义别的函数(即前述的嵌套函数)。就像写一个整数字面量“1”那样,Scala也可以定义函数的字面量。函数字面量是一种匿名函数的形式,它可以存储在变量里、成为函数参数或者当作函数返回值,其定义形式为:

(参数1: 参数1类型, 参数2: 参数2类型, ...) => { 函数体 }

通常,函数字面量会赋给一个变量,这样就能通过“变量名(参数)”的形式来使用函数字面量。在参数类型可以被推断的情况下,可以省略类型,并且参数只有一个时,圆括号也可以省略。

函数字面量的形式可以更精简,即只保留函数体,并用下划线“_”作为占位符来代替参数。在参数类型不明确时,需要在下划线后面显式声明其类型。多个占位符代表多个参数,即第一个占位符是第一个参数,第二个占位符是第二个参数……因此不能重复使用某个参数。例如:

scala> val f = (_: Int) + (_: Int)
f: (Int, Int) => Int = $$Lambda$1072/1534177037@fb42c1c

scala> f(1, 2)
res0: Int = 3

 无论是用“def”定义的函数,还是函数字面量,它们的函数体都可以把一个函数字面量作为一个返回结果,这样就成为了返回函数的函数;它们的参数变量的类型也可以是一个函数,这样调用时给的入参就可以是一个函数字面量。类型为函数的变量,其冒号后面的类型写法是“(参数1类型, 参数2类型,...) => 返回结果的类型”。例如:

scala> val add = (x: Int) => { (y: Int) => x + y }
add: Int => (Int => Int) = $$Lambda$1192/1767705308@55456711

scala> add(1)(10)
res0: Int = 11

scala> def aFunc(f: Int => Int) = f(1) + 1
aFunc: (f: Int => Int)Int

scala> aFunc(x => x + 1)
res1: Int = 3

在第一个例子中,变量add被赋予了一个返回函数的函数字面量。在调用时,第一个括号里的“1”是传递给参数x,第二个括号里的“10”是传递给参数y。如果没有第二个括号,得到的就不是11,而是“(y: Int) => 1 + y”这个函数字面量。

在第二个例子中,函数aFunc的参数f是一个函数,并且该函数要求是一个入参为Int类型、返回结果也是Int类型的函数。在调用时,给出了函数字面量“x => x + 1”。这里没有显式声明x的类型,因为可以通过f的类型来推断出x必须是一个Int类型。在执行时,首先求值f(1),结合参数“1”和函数字面量,可以算出结果是2。那么,“f(1) + 1”就等于3了。

五、部分应用函数

上面介绍的函数字面量实现了函数作为一等值的功能,而用“def”定义的函数也具有同样的功能,只不过需要借助部分应用函数的形式来实现。例如,有一个函数定义为“def max(...) ...”,若想要把这个函数存储在某个变量里,不能直接写成“val x = max”的形式,而必须像函数调用那样,给出一部分参数,故而称作部分应用函数(如果参数全给了,就成了函数调用)。部分应用函数的作用,就是把def函数打包到一个函数值里,使它可以赋给变量,或当作函数参数进行传递。例如:

scala> def sum(x: Int, y: Int, z: Int) = x + y + z
sum: (x: Int, y: Int, z: Int)Int

scala> val a = sum(1, 2, 3)
a: Int = 6

scala> val b = sum(1, _: Int, 3)
b: Int => Int = $$Lambda$1204/1037479646@5b0bfe86

scala> b(2)
res0: Int = 6

scala> val c = sum _
c: (Int, Int, Int) => Int = $$Lambda$1208/1853277442@5e4c26a1

scala> c(1, 2, 3)
res1: Int = 6

变量a其实是获得了函数sum调用的返回结果,变量b则是获得了部分应用函数打包的sum函数,因为只给出了参数x和z的值,参数y没有给出。注意,没给出的参数用下划线代替,而且必须显式声明参数类型。变量c也是部分应用函数,只不过一个参数都没有明确给出。像这样一个参数都不给的部分应用函数,只需要在函数名后面给一个下划线即可,注意函数名和下划线之间必须有空格。

如果部分应用函数一个参数都没有给出,比如例子中的c,那么在需要该函数作入参的地方,下划线也可以省略。例如:

scala> def needSum(f: (Int, Int, Int) => Int) = f(1, 2, 3)
needSum: (f: (Int, Int, Int) => Int)Int

scala> needSum(sum _)
res2: Int = 6

scala> needSum(sum)
res3: Int = 6

六、闭包

一个函数除了可以使用它的参数外,还能使用定义在函数以外的其他变量。其中,函数的参数称为绑定变量,因为完全可以根据函数的定义得知参数的信息;而函数以外的变量称为自由变量,因为函数自身无法给出这些变量的定义。这样的函数称为闭包,因为它要在运行期间捕获自由变量,让函数闭合,定义明确。自由变量必须在函数前面定义,否则编译器就找不到,会报错。

闭包捕获的自由变量是闭包创建时活跃的那个自由变量,后续若新建同名的自由变量来覆盖前面的定义,由于闭包已经闭合完成,所以新自由变量与已创建的闭包无关。如果闭包捕获的自由变量本身是一个可变对象(例如var类型变量),那么闭包会随之改变。例如:

var more = 1

val addMore = (x: Int) => x + more  // addMore = x + 1

more = 2                                           // addMore = x + 2

var more = 10                                   // addMore = x + 2

more = -100                                      // addMore = x + 2

七、函数的特殊调用形式

   Ⅰ、具名参数

普通函数调用形式是按参数的先后顺序逐个传递的,但如果调用时显式声明参数名并给其赋值,则可以无视参数顺序。按位置传递的参数和按名字传递的参数可以混用,例如:

scala> def max(x: Int, y: Int, z: Int) = {
         |     if(x > y && x > z) println("x is maximum")
         |     else if(y > x && y > z) println("y is maximum")
         |     else println("z is maximum")
         |  }
max: (x: Int, y: Int, z: Int)Unit

scala> max(1, z = 10, y = 100)
y is maximum 

   Ⅱ、默认参数值

函数定义时,可以给参数一个默认值,如果调用函数时缺省了这个参数,那么就会使用定义时给的默认值。默认参数值通常和具名参数结合使用。例如:

scala> def max(x: Int = 10, y: Int, z: Int) = {
         |     if(x > y && x > z) println("x is maximum")
         |     else if(y > x && y > z) println("y is maximum")
         |     else println("z is maximum")
         |  }
max: (x: Int, y: Int, z: Int)Unit

scala> max(y = 3, z = 5)
x is maximum

   Ⅲ、重复参数

Scala允许把函数的最后一个参数标记为重复参数,其形式为在最后一个参数的类型后面加上星号“*”。重复参数的意思是可以在运行时传入任意个相同类型的元素,包括零个。类型为“T*”的参数的实际类型是“Array[T]”,即若干个T类型对象构成的数组。尽管是T类型的数组,但要求传入参数的类型仍然是T。如果传入的实参是T类型对象构成的数组,则会报错,除非用“变量名: _*”的形式告诉编译器把数组元素一个一个地传入。例如: 

scala> def addMany(msg: String, num: Int*) = {
         |     var sum = 0
         |     for(x <- num) sum += x
         |     println(msg + sum)
         |  }
addMany: (msg: String, num: Int*)Unit

scala> addMany("sum = ", 1, 2, 3)
sum = 6

scala> addMany("sum = ")
sum = 0

scala> addMany("sum = ", Array(1, 2, 3))
<console>:13: error: type mismatch;
 found   : Array[Int]
 required: Int
       addMany("sum = ", Array(1, 2, 3))
                              ^

scala> addMany("sum = ", Array(1, 2, 3): _*)
sum = 6

八、柯里化

对大多数编程语言来说,函数只能有一个参数列表,但是列表里可以有若干个用逗号间隔的参数。Scala有一个独特的语法——柯里化,也就是一个函数可以有任意个参数列表。柯里化往往与另一个语法结合使用:当参数列表里只有一个参数时,在调用该函数时允许单个参数不用圆括号包起来,改用花括号也是可行的。这样,在自定义类库时,自定义方法就好像“if(...) {...}”、“while(...) {...}”、“for(...) {...}”等内建控制结构一样,让人看上去以为是内建控制,丝毫看不出是自定义语法。例如:

scala> def add(x: Int, y: Int, z: Int) = x + y + z
add: (x: Int, y: Int, z: Int)Int

scala> add(1, 2, 3)
res0: Int = 6

scala> def addCurry(x: Int)(y: Int)(z: Int) = x + y + z
addCurry: (x: Int)(y: Int)(z: Int)Int

scala> addCurry(1)(2) {3}
res1: Int = 6

九、传名参数

第四点介绍了函数字面量如何作为函数的参数进行传递,以及如何表示类型为函数时参数的类型。如果某个函数的入参类型是一个无参函数,那么通常的类型表示法是“() => 函数的返回类型”。在调用这个函数时,给出的参数就必须写成形如“() => 函数体”这样的函数字面量。

为了让代码看起来更舒服,也为了让自定义控制结构更像内建结构,Scala又提供了一个特殊语法——传名参数。也就是类型是一个无参函数的函数入参,传名参数的类型表示法是“=> 函数的返回类型”,即相对常规表示法去掉了前面的空括号。在调用该函数时,传递进去的函数字面量则可以只写“函数体”,去掉了“() =>”。例如:

var assertionEnabled = false

// predicate是类型为无参函数的函数入参
def myAssert(predicate: () => Boolean) =
  if(assertionEnabled && !predicate())
    throw new AssertionError
// 常规版本的调用
myAssert(() => 5 > 3)

// 传名参数的用法,注意因为去掉了空括号,所以调用predicate时不能有括号
def byNameAssert(predicate: => Boolean) =
  if(assertionEnabled && !predicate)
    throw new AssertionError
// 传名参数版本的调用,看上去更自然
byNameAssert(5 > 3)

 可以看到,传名参数使得代码更加简洁、自然,而常规写法则很别扭。事实上,predicate的类型可以改成Boolean,而不必是一个返回布尔值的函数,这样调用函数时与传名参数是一致的。例如:

// 使用布尔型参数的版本
def boolAssert(predicate: Boolean) =
  if(assertionEnabled && !predicate)
    throw new AssertionError
// 布尔型参数版本的调用
boolAssert(5 > 3)

 尽管byNameAssert和boolAssert在调用形式上是一样的,但是两者的运行机制却不完全一样。如果给函数的实参是一个表达式,比如“5 > 3”这样的表达式,那么boolAssert在运行之前会先对表达式求值,然后把求得的值传递给函数去运行。而myAssert和byNameAssert则不会一开始就对表达式求值,它们是直接运行函数,直到函数调用入参时才会对表达式求值,也就是例子中的代码运行到“!predicate”时才会求“5 > 3”的值。

为了说明这一点,可以传入一个产生异常的表达式,例如除数为零的异常。例子中,逻辑与“&&”具有短路机制:如果&&的左侧是false,那么直接跳过右侧语句的运行(事实上,这种短路机制也是通过传名参数实现的)。所以,布尔型参数版本会抛出除零异常,常规版本和传名参数版本则不会发生任何事。例如:

scala> myAssert(() => 5 / 0 == 0)

scala> byNameAssert(5 / 0 == 0)

scala> boolAssert(5 / 0 == 0)
java.lang.ArithmeticException: / by zero
  ... 28 elided

 如果把变量assertionEnabled设置为true,让&&右侧的代码执行,那么三个函数都会抛出除零异常:

scala> assertionEnabled = true
assertionEnabled: Boolean = true

scala> myAssert(() => 5 / 0 == 0)
java.lang.ArithmeticException: / by zero
  at .$anonfun$res30$1(<console>:13)
  at .myAssert(<console>:13)
  ... 28 elided

scala> byNameAssert(5 / 0 == 0)
java.lang.ArithmeticException: / by zero
  at .$anonfun$res31$1(<console>:13)
  at .byNameAssert(<console>:13)
  ... 28 elided

scala> boolAssert(5 / 0 == 0)
java.lang.ArithmeticException: / by zero
  ... 28 elided

十、总结

本章内容是对Scala的函数的讲解,重点在于理解函数作为一等值的概念,函数字面量的作用以及部分应用函数的作用。在阅读复杂的代码时,常常遇见诸如“def xxx(f: T => U, ...) ...”或 “def xxx(...): T => U”的代码,要理解前者表示需要传入一个函数作为参数,后者表示函数返回的对象是一个函数。在学习初期,理解函数是一等值的概念可能有些费力,通过大量阅读和编写代码才能熟能生巧。同时不要忘记前一章说过,函数的参数都是val类型的,在函数体内不能修改传入的参数。

 

上一章   Scala基础——变量定义与基本类型

下一章   Scala基础——类和对象

  • 37
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 9
    评论
第三章介绍了Scala函数和方法,以及它们的区别。具体内容如下: 1. 函数和方法的区别:函数是一段可以独立调用的代码,它可以像变量一样被传递、返回和赋值;而方法是属于某个对象或类的一段代码,它必须通过对象或类来调用。 2. 函数的定义方式:可以使用def关键字定义函数,也可以使用匿名函数(lambda表达式)。 3. 函数的参数:Scala函数可以没有参数,也可以有多个参数。参数可以有默认值,也可以是可变参数。 4. 函数的返回值:Scala函数可以没有返回值,也可以有返回值。返回值类型可以显式声明,也可以自动推断。 5. 方法的定义方式:方法必须定义在对象或类中,使用def关键字表示。方法可以有访问修饰符和参数列表,也可以有返回值类型和方法体。 6. 方法的参数:和函数一样,方法可以有多个参数,也可以有默认值和可变参数。 7. 方法的返回值:方法必须有返回值类型,如果没有显式声明,则默认返回Unit类型。 8. 函数和方法的调用:函数可以直接调用,也可以通过变量、高阶函数等方式调用;方法必须通过对象或类来调用。 9. 函数式编程的特点:函数式编程强调函数的纯粹性、不可变性和高阶函数的使用,它能够简化代码、提高可读性和可维护性。 总之,Scala函数和方法都是非常重要的编程工具,它们可以让我们更加灵活地组织代码,提高开发效率和代码质量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值