Kotlin学习笔记——函数

Aengus Sun 930 2020-06-30

函数Function

Kotlin中的函数相较于Java增加了很多特性,比如高阶函数、扩展函数、内联函数等,我们会逐一进行介绍。

之前我们已经说过,函数的声明如下:

fun functionName(parameter: Type): ReturnType {
    // 函数体
}

其中如果无返回值,那么ReturnTypeUnit或者不写。

如果函数体只有一个return语句,我们还可以直接省略大括号和return关键字,直接用等号返回值:

fun power(x: Int) = x * x

Kotlin中去掉了expr? a : b的用法,取而代之的是使用if语句,用法如下:

fun isMale(people: People) = if (people.sex == "男") true else false

选择表达式when

Kotlin使用when关键字代替了Java中的switch,并且提供了更加强大的功能,其基本用法如下:

when (condition) {
    case1 -> doSomeThing1()
    case2 -> doSomeThing2()
    else -> doSomeThing3()
}

可以看到Kotlin用else关键字代替了Java中的default并且可以自动break。除此以外还可以通过is关键字判断其类型,is关键字还可以帮我们完成类型转换,这就是Kotlin中的smartcast(顺便说一句Kotlin的类型转换使用as关键字)。下面的例子是一个较为完整的例子,需要注意的是when表达式会取每个case块的最后一行作为整个表达式的返回值,这种思想在Kotlin中非常常见,如Lambda表达式,上面的if语句示例中也可以看到每个condition的最后一行作为了整个语句的返回值(truefalse):

open class Animal(val name: String = "未知动物")
class Cat(val catName: String) : Animal(catName) {
    fun miaow() {
        println("$catName 在叫")
    }
}
class Dog(val dogName: String) : Animal(dogName) {
    fun bark() {
        println("$dogName 在叫")
    }
}
// 需要指定返回类型,否则类型推导会出错
fun whichAnimalAndReturnAnotherAnimal(animal: Animal): Animal = when (animal) {
    is Cat -> {
        animal.miaow()
        Cat("汤姆猫")
    }
    is Dog -> {
        animal.bark()
        Dog("二哈")
    }
    else -> {
        println("没见过这种动物")
        Animal()
    }
}
fun main() {
    val cat = whichAnimalAndReturnAnotherAnimal(Cat("加菲猫"))
    println(cat.name)
    val dog = whichAnimalAndReturnAnotherAnimal(Dog("拉布拉多"))
    println(dog.name)
    val unknown = whichAnimal(object : Animal() {})
    println(unknown.name)
}
// 输出:
// 加菲猫 在叫
// 汤姆猫
// 拉布拉多 在叫
// 二哈
// 没见过这种动物
// 未知动物

可以看到当代码判断了类型后,可以直接调用这个子类型特有的方法,而不用我们手动进行类型转换。

默认参数Default Params

Kotlin中的函数和构造函数一样,也支持默认参数,如下:

fun sumThree(x: Int, y: Int = 1, z: Int = 1) = x + y + z
// 调用
sumThree(1) // 3

需要注意的是,如果某个非默认参数之前有默认参数,那么必须使用命名参数对这个非默认参数进行赋值,如下:

fun sumThree2(x: Int = 1, y: Int, z: Int = 1) = x + y + z
// 调用
sumThree(y = 1) // 3

比较推荐的做法是将所有的非默认参数放在默认参数之前,并且调用时尽量用命名参数的方式进行传参,以提高代码可读性。

Getter/Setter

Kotlin会对其属性生成默认的setter和getter方法,我们可以通过下面的方法重写setter和getter方法:

class User {
    var age: Int = 0
        set(value) {
            if (value > 0) {
                field = value
            } else {
                field = 0
            }
        }
    var name: String = "Aengus sun"
        get() = field.split(" ")[0]
}

正常情况下属性都需要初始化,其中field关键字代表我们重写setter或getter方法的字段,在setter和getter方法中,只能通过thisfield关键字进行某些操作,而不能直接访问属性名。对于val属性只有getter方法。

我们也可以使用lateinit关键字实现稍后对属性进行初始化,这时候编译器便不会对未初始化的属性报错,但是要谨慎使用这个关键字,否则运行时仍有可能报出NullPointerException

class Activity {
    lateinit var view: View 
}

可变参数Varargs

Java中可以通过...传入可变数量的参数,而Kotlin中则使用vararg关键字实现:

fun varargs(vararg p: Int) {
    for (item in p) {
        println(item)
    }
}

扩展函数Extension

扩展函数是一个非常有用的特性。扩展函数是指给某个已经封装好的类或者不可更改源代码的类增加“成员函数”,这样我们就可以使用调用成员函数的方法来调用我们自定义的函数。扩展函数声明方式如下:

class Person(val firstName: String, val lastName: String)
// 下面是扩展函数
fun Person.getFullName() = "${this.firstName} ${this.lastName}"
// 可以用调用成员函数的方式调用扩展函数
Person("Aengus", "Sun").getFullName() // Aengus Sun

扩展函数可以和成员函数相同,但是调用时成员函数优先调用,扩展函数也可以和成员函数使用相同的名称但是不同的参数以实现函数重载。

扩展函数也可以用在可空类型上,如:

fun Any?.toString(): String = if (this == null) "null" else this.toString()

主要注意的是如果父类和子类有相同的扩展函数,如果在某函数中声明的参数为父类,在运行时传入的参数为子类,那么调用的扩展函数将是父类的扩展函数而不是子类的,也就是扩展函数不会在运行期确定调用对象。示例如下:

open class Father
fun Father.getName() = "父类"
class Son : Father()
fun Son.getName() = "子类"
fun printName(p: Father) {
    println(p.getName())
}
fun main() {
    printName(Son())  // 输出 “父类”
}

也可以为类定义扩展属性,但是扩展属性不允许进行初始化,仅允许定义getter与setter:

val <T> List<T>.lastIndex: Int // 泛型方法与Java中的一致
    get() = size - 1

伴生类也同样可以拥有扩展函数与扩展类,用法和标准扩展函数一致:

class MyClass {
    companion object { /* ... */ }
}
fun MyClass.Companion.doSomeThing() { /* ... */ }
// 调用
MyClass.doSomeThing() // 或 MyClass.Companion.doSomeThing()

扩展函数可以被导入;扩展函数可以定义在与类同级的地方,这类扩展函数可以在任何地方使用;也可以定义在类内部,这类扩展函数只能在这个类中调用:

// 导入以及调用
import com.example.functions.getLongestString
var list = listOf("a", "ab", "abc")
list.getLongestString()
// 定义在和类同级的地方
fun Int.power() = this * this
class Main {
    // 定义在类内部
    fun Int.double() = 2 * this
}

如果某个类中定义了另外一个类的扩展函数,并且与另外一个类的成员函数名相同,那么另一个类的函数被优先调用:

class Test {
    // Int已经有 toString() 函数了
    fun Int.toString() = "Int: $this"
    
    fun test() {
        println(1.toString) // 1
    }
}

中缀表示法Infix

中缀表示法可以理解为用二元运算符的形式调用函数,使用infix关键字声明中缀函数:

infix fun String.concatWithWaveLine(another: String): String = "$this~$another"
// 调用
"aaa" concatWithWaveLine "bbb"  // aaa~bbb
// 和下面的方式调用一样
"aaa".concatWithWaveLine("bbb")

中缀函数的优先级非常低:

"aaa" concatWithWaveLine "bbb" + "ccc" // "aaa" concatWithWaveLine ("bbb" + "ccc")

中缀函数有以下几个限制:

  • 中缀函数必须是成员函数或者扩展函数;
  • 函数只能有一个参数;
  • 参数不能是可变参数而且不能有默认值;

高阶函数Higher-Order

在Kotlin一切皆对象,所以函数也可以作为参数传入到另一个函数中,那些以函数作为参数或者返回类型的函数就叫做高阶函数。函数是一类对象而不是一种对象,因为函数由参数和返回类型唯一确定,比如参数为String,返回Int的函数和参数为Int,返回Int的函数就不是一种函数。函数类型的表示方法如下:

(param1: Type1, param2: Type2) -> RetunType

括号中的为参数,箭头右边是返回类型。参数也可以不需要参数名,直接使用类型作为占位符,但是推荐上面的写法,方便IDE自动补全:

(Type1, Type2) -> ReturnType

高阶函数声明如下:

// 参数为函数
fun higherOrderFun1(block: () -> Int): Unit
// 返回类型为函数
fun higherOrderFun2(): () -> Int

很多Kotlin自带的函数都接收一个函数作为参数,比如之前所说的by关键字常用的lazy()函数,它就是接收一个函数参数并进行调用。

匿名表达式Lambda

Java从1.8开始也支持了Lambda表达式,但是并没有实质上的突破,仅仅是方便编写代码,而Kotlin中的Lambda表达式则强大很多。首先,在Kotlin中一切皆对象,所以我们可以直接利用Lambda表达式赋值给一个变量;利用Lambda表达式也可以非常方便的调用高阶函数。

Lambda表达式的声明方式如下:

val sum: (a: Int, b: Int) -> Int = { a: Int, b: Int -> a + b }

等号的右侧便是一个Lambda表达式。Lambda表达式用一个大括号围起来的代码块,代码块里首先是参数声明,然后是一个箭头,接着是函数体。**Lambda表达式会取代码块中的最后一行作为整个代码块中的返回值,**这种思想我们在之前的when表达式中已经见过了。此外Kotlin的类型推导也允许我们省掉参数声明的类型声明,比如上面的例子我们可以省略掉后面的a,b的类型说明:

val sum: (a: Int, b: Int) -> Int = { a, b -> a + b }
// 或者
val sum = { a: Int, b: Int -> a + b }

Lambda表达式在高阶函数中的用法如下:

fun higherOrderFun(a: Int, b: Int, block: (a: Int, b: Int) -> Int): Int = block(a, b)
// 调用
val c = higherOrderFun(1, 2, { a, b ->
    a + b
})  // c = 3

在使用Lambda表达式时,如果Lambda是最后一个参数,可以直接将Lambda代码块写在外面;如果只有Lambda一个参数,可以直接省略括号;如果Lambda表达式只有一个参数,还可以直接写代码块,而这个参数默认用it表示:

fun higherOrderFun(a: Int = 1, b: Int = 1, block: (a: Int, b: Int) -> Int): Int = block(a, b)
// 调用
higherOrderFun(10, 10) { a, b -> 
    a + b
}
higherOrderFun { a, b ->
    a + b
}
// 唯一参数示例
fun <T> List<T>.every(block: (item: T) -> Unit) {
    for (item in this) {
        block(item)
    }
}
// 调用
fun <T> printAll(list: List<T>) {
    list.every {
        println(it) // 这里的 it 是默认参数名
    }
}

上面提到Lambda表达式会取最后一行作为返回值,在表达式内部原则不允许使用return,虽然可以使用下面的方式使用return,但是并不推荐:

higherOrderFun(1, 2) { a, b ->
    return@higherOrderFun a + b
}

始终记住一个原则:Kotlin中return的规范用法是退出命名函数或匿名函数。

函数类型

上面的sum便是一个函数类型,可以直接将函数类型作为参数传给高阶函数中,下面是一个完整的例子:

fun higherOrderFun(a: Int, b: Int, block: (a: Int, b: Int) -> Int) {
    println(block(a, b))
}
fun main() {
    val abs = {a: Int, b: Int -> 
        if (a > b) a - b else b - a
    }
    higherOrderFun(1, 2, abs)  // 1
    higherOrderFun(2, 1, abs)  // 1
}

上面我们声明了一个高阶函数,它接受两个Int参数与一个函数参数,我们使用Lambda的方法声明了一个函数类型abs,并可以直接将其作为参数传给了高阶函数中。

直接声明的函数并不是一个函数类型,但我们可以使用::操作符将其变为一个函数类型:

fun higherOrderFun(a: Int, b: Int, block: (a: Int, b: Int) -> Int) {
    println(block(a, b))
}
fun abs(a: Int, b: Int) = if (a > b) a - b else b - a
fun main() {
    higherOrderFun(1, 2, ::abs)  // 1
    higherOrderFun(2, 1, ::abs)  // 1
}

若一个函数是顶级函数(即不属于任何一个类,就像上面的abs()),则直接使用::function的方式;如果函数是某个类Test的成员函数,则使用Test::function的方式将其变为一个函数类型。Kotlin官方把这种方法称作函数引用。

匿名函数

匿名函数和Lambda表达式非常像,不同之处是匿名函数结构上更像是一个函数,其声明方法如下:

fun(param1: Type1, param2: Type2): ReturnType { /* ... */ }

可以看到它和普通函数的区别仅仅是没有函数名。匿名函数也可以作为参数用在高阶函数中:

higherOrderFun(1, 2, fun(a: Int, b: Int): Int {
    return a + b
})

内联函数Inline

使用高阶函数增加了运行时开销,这是因为每个Lambda表达式都生成了一个函数类型对象。内联的含义是在编译时,内联函数的函数体会被直接复制到调用它的地方,省去了创建对象的开销。内联函数声明如下:

inline fun functionName(p: Type, block: () -> ReturnType2): ReturnType1

内联函数适合较小的函数体,如果函数体较大则会增加整个代码的体积,所以不能滥用。由于内联函数会被直接替换,所以内联函数中的return语句也会直接让调用它的函数退出,有时甚至会因为返回类型不一致导致报错。

下面是内联函数编译后的例子:

inline fun test(a: Int, block: (a: Int) -> Int) {
    println("需要处理的数字是 $a")
    val res = block(a)
    println("处理后是 $res")
}
fun negative(num: Int) {
    test(1) { a ->
        -a
    }
}
// 编译后
fun negative(num: Int) {
    println("需要处理的数字是 $a")
    val res = -a
    println("处理后是 $res")
}

内联函数也有一个问题,内联函数中的函数参数也是内联的,由于函数参数在编译时会被替换成函数体,如果在内联函数中有另外一个非内联的函数调用了内联函数参数就会发生报错,这时候我们可以用noinline关键字将函数参数变为非内联的:

inline fun test(a: Int, block: () -> Unit, noinline call: () -> Unit)

内联函数中还有一个关键字是crossinline,它的作用是禁止被标记为crossinline的Lambda表达式非局部返回。非局部返回是指内联函数中的Lambda表达式中允许含有普通return语句(普通Lambda中不允许),而crossinline的作用就是禁止它:

inline fun test(crossinline block: () -> Unit)

这样Lambda表达式中含有return语句就会报错了:

fun callFun() {
    test {
        return  // 报错
    }
}

挂起函数Suspend

挂起函数声明如下:

suspend fun functionName(param1: Type, param2: Type): ReturnType { /* ... */ }

挂起函数的主要作用在协程上,我们将会在之后进行说明。

运算符重载Operation

Kotlin支持运算符重载,和C++不同之处在于,Kotlin指明需要重载的运算符不是通过符号本身,而是使用符号名。Kotlin运算符重载使用operator关键字,声明如下:

data class Point(val x: Int, val y: Int) {
    operator fun plus(point: Point) = copy(this.x + point.x, this.y + point.y)
}
operator fun Point.minus(p: Point) = copy(this.x - p.x, this.y - p.y)

可以看到可以通过成员函数或者扩展函数的方式进行运算符重载,上面两个分别代表重载+-号,重载后就可以用下面这种方式调用了:

val p1 = Point(1, 1)
val p2 = Point(2, 2)
val p3 = p1 + p2	// Point(3, 3)
val p4 = p2 - p1  // Point(1, 1)

可以重载的运算符如下:

运算符重载方式 重载方式重载方式
+aa.unaryPlus() -aa.unaryMinus()
!aa.not() a++a.inc()
a--a.dec() a + ba.plus(b)
a - ba.minus(b) a * ba.times(b)
a / ba.div(b) a % ba.rem(b)
a..ba.rangeTo(b) a in bb.contains(a)
a !in b!b.contains(a) a[i]a.get(i)
a[i, j]a.get(i, j) a[i] = ba.set(i, b)
a()a.invoke() a(i)a.invoke(i)
a(i_1, ..., i_n)a.invoke(i_1, .., i_n) a += ba.plusAssign(b)
a -= ba.minusAssign(b) a *= ba.timesAssign(b)
a /= ba.divAssign(b) a % ba.remAssign(b)
a == ba?.equals(b) ?: (b === null) a != b!(a?.equals(b)) ?: (b === null)
a > ba.compareTo(b) > 0 a < ba.compareTo(b) < 0
a >= ba.compareTo(b) >= 0 a <= ba.compareTo(b) <= 0