函数,目的是为了重用性、可组合性、简短、可读。
在Scala中,函数(function)、方法(method)在语义上的差别很小。但不同的“语境“下:
- 函数,是一个完整的对象,可以赋值给一个变量,并且可将其作为一个参数传入到方法中(而方法不可以)。不过它其实是继承了Trait(相当于Java的接口,而实际上比接口 功能还更强大)的类的对象;
- 方法,是组成类的一部分,即在类中定义的函数就是方法;
- 使用
val
关键字可以定义函数,def
关键字可以定义方法
Scala在省略代码量是下足功夫,只要是能推断出来的,都可以不写!!甚至连def
、{}
、函数名、返回值类型、=
等都可以省略!!!
以下将围绕函数组成部分中的 参数、返回值的数据类型、函数体:
一、关于参数
1、纯函数、非纯函数
纯函数:输入输出数据流全是显式的(Explicit),就是说 函数体内的参数全部由函数提供,非纯函数反之。
scala> def add(a:Int,b:Int) = a + b //纯函数。在实际中,用的比较少
add: (a: Int, b: Int)Int
scala> add(1,2)
res0: Int = 3
scala> var a = 1
a: Int = 1
scala> def addA(b:Int) = a + b //非纯函数。实际中用的比较多,但是开发过程尽量少用。不过也避免不了,好多情况是纯函数解决不了的
addA: (b: Int)Int
scala> addA(2)
res1: Int = 3
2、括号内是参数列表,各个参数之间用逗号隔开。若有参数,那么所有参数就必须指定数据类型(就算是默认参数也必须指定);没有参数,可以不写括号()
scala> def my_max(x:Int, y:Int) = if (x>y) x else y
my_max: (x: Int, y: Int)Int
scala> my_max(1,2)
res15: Int = 2
scala> def a = 1
a: Int
scala> a
res16: Int = 1
scala> def hi = "hi"
hi: String
scala> hi() //调用时,不能使用hi()
<console>:9: error: not enough arguments for method apply: (index: Int)Char in class StringOps.
Unspecified value parameter index.
hi()
^
scala> hi
res3: String = hi
scala> def hi() = "hi" //调用时,可用hi、hi()都行
hi: ()String
scala> hi
res4: String = hi
scala> hi()
res5: String = hi
3、默认参数:有默认值的参数
在函数定义时,直接为该参数 赋好值。
- 在调用它时,可以不设置已有默认值的参数;
- 也可以在调用时,设置默认参数的值,设置值时会按照函数该默认参数所在的顺序依次设置。传统调用函数时,参数都是按照顺序。
- 也可以在调用时,指定参数名对其进行赋值。新值会覆盖掉默认值。此时,如下的参数
left="#"
就称之为命名参数,这样就不用按照顺序了。
scala> def m2(name:String, left:String="<", right:String=">") = {
| left + name + right
| }
m2: (name: String, left: String, right: String)String
scala> m2("mayun")
res10: String = <mayun>
scala> m2("mayun", "<<")
res11: String = <<mayun>
scala> m2("mayun",left="#")
res12: String = #mayun>
这在Spark或其他时,可用于设置默认配置等等。很有用处。
4、变长参数(可变参数):不确定参数个数时使用
scala> def sum(args:Int*) = {
| var res = 0
| for(arg <- args) res += arg
| res
| }
sum: (args: Int*)Int
scala> sum(1,2,3,4,5,6,7,8,9)
res14: Int = 45
scala> sum(1 to 9)
<console>:9: error: type mismatch;
found : scala.collection.immutable.Range.Inclusive
required: Int
sum(1 to 9)
^
scala> sum(1 to 9 :_*)
res16: Int = 45
上述sum函数
的参数类型是Seq类型
,如果有一个值的序列不能传入到变长参数中,此时会报错:type mismatch
类型不能匹配
Scala设计的解决方法是:在传入的已有序列后追加 :_*
,将一个Range
转成一个Seq
。
5、参数组
将参数分解成参数组。
def add(x:Int, y:Int) = x + y
等价
scala> def add(x:Int) (y:Int) = x + y
add: (x: Int)(y: Int)Int
scala> add(1,2) //但调用时,这样是错误的!
<console>:9: error: too many arguments for method add: (x: Int)(y: Int)Int
add(1,2)
^
scala> add(1)(2)
res19: Int = 3
6、传名参数
表现形式:(参数名: =>Int)
。注意英文冒号:
和=>
之间有空格
scala> def doub(x: =>Int) = x*2
doub: (x: => Int)Int
scala> doub(2)
res31: Int = 4
scala> doub(doub(2))
res32: Int = 8
这样的函数,它的参数可以接受值、也可以接受函数(当然所接受函数的返回值得是一致的,如上述的Int
)
7、类型参数
前面传的都是值 参数,现在向函数传递类型参数
//需求:函数输入什么数据类型,函数就返回什么类型。
//方案:给函数传入个类型参数,根据传入的类型,返回什么类型。
scala> def m3[A](a:A):A=a
m3: [A](a: A)A
scala> m3("hi")
res23: String = hi
scala> m3(1.0)
res24: Double = 1.0
scala> m3(1)
res25: Int = 1
scala> m3(true)
res26: Boolean = true
8、命名参数
函数调用时,传参时,把参数名带上,就与顺序无关了。有这种 通过名字识别的机制。
当然,最好是按常理出牌,按顺序。
scala> def speed(distance:Float, time:Float):Float = {
| distance/time
| }
speed: (distance: Float, time: Float)Float
scala> speed(100,10)
res32: Float = 10.0
scala> speed(distance=100,time=10)
res33: Float = 10.0
scala> speed(time=10,distance=100)
res34: Float = 10.0
二、关于返回值
1、返回值的数据类型 写不写?
递归函数不可省略返回值的数据类型,其他情况可以省略(函数可以通过等号右侧的函数体推断出返回值的数据类型)
2、返回值也可以不写,写上将会有更好的可读性。默认函数体最后一个表达式(最后一行) 为返回值,不需要使用return
。但是需要在函数的表达式快结束前退出 并返回其值,可使用关键字return
scala> def m1(str:String):String = {
| if(str == null) return null
| str.trim()
| }
m1: (str: String)String
scala> m1(null)
res7: String = null
scala> m1("hello ")
res8: String = hello
Scala函数都有返回值,只不过有的返回的是 Unit,()
scala> def greet() = println("Hello, world!")
greet: ()Unit
scala> greet
Hello, world!
scala> def greet = println("Hello, world!")
greet: Unit
scala> greet
Hello, world!
其中,Unit
是Scala语言中 数据类型的一种,表示无值,用于不返回任何结果的函数/方法。(类似Java中的Void)
3、过程
在定义函数时,如果函数体直接包裹在了{}
中,而没有用=
连接,那么函数的返回值类型就是Unit
(即便在函数定义时指定了返回值,返回值类型依旧是Unit
)。这样的函数称之为 过程。
scala> def greet{println("Hello,world!")}
greet: Unit
scala> greet
Hello,world!
综合:函数类型和函数值
函数类型,由输入类型、输出类型的一个简单组合。有一个箭头从输入类型指向输出类型。
scala> def doub(x:Int) = x*2 //此处的函数类型为 Int=>Int
doub: (x: Int)Int
scala> val myDoub:Int=>Int=doub //Int=>Int 是一个整体,代表函数类型
myDoub: Int => Int = <function1>
scala> myDoub(3)
res29: Int = 6
myDoub,是函数值。
scala> val md = doub _
md: Int => Int = <function1>
scala> md(3)
res30: Int = 6
上述没有显示函数类型(Int=>Int
)来区分函数调用。下划线 _
相当于一个占位符,表示一个函数调用,在此会返回一个函数值,并将这个值保存到md
变量中。
三、关于函数体
1、只有一条语句时,花括号{}
可以省略。单行写
2、函数体中,可以加入各种控制结构。
3、函数体是最重要的部分。但除此之外的部分却又很多花样!!!
四、彩蛋?匿名函数
函数字面值,业界喜欢称之为 匿名函数。语法:
关键字 变量名 = (参数名:数据类型) => {函数体}
scala> var f = (x:Int) => x+1
f: Int => Int = <function1>
scala> f(2)
res47: Int = 3
scala> var mul = (x:Int,y:Int) => x*y
mul: (Int, Int) => Int = <function2>
scala> mul(2,3)
res48: Int = 6
正常函数是 参数、函数体之间用 =
连接;
匿名函数是 参数、函数体之间用 =>
连接。
其他各种“特色”函数:递归函数、嵌套函数、高阶函数、偏函数等,以后细讲
五、还是彩蛋?函数、方法的区别
理论方面在开头已讲述。
1、定义方式
方法(method)一般是以def
关键字开头,带有参数列表(或无)的一个逻辑操作块。这其实就像object
、class
中的成员方法一样。
函数(function)是一个赋值给一个变量的匿名方法(带、或不带参数列表),并且可以通过=>
转换符号连接逻辑代码块的一个表达式。=>
转换符号后面的逻辑代码块写法 跟方法(上述将的函数体)相同。
scala> def m1(x:Int) = x+1 //定义方法
m1: (x: Int)Int
scala> val f1 = (x:Int) => x+1 //定义函数
f1: Int => Int = <function1>
2、表现形式
scala> m1
<console>:9: error: missing arguments for method m1;
follow this method with `_' if you want to treat it as a partially applied function
m1
^
scala> f1
res34: Int => Int = <function1>
方法(带参)不能作为最终的表达式,如上报错了;
不带参数的方法可以,但此时就是方法调用了(本质上不同了),因为Scala允许无参方法调用时省略括号()
。
函数可以作为最终的表达式出现。
方法名 意味着方法调用;函数名 只是代表函数自身。
3、参数要求
scala> def m2 = 666 //不带参数列表。即没有入参的函数,调用时不用写括号。
m2: Int
scala> m2
res38: Int = 666
scala> m2() //本身就没有参数。写法本身就错了
<console>:9: error: Int does not take parameters
m2()
^
scala> def m3()=555 //空的参数列表
m3: ()Int
scala> m3
res40: Int = 555
scala> m3()
res41: Int = 555
scala> val f2=() => 666 //必须设置参数列表,否则报错
f2: () => Int = <function0>
scala> val f2= => 666
<console>:1: error: illegal start of simple expression
val f2= => 666
^
方法 可以没有参数列表;也可以是空的参数列表。
函数 必须设置参数列表,可以是空的参数列表,但一定要设置。
4、函数、方法相互转换
将 x => func(x)
简化为 func _
或func
的过程称之为:ETA-conversion(函数转换成方法);
将func _
或func
展开为 x => func(x)
的过程称之为:ETA-expansion(方法转换成函数)。
scala> def m4(x:Int) = f1(x) //把函数转换成方法
m4: (x: Int)Int
scala> m4(2)
res44: Int = 3
scala> val f3 = m1 _ //把方法转换成函数
f3: Int => Int = <function1>
scala> f3(2)
res43: Int = 3
其中,下划线_
是作为Scala中的占位符,它可以用传入的参数代替。这就是占位符语法。
scala> val f5 = (_:Int) + (_:Int)
f5: (Int, Int) => Int = <function2>
scala> f5(2,3)
res46: Int = 5
占位符 还可以用于设置函数的别名,如上述m1
,在此m1
不用传入参数,直接加个占位符即可。