1 认识Kotlin的函数
1.1 一等公民
在Kotlin中,函数有自己的类型,与类同级,是"一等公民"。
可以比较一下Java中的方法加深理解:在Java中,没有函数,都称为方法(二者区别见后文),方法只能定义在类里面,不能脱离类单独存在;在Kotlin中,函数和方法都存在,脱离类定义的叫函数,定义在类里面的叫方法。
在Kotlin中,函数fun与类class级别相同。可以在合适的条件下赋值、传递和调用
1.2 Kotlin函数的学习路线图
2 Kotlin函数基础
2.1 函数定义
Kotlin中的函数定义由fun关键字开头;接函数名与小括号对,小括号中是形参列表,多个形参用逗号隔开;小括号后面接冒号,用于隔开函数的返回类型;函数的返回类型后面接大括号对,大括号对里面则是函数体,如下所示。
- Kotlin中的空返回值不是
void,而是Unit; - 函数如果返回值是
Unit,则可以省略:Unit。
2.2 函数与方法
- 方法可以认为是函数的一种特殊类型,可以说函数包含了方法;
- 从形式上,有receiver的函数即为方法,所谓receiver,是指调用方法的对象或运行时类;
- 脱离类定义的叫函数,定义在类里面的叫方法
//Kotlin
class people {
//TODO:方法(Method)
fun sayHi() {
println("Hi!")
}
}
//TODO:函数(function)
fun doNothing(anything:Any):Any {
return anything
}
fun main() {
//方法的调用者叫receiver
val p = people()
p.sayHi()
//函数没有receiver
doNothing(123)
}
2.3 函数类型
2.3.1 函数类型基础
先看一张图:
再思考一个问题,上面说到Kotlin中的函数可以传递,也就是可以作为函数形参,这个形参如何定义,是什么类型?
-
Kotlin综合形参类型列表和返回值类型,给出了函数的类型形如
(形参列表)->返回值类型; -
函数类型中的
->Unit不能省略; -
形参类型列表不同于形参列表,形参列表是
arg1:Type1,arg2:Type2,对应的形参类型列表不包含形参名,是Type1,Type2; -
定义在类里面的函数(方法)呢?
对于定义在类里面的函数(方法),其函数类型有三种写法:
- 第一种,
Receiver.(Type1,Type2)->Type3; - 第二种,
(Receiver,Type1,Type2)->Type3; - 第三种,见2.3.3
其实像第二种这样将receiver写成第一个形参的,在面向对象的语言中比较多见,例如在Python中,定义在类中的所有函数的第一个参数都是self,即为receiver。只是,在Java中,编译器帮我们省略了这一点,以至于现在猛地一看有些不适应。
#Python
class People:
def sayWords(self, words):
print(words)
2.3.2 receiver具体化
对于以下这个函数(方法):
//Kotlin
class Person(var name: String, var age: Int) {
fun sayHi() {
println("Hello! How are you doing?")
}
}
fun main() {
val mike = Person("Mike", 23)
val func1 = Person::sayHi
val func2 = mike::sayHi
}
Person::sayHi与mike::sayHi是两个不同的函数,如果将receiver写成函数(方法)的第一个传入参数,则
Person::sayHi的函数类型是(Person)->Unitmike::sayHi的函数类型是()->Unit
这种receiver具体化的情况与数学中的变量降维类似:
f(x, y, z) = (x + y) * z
f(2, y, z) = (2 + y) * z
#当x具体化为2后,不再是变量,函数可以写成
g(y, z) = (2 + y) * z
2.3.3 函数类型的三种写法
对于Person类中的sayHi函数(方法),其函数类型有三种表示方式:
(Person) -> Unit:将receiver作为第一个形参写到括号里面;Person.() -> Unit:将receiver放到括号前面,用调用符分隔;Function1<Person, Unit>:使用Kotlin内置的函数类型容器,其中Function1的1表示<>中只有前1个是传入参数,之后的是传出参数;有多少传入参数,就使用对应的函数类型容器:Function0<Out>Function2<In1, In2, Out>Function3<In1, In2, In3, Out>- ... ...
//Kotlin
class Person(var name: String, var age: Int) {
fun sayHi() {
println("Hello! How are you doing?")
}
}
val f1: (Person) -> Unit = Person::sayHi
val f2: Person.() -> Unit = Person::sayHi
val f3: Function1<Person, Unit> = Person::sayHi
2.4 函数引用
函数引用这个词应该是没有听说过的,但是类的引用在Java中不少。
//Java
class Person {
String name;
int age;
Person(String name, int age){
this.name = name;
this.age = age;
}
public void greet(Person p) {
System.out.println("Hello!"+p.name);
}
}
public static void main(String[] args) {
//Person类的引用
Person mike = new Person("Mike", 22);
Person jack = new Person("Jack", 23);
//引用可以赋值给相同类型的变量
Person aBoy = mike;
//引用可以作为实参传递给形参
mike.greet(jack);
}
函数引用也是类似的,前面的函数类型给函数定好了框架,没有规定函数的名字和函数体,因此同一个函数类型之下可以有无数种具体的函数实现。我们可以定义一个类型相同的变量来接收该类型的函数实现:
//Kotlin
fun print(content:String) {
println("print println : $content")
}
//函数引用给变量赋值
val p:(String)->Unit = ::print
//设置函数类型的形参
fun printAndReturn(func:(String)->Unit):String {
val str = "Hello"
func(str)
return str
}
fun main() {
//引用传递
val str = printAndReturn(p)
println("main println : $str")
}
以上代码中,函数print是函数类型(String)->Unit的一个具体实现,可以定义一个该类型的变量p来接收函数print的引用,该函数的引用表示为::print,可以理解双冒号::为C++中的命名空间符号,只是这里的函数print没有receiver,所以::之前是空的。
注意!函数引用的写法仅仅取决于receiver和函数名,写作
receiver::funcName,当然,没有receiver的就写作像上面的::funcName。若存在类似于函数重载的情况,一旦编译器无法推导,则报错。
2.5 函数调用
任何位置的函数都有两种调用方式:
funcName(In1, In2);funcName.invoke(In1, In2)。
第一种是常见的写法,第二种的invoke函数(方法)是所有函数类型的内置方法,顾名思义,就是调用的意思。
//Kotlin
class Person(var name: String, var age: Int) {
fun communicate(selfIntroduction: (Person) -> Unit, ask: (Person) -> Unit, p: Person) {
selfIntroduction(this)//TODO:第一种函数调用方式
ask.invoke(p)//TODO:第二种函数调用方式
}
}
fun selfIntroduction(p: Person) {
println("I am ${p.name} and I am ${p.age} years old.")
}
fun ask(p: Person) {
println("Are you ${p.name}? Are you ${p.age} years old?")
}
fun main() {
val mike = Person("Mike", 23)
val jack = Person("Jack", 22)
mike.communicate(::selfIntroduction, ::ask, jack)
}
2.5 变长参数
在Java中的main函数(方法)的写法有两种:
//Java
//1
public static void main(String[] args){}
//2
public static void main(String...args){}
第一种是String数组的形参接受控制台的传入参数;第二种是String类型的变长参数接受控制台的传入参数。
与之类似,Kotlin也有变长参数,例如写一个函数希望接收多个Int:
//Kotlin
//1
fun multiParam(ints: IntArray) {}
//2
fun multiParam(vararg ints:Int) {}
借助vararg关键字,指定紧随其后的形参是一个可变长度的参数。
传进来怎么用?其实传进来的ints就是一个IntArray类型的变量:
//Kotlin
fun multiParam(vararg ints: Int) {
println(ints.size)
println(ints[0])
ints.forEach {
println(it)
}
}
2.6 多返回值
借助Pair,Triple等内置引用类型与()解构运算实现函数的伪多返回值效果。
//Kotlin
fun multiOutput(): Triple<Int, Long, String> {
return Triple(1, 9L, "Hello")
}
fun main() {
val (i, l, str) = multiOutput()
println("i = $i\nl = $l\nstr = $str")
//i = 1
//l = 9
//str = Hello
}
2.7 默认参数与具名参数
在定义函数的时候,可以给形参赋值默认参数,在调用时,若不传入该参数,则在函数内部使用该参数的默认值:
//Kotlin
fun defaultParam(x: Int, y: String, z: Long = 0L) {
println("x = $x\ny = $y\nz = $z\n")
}
fun main() {
defaultParam(10, "hello")
}
一般而言,将带有默认值的形参放在形参列表的最后,避免混淆编译器
调用时,指定具名参数,也可以不考虑上面的一般而言,这里的具名值得是形参的名字