第四章 Scala基础——函数及其几种形式_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类型的,在函数体内不能修改传入的参数。
————————————————
版权声明:本文为CSDN博主「_iChthyosaur」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_34291505/article/details/86750352

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/hhuzhang/article/details/106559270

智能推荐

攻防世界_难度8_happy_puzzle_攻防世界困难模式攻略图文-程序员宅基地

文章浏览阅读645次。这个肯定是末尾的IDAT了,因为IDAT必须要满了才会开始一下个IDAT,这个明显就是末尾的IDAT了。,对应下面的create_head()代码。,对应下面的create_tail()代码。不要考虑爆破,我已经试了一下,太多情况了。题目来源:UNCTF。_攻防世界困难模式攻略图文

达梦数据库的导出(备份)、导入_达梦数据库导入导出-程序员宅基地

文章浏览阅读2.9k次,点赞3次,收藏10次。偶尔会用到,记录、分享。1. 数据库导出1.1 切换到dmdba用户su - dmdba1.2 进入达梦数据库安装路径的bin目录,执行导库操作  导出语句:./dexp cwy_init/[email protected]:5236 file=cwy_init.dmp log=cwy_init_exp.log 注释:   cwy_init/init_123..._达梦数据库导入导出

js引入kindeditor富文本编辑器的使用_kindeditor.js-程序员宅基地

文章浏览阅读1.9k次。1. 在官网上下载KindEditor文件,可以删掉不需要要到的jsp,asp,asp.net和php文件夹。接着把文件夹放到项目文件目录下。2. 修改html文件,在页面引入js文件:<script type="text/javascript" src="./kindeditor/kindeditor-all.js"></script><script type="text/javascript" src="./kindeditor/lang/zh-CN.js"_kindeditor.js

STM32学习过程记录11——基于STM32G431CBU6硬件SPI+DMA的高效WS2812B控制方法-程序员宅基地

文章浏览阅读2.3k次,点赞6次,收藏14次。SPI的详情简介不必赘述。假设我们通过SPI发送0xAA,我们的数据线就会变为10101010,通过修改不同的内容,即可修改SPI中0和1的持续时间。比如0xF0即为前半周期为高电平,后半周期为低电平的状态。在SPI的通信模式中,CPHA配置会影响该实验,下图展示了不同采样位置的SPI时序图[1]。CPOL = 0,CPHA = 1:CLK空闲状态 = 低电平,数据在下降沿采样,并在上升沿移出CPOL = 0,CPHA = 0:CLK空闲状态 = 低电平,数据在上升沿采样,并在下降沿移出。_stm32g431cbu6

计算机网络-数据链路层_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输-程序员宅基地

文章浏览阅读1.2k次,点赞2次,收藏8次。数据链路层习题自测问题1.数据链路(即逻辑链路)与链路(即物理链路)有何区别?“电路接通了”与”数据链路接通了”的区别何在?2.数据链路层中的链路控制包括哪些功能?试讨论数据链路层做成可靠的链路层有哪些优点和缺点。3.网络适配器的作用是什么?网络适配器工作在哪一层?4.数据链路层的三个基本问题(帧定界、透明传输和差错检测)为什么都必须加以解决?5.如果在数据链路层不进行帧定界,会发生什么问题?6.PPP协议的主要特点是什么?为什么PPP不使用帧的编号?PPP适用于什么情况?为什么PPP协议不_接收方收到链路层数据后,使用crc检验后,余数为0,说明链路层的传输时可靠传输

软件测试工程师移民加拿大_无证移民,未受过软件工程师的教育(第1部分)-程序员宅基地

文章浏览阅读587次。软件测试工程师移民加拿大 无证移民,未受过软件工程师的教育(第1部分) (Undocumented Immigrant With No Education to Software Engineer(Part 1))Before I start, I want you to please bear with me on the way I write, I have very little gen...

随便推点

Thinkpad X250 secure boot failed 启动失败问题解决_安装完系统提示secureboot failure-程序员宅基地

文章浏览阅读304次。Thinkpad X250笔记本电脑,装的是FreeBSD,进入BIOS修改虚拟化配置(其后可能是误设置了安全开机),保存退出后系统无法启动,显示:secure boot failed ,把自己惊出一身冷汗,因为这台笔记本刚好还没开始做备份.....根据错误提示,到bios里面去找相关配置,在Security里面找到了Secure Boot选项,发现果然被设置为Enabled,将其修改为Disabled ,再开机,终于正常启动了。_安装完系统提示secureboot failure

C++如何做字符串分割(5种方法)_c++ 字符串分割-程序员宅基地

文章浏览阅读10w+次,点赞93次,收藏352次。1、用strtok函数进行字符串分割原型: char *strtok(char *str, const char *delim);功能:分解字符串为一组字符串。参数说明:str为要分解的字符串,delim为分隔符字符串。返回值:从str开头开始的一个个被分割的串。当没有被分割的串时则返回NULL。其它:strtok函数线程不安全,可以使用strtok_r替代。示例://借助strtok实现split#include <string.h>#include <stdio.h&_c++ 字符串分割

2013第四届蓝桥杯 C/C++本科A组 真题答案解析_2013年第四届c a组蓝桥杯省赛真题解答-程序员宅基地

文章浏览阅读2.3k次。1 .高斯日记 大数学家高斯有个好习惯:无论如何都要记日记。他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢?高斯出生于:1777年4月30日。在高斯发现的一个重要定理的日记_2013年第四届c a组蓝桥杯省赛真题解答

基于供需算法优化的核极限学习机(KELM)分类算法-程序员宅基地

文章浏览阅读851次,点赞17次,收藏22次。摘要:本文利用供需算法对核极限学习机(KELM)进行优化,并用于分类。

metasploitable2渗透测试_metasploitable2怎么进入-程序员宅基地

文章浏览阅读1.1k次。一、系统弱密码登录1、在kali上执行命令行telnet 192.168.26.1292、Login和password都输入msfadmin3、登录成功,进入系统4、测试如下:二、MySQL弱密码登录:1、在kali上执行mysql –h 192.168.26.129 –u root2、登录成功,进入MySQL系统3、测试效果:三、PostgreSQL弱密码登录1、在Kali上执行psql -h 192.168.26.129 –U post..._metasploitable2怎么进入

Python学习之路:从入门到精通的指南_python人工智能开发从入门到精通pdf-程序员宅基地

文章浏览阅读257次。本文将为初学者提供Python学习的详细指南,从Python的历史、基础语法和数据类型到面向对象编程、模块和库的使用。通过本文,您将能够掌握Python编程的核心概念,为今后的编程学习和实践打下坚实基础。_python人工智能开发从入门到精通pdf