【scala系列】8、方法与函数


1. 方法

  • Scala 中使用 def 语句定义方法, val 语句定义函数。

  • 定义方法的通用格式 :def functionName ([参数列表]) : [return type] = { 方法体 } 。

  • 如果一个方法有返回值,方法的最后一条语句的返回值,一定要和方法的返回值类型保持一致。

  • 如果不写等于号和方法主体,那么方法会被隐式声明为抽象(abstract),包含它的类型也是一个抽象类型。

1.1 main方法

//Scala的main方法(包括所有类似java的static方法)必须定义在一个object内:
object Test1{
  def main(args: Array[String]) {
    println("hello world")
    }
}

//继承 Application 不带命令行参数的简化main方法:
object app1 extends Application {
  println("hello world")
}

1.2 方法的定义

1.2.1 定义带返回值的方法

  • 方法的返回值不需要加return,方法的最后一行作为整个方法的返回值
  • 定义方法时 “:” 后面表示方法的返回值,如果定义了方法的返回值,当返回类型不一致时代码会编译不通过
  • 可以忽略掉方法的返回值类型,scala会根据最后一条语句的返回值推断出方法的返回值类型
def m1(x:Int,y:Int):Int = {
  x + y
}

def m2(x: Int, y: Int) {
  x + y
}

1.2.2 定义没有返回值的方法

  • 可以使用Unit来标注,表现为 “()”类似于java中的void
  • 也在参数列表括号后面不加 “=”,直接添加方法体{}, 我们称这种方法为过程

def m3(x: Int, y: Int): Unit = {
  x + y
}

def m3_1(x: Int, y: Int) {
  x + y
}

1.2.3 不定义参数名称只参数类型

先定义方法参数列表类型,具体的参数名称在方法体中,函数式编程中经常会用到 在spark 源码中大量出现

def m4: (Int, Int, Int) => Int = {
  (x, y, z) => {
    x + y + z
  }
}

1.2.4 定义无参方法

  • 定义无参函数时带括号,调用时带不带括号都可以
  • 定义无参函数时不带括号,调用时一定不能带括号
  def m6() = {
    println("hello")
  }

  // 调用方式 m6_1, 如果使用 m6_1() 则会报错
  def m6_1 = {
    println("hello")
  }

1.2.5 柯理化的两种定义方式

// 调用 m5(1)(2)
def m5(x: Int)(y: Int) = {
  x + y
}

// 调用 m5_1(1)(2)
def m5_1(x: Int) = (y: Int) => {
  x + y
}


def sum(a:Int, b:Int) = { a + b } // sum(1, 2) = 3

//Curry化后:
def sum(a:Int)(b:Int) = { a + b } // sum(1)(2) = 3

//或者:
def sum(a:Int) = { (b:Int)=> a + b } // sum(1)(2) = 3
// 调用方式二:val t1 = sum(10);  val t2 = t1(20)

1.2.6 定义递归方法

定义递归方法时要求我们必须写明方法的返回值,不能省略,否则会报错。

/** */
def m7(num: Int): Int = {
    if (num <= 0) {
      0
    } else {
      m7(num - 1)
    }
}

1.2.7 定义带可变参数的方法

/**
  * 当参数个数不固定时,可以将参数定位可变参数,可变参数为方法的最后一个参数
  * @param nums
  * @return
  */
def m10(nums:Int*):Int={
  var sum = 0
  for(num <- nums){
    sum += num
  }
  sum
}

1.2.8 定义带默认参数的方法

有时我们调用某些方法时,不希望给出具体的值,而是希望使用参数自身默认的值,此时就在定义方法时使用默认参数
在调用方法的时候,赋值是从左往右以此赋值,所以需要把没有默认值的参数放在前面,或者带参数名调用

/**
  * m11(name="小明",sex="男",age = 12)
  * m11(12)
  */
def m11(age:Int,name:String = "小明",sex:String ="男")={
  printf("name = %s, age= %s, sex= %s \n",name,age,sex)
}

调用方式:

m11(name="小明",sex="男",age = 12)
或
m11(12) //调用时 需要把非默认参数放在参数列表前面

1.2.9 方法的总结

  • 不需要返回值的函数:可以使用 def f() {…},会返回Unit,即使使用return 也会返回Unit。即:def f() {…} 等价于 def f():Unit = {…}

  • 需要返回值的函数:可以使用 def f() = {…} 或者 def f = {…}。def f = { “hello world” } // f是匿名函数 {“hello world”}的别名

  • 三种定义方式的区别:
定义方式 调用方式 返回值
def f() { return .. } f, f()皆可 始终返回:Unit
def f() = … f, f()皆可 返回Unit或者值
def f = … f 返回Unit或者值

1.3 方法的调用


m6() //定义无参函数时带括号,调用时带不带括号都可以

m6_1 //定义无参函数时不带括号,调用时一定不能带括号

// 对象的无参数方法的调用,可以省略 "." 和 "()"
println("hello world" toUpperCase) // "HELLO WORLD"
// 同
println("hello world".toUpperCase)// "HELLO WORLD"
// 同
println("hello world".toUpperCase())// "HELLO WORLD"

// 对象的1个参数方法的调用,也可以省略 "." 和 "()"
println("hello world" indexOf "w") // 6
// 同
println("hello world".indexOf("w")) // 6
// 同
println("hello world" indexOf("w")) // 6

//对象的多个参数方法的调用,也可省略"."但不能省略"()"
println("hello world" substring (0, 5)) // "hello"
// 同
println("hello world".substring(0, 5)) // "hello"

// 在class或者object中可以使用this调用
this.m6()

2. 函数

  • 函数的地位和一般的变量是同等的,可以作为函数的参数,可以作为返回值

  • 传入函数的任何输入是只读的,比如一个字符串,不会被改变,只会返回一个新的字符串。

  • 官方并没有明确的给出方法和函数的区别,包括我们在网上看的一些常用的资料也不会很详细的区分两者的区别

2.1 函数的定义

2.1.1 定义通用格式的函数

val f1 = (x:Int,y:Int) =>{
 x + y 
}

2.1.2 定义函数的参数列表类型

先定义函数的参数列表类型,具体的函数参数在函数体中定义

val f2:(Int,Int,Int)=>Int={
  (x,y,z) => {
    x + y + z
  }
}

2.1.3 定义映射式函数

  • 映射式定义,是一种特殊的定义,相当于数学中的映射关系。
  • 也可以看成是没有参数的函数,返回一个匿名函数,调用的时候是调用这个返回的匿名函数。

def f3 : Int => Double = {
    case 1 => 0.1
    case 2 => 0.2
    case _ => 0.0
}

def f3_1 : Option[String] => String = {
    case Some(x) => x
    case None => null
}

def f3_2:(Int,Int)=>Int = _+_

// 如果唯一的"_"在最后,可以省略
def f3_3:Int=>Int = 30+
// 同
def f3_4:Int=>Int = 30+_

2.1.4 定义偏函数

偏函数(partially applied function)即用 ““(下划线)代替1个或多个参数的函数,使用 ““ 代替参数时需要指定参数的类型
partial函数是一个正常函数中抽出来的一部分(参数不全),故称为partial函数。


val f4 = sum _ // 正确,调用f4(1,2,3) = 6
// 同
(sum _) // 调用(1,2,3)  = 6


val f4_1 = sum(10,_,20) // 错误,需要指定"_" 参数类型


val f4_2 = sum(10,_:Int,20) // 正确  调用f4_2(100) = 130
// 同
(sum(10,_:Int,10)) // 调用 (100) = 130


val f4_3 = sum(_:Int,100,_:Int) //调用 f4_3(10,1) =  111
// 同
(sum(_:Int,100,_:Int)) // 调用 (10,1) = 111

2.1.5 定义匿名函数 (有参)

  • 有一个参数且在最后: (函数实现)(参数)

((i:Int)=> i*i)(3) // 9

((i:Int, j:Int) => i+j)(3, 4) // 7

// 有一个参数且在最后的匿名函数
(10*)(2) // 20
// 同
((x:Int)=>10*x)(2)// 20


(10+)(2) // 12
// 同 
((x:Int)=>10+x)(2) /12


(List("a","b","c") mkString)("=") // a=b=c

// 有参匿名函数 求x的平方
val f5 = (i:Int)=> i*i

// 调用
println(f5(3)) //9

val l1 = List(1,2,3,4).map(f5)
println(l1) //List(1, 4, 9, 16)

2.1.6 定义匿名函数 (无参)

  • 无参数: (()=>函数实现)()
 (()=> 10)() // 10

2.1.7 定义匿名函数 (无参无返回值)

  • 无返回值: ((命名参数列表)=>Unit)(参数列表)
(() => Unit)

(() => {println("hello"); 20*10} )()

注:大量的匿名函数增加了代码的阅读难度。看到符号 “=>” 就要想到函数

2.1.8 定义匿名函数 (匿名参数)

  • 匿名函数中的匿名参数使用 “_”
  • 多个下划线指代多个参数,而不是单个参数的重复使用。
  • 第一个下划线代表第一个参数,第二个下划线代表第二个,如此类推。

// 返回值、函数名和return关键字可以省略
((i:Int, j:Int) => i+j)(3, 4) // 7

// i,j变量名可以省略,变为:

((_:Int) + (_:Int))(3,4) // 7

//括号(_:Int)括号是必须的

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

val sum1 = sum _

val sum2 = sum(1000,_:Int,100)

sum1(1,2,3) // 6

sum2(5)// 1105

2.2 函数传递

  // 求平方函数
  val foreachFun : (Int)=>Unit={
    (x)=>{
      println (s"x^2 = ${x*x}")
    }
  }

  // 过滤大于3的函数
  val filterFun = (x:Int) =>{
    x > 3
  }

  // 2.函数传递
  val arr = Array(1,2,3,4,5)

  //将定义好的函数放进括号内
  arr.foreach(foreachFun)

  /**
    * x^2 = 1
    * x^2 = 4
    * x^2 = 9
    * x^2 = 16
    * x^2 = 25
    **/

  arr.filter(filterFun).foreach(println)

  /**
    * 4
    * 5
    */

  //也可以在括号内直接写函数体
  arr.foreach(x => println (s"x^2 = ${x*x}"))

  /**
    * x^2 = 1
    * x^2 = 4
    * x^2 = 9
    * x^2 = 16
    * x^2 = 25
    **/

2.3 函数作为参数


def f6(x:Int, y:Int, m:(Int, Int)=>Int) = m(x,y)
println(f6(3,4, (x,y)=>x+y)) // 7
println(f6(3,4,(x,y) => scala.math.sqrt(x*x+y*y).toInt)) // 5

def f7(x:Int, y:Int, m: =>Unit) = { println(x*y); m }
f7(3,4,println("end")) // 12// end

def f8(f2:Int=>Int) = f2(5)
println(f8(100-))//105  100+是匿名函数 ((x:Int)=>100+x)的简写
println(f8(100+))//95  100-是匿名函数 ((x:Int)=>100+x)的简写

2.4 函数作为返回值

  def f9(s: String): (Int => String) = {
    n: Int => {
      s * n
    }
  }

  println(f9("*")(5)) //  <function1>*****

3. 函数式编程

首选使用val,用var之前要仔细考虑是否真正需要。

  • if语句
// 非函数式

var s = ""
if ( args.length > 0 ) {
  s = args(0)
} 

// 函数式
val s = if ( args.length > 0 ) args(0) else ""
  • 例如打印

1 2 3

2 4 6

3 6 9


// 非函数式
for(i<-1 to 3; j<-1 to 3) {
    print( i * j + " ");
    if (j==3){
     println
    }
}


// 函数式

def ff(n:Int) = 1 to 3 map ( _*n) mkString " "
println(1 to 3 map ff mkString "\n")

4. 递归

使用递归的原因:

  • 很多数学关系、逻辑关系本身就是递归描述的。

  • 函数式编程不鼓励用变量循环,而是用递归。

递归包含递归出口和递归体:

  • 递归出口:即递归的终止条件
  • 递归体:即和前面或后面的值之间的关系

// 计算幂运算,2^5

def pow(n: BigInt, m: BigInt): BigInt = if (m == 0) 1 else pow(n, m - 1) * n

val p = pow(2, 5)
println(p) //32

5. 尾递归

定义:函数尾(最后一条语句)是递归调用的函数。
tail-recursive会被优化成循环,所以没有堆栈溢出的问题。

  • 使用递归计算阶乘
//线性递归计算阶乘
def nn1(n:Int):BigInt = if (n==0) 1 else nn1(n-1)*n
println(nn1(1000)) // 4023...000
println(nn1(10000)) // 崩溃
println(nn1(5)) //120
nn1(5)
{5 * nn1(4) }
{5 * {4 * nn1(3) }}
{5 * {4 * {3 * nn1(2) }}}
{5 * {4 * {3 * {2 * nn1(1) }}}}
{5 * {4 * {3 * {2 * 1}}}}
{5 * {4 * {3 * 2}}}
{5 * {4 * 6}}
{5 * 24}
120

// 直到递归到一个确定的值后,又从这个具体值向后计算,
// 更耗资源,每次重复的调用都使得调用链条不断加长,系统使用栈进行数据保存和恢复。


//尾递归计算阶乘
def nn2(n:Int, rt:BigInt):BigInt = if (n==0) rt else nn2(n-1, rt*n)
println(nn2(1000,1)) // 40...8896
println(nn2(10000,1)) // 2846...000
println(nn2(5,1)) //120
nn2(5, 1) // 初始参数
nn2(4, 1*5=5) // 第一次递归 n=5 ,rt=1 ,rt*n = 5
nn2(3, 4*5=20) // 第二次递归 n=4 ,rt=5 ,rt*n = 20
nn2(2, 3*20=60) // 第三次递归 n=3 ,rt=20 ,rt*n = 60
nn2(1, 2*60=120) // 第四次递归 n=2 ,rt=60 ,rt*n = 120
120

// 每递归一次就算出相应的结果。
  • 使用递归查找第10001个质数
  // 判断是不是质数 n 对 1 - (n/2) 的每个数取余都不为0,则为质数
  def prime(n:Int) = {
    2 to math.sqrt(n).toInt forall (n%_!=0)
  }

  // p+1 to 2*p 的范围内查找质数
  def next(p:Int) = {
    p+1 to 2*p find prime get
  }

  // 判断查找次数 5 次
  def f(p:Int, i:Int):Int = if (i==5) p else f(next(p), i+1)

  // 从2 开始查找,递增为1
  println(f(2,1)) // 104743

  //或者使用Stream:
  def f2(p:Int):Stream[Int] = p #:: f2(next(p))

  f2(2).take(10001).last // 104743
  • 使用递归计算斐波那契数列(后一个数是前两个数的和)
//例子:fib数列(1 1 2 3 5 8 13 21)
def fib2(n1:BigInt, n2:BigInt, i:Int, n:Int):BigInt ={
  if (i==n) {
    n1
  } else {
    fib2(n2, n1+n2, i+1, n)
  }
}
def fib(n:Int) = fib2(1,1,0,n)
0 to 10 map fib
fib(100-1) 
// 第100个fib数=354224848179261915075

6. 方法与函数的转换

    def m1( x : Int , y : Int ) = {
      x + y
    }

    //通过下划线将方法转换为函数
    val f1 = m1 _
    println(f1) //<function2>

    val sum = f1(1,2)
    println(sum) //3

文章作者: hnbian
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hnbian !
评论
 上一篇
【scala系列】9、Scala模式匹配 【scala系列】9、Scala模式匹配
1. 模式匹配介绍模式匹配是检查某个值(value)是否匹配某一个模式的机制,一个成功的匹配同时会将匹配值解构为其组成部分。它是Java中的switch语句的升级版,同样可以用于替代一系列的 if/else 语句。 1.1 语法一个模式匹配
2020-04-08
下一篇 
【scala系列】7、容器类型:Map、Scala与Java容器类型转换介绍 【scala系列】7、容器类型:Map、Scala与Java容器类型转换介绍
1. Map映射(Map)是一种可迭代的键值对结构(也称映射或关联)。在scala中map分为可变长(mutable)与不可变长(imutable),不可变长map映射初始化之后,其长度与值都不能改变。Scala的Predef类提供了隐式转
2020-04-01
  目录