Kotlin 入门:变量、函数和控制语句

227 阅读11分钟

变量和函数

变量

定义变量

Kotlin 中定义变量需要使用两种关键字:val 和 var。

val 是 value 的简写,用来声明一个不可变的变量,也就是变量初始被赋值之后,不能被重新赋值,在 Java 中就是在变量前加上了 final 关键字。

var 是 variable 的简写,用来声明一个可变的变量,变量初始被赋值之后,还是可以随便修改它的值。

我们只需这样就能声明一个不可变的变量:

fun main(){
    val a = 10
    println("a = $a")
}

这里我们并不用显式地指定变量的类型,因为 Kotlin 具有类型推导机制,它会根据变量的赋值操作,来判断变量的类型。

然而,有些情况下,我们还是需要显式地指定变量的类型。

比如一个延迟初始化的变量(变量声明时没有立即被赋值,你可以先不关心它是什么),Kotlin 就无法推导出这个变量的类型。又或者为了代码的可读性或者特殊需要,我们也要显式声明变量的类型,就像这样:

fun main() {
    val a: Short = 10 // 显式指定为 Short 类型,否则会被推导为 Int 类型
    println("a = $a")
}

数据类型

Kotlin 其实是没有 Java 的基本类型的,所有数据类型都是对象数据类型

比如刚刚的 Int 实际上是一个类:

public class Int private constructor() : Number(), Comparable<Int> {
    // ..
}

那我们来看看 Java 中的基本数据类型在 Kotlin 中对应的对象数据类型:

Java基本数据类型Kotlin对象数据类型
intInt
longLong
shortShort
floatFloat
doubleDouble
booleanBoolean
charChar
byteByte

从上到下分别为:整型、长整型、短整型、单精度浮点型、双精度浮点型、布尔型、字符型、字节型。

val 和 var 的选择

现在我们修改不可变变量的值,比如乘以 2 倍:

fun main() {
    val a: Int = 10
    a = a * 2 // 错误
    println("a = $a")
}

发现报错了:Val cannot be reassigned

这是因为 a 是不可变的变量,不能被重新赋值,所以我们就需要改为可变变量,只需使用 var 关键字声明变量:

fun main() {
    var a: Int = 10
    a *= 2
    println("a = $a")
}

这样就行了,运行结果:

那现在你可能会有一个问题,val 连修改变量的值都不行,为什么要用它?

其实 val 的出现是为了解决 Java 中 final 关键字没有被正确使用的问题。

因为在 Java 中,你不知道一个可变变量会在什么时候被谁修改了,即使它本不应该被修改,这样就会导致一些很难排查的问题。因此除非一个变量明确允许被修改,否则都应该给它加上 final 关键字。

但不是每个程序员都有这种意识的,导致 Java 程序员几乎都没有主动使用过 final。而 Kotlin 意识到了这点,在设计时就提供了 var 和val 两种不同的声明变量的关键字,让我们在声明变量的时候就进行思考:该变量是可变的还是不可变的。

那什么时候该用 val,什么时候又该用 var 呢?

使用原则:能用 val 就使用 val 来声明变量,保证变量不可被修改。当 val 确实不能满足需求时(变量需要被改变),再使用 var。

函数

定义和调用

前面我们看到的 main 函数也是一个函数,只不过是一个特殊的函数,它是程序的入口,程序运行时,就是从 main 函数开始执行的。

自定义函数的语法规则:

fun methodName(param1: Int, param2: Int): Int {
    return 0
}

解释一下:

  • fun 是定义函数的关键字

  • methodName 是函数名

  • 括号里声明的是该函数接收什么参数,每个参数的声明格式是“变量名: 参数类型”,参数之间使用逗号隔开。如果不需要接收任何参数,写一个空括号就行了

  • 括号后的那部分是可选的,用于声明函数的返回类型。如果函数没有实际的返回值,可以直接不写。也可以写上 : Unit,因为没有返回值的话,默认就是返回 Unit,Unit 类似于 Java 中的 void,只不过 Unit 不是关键字,而是一个单例对象。

  • 大括号{}内部的就是函数体,我们在这里编写函数的具体逻辑

现在我们就来定义一个函数:

fun sum(num1: Int, num2: Int): Int {
    val sum = num1 + num2 // 求和
    return sum // 返回结果
}

这里定义了一个函数名为 sum 的函数,接收两个整型参数,然后返回这两个参数之和。

怎么调用这个函数呢?

格式是 “函数名(传入的参数值)”,这里我们在 main 函数中调用:

fun main() {
    val a = 10
    val b = 20
    val sum = sum(a, b)
    println("a + b = $sum")
}

fun sum(num1: Int, num2: Int): Int {
    val sum = num1 + num2
    return sum
}

运行结果:

语法糖

Kotlin 为函数提供了一种简洁的语法形式,称为“单表达式函数”(Single-Expression functions)。当一个函数的函数体中只有一行代码(一个表达式)时,我们可以不用写函数体 {} 和 return 关键字,将这行代码放在函数定义的尾部,然后中间使用 = 等号进行连接即可。

比如我们刚刚的函数的逻辑,是可以简化为一行代码的:

fun sum(num1: Int, num2: Int): Int {
    return num1 + num2
}

使用单表达式函数语法糖

fun sum(num1: Int, num2: Int): Int = num1 + num2

然后由于 Kotlin 的自动类型推导机制,如果函数的返回类型可以通过赋值操作推导出来,我们可以不显式声明函数的返回值类型,代码进一步简化:

fun sum(num1: Int, num2: Int) = num1 + num2

程序的逻辑控制

程序的执行语句主要有三种:顺序语句、条件语句和循环语句。顺序语句就是代码一行一行、从上到下依次执行,我们之前写的代码都是顺序语句。

但有时我们的逻辑更为复杂,需要根据不同条件来执行不同代码,或者重复地执行某段代码时,就需要使用条件语句和循环语句。

if 条件语句

举个例子,我们要在函数中实现返回两个参数中的较大值的功能,可以这样写:

fun largeNumber(num1: Int, num2: Int): Int {
    var large = 0 // 较大值

    if (num1 > num2) {
        large = num1
    } else {
        large = num2
    }
    
    return large
}

这时你会发现有警告:'Assignment' can be lifted out of 'if',意思是“赋值操作可以从 if 语句中提取出来”。

我们点击自动修复后,发现代码变为了这样:

fun largeNumber(num1: Int, num2: Int): Int {
    var large = 0

    large = if (num1 > num2) {
        num1
    } else {
        num2
    }

    return large
}

这是因为 Kotlin 中的 if 语句是一个表达式,本身是有返回值的。它的返回值就是每一个条件分支中的最后一行代码的执行结果。所以代码可以简化为这种形式。

我们还可以将 large 变量的初始化和赋值合并简化为一条语句,并且由于这样修改后,large 没有被重新赋值了,所以可以使用 val 来声明:

fun largeNumber(num1: Int, num2: Int): Int {
    val large = if (num1 > num2) {
        num1
    } else {
        num2
    }

    return large
}

再仔细观察,你会发现 large 其实是一个多余的变量,我们可以直接返回 if 表达式的结果,代码进一步简化:

fun largeNumber(num1: Int, num2: Int): Int {
    return if (num1 > num2) {
        num1
    } else {
        num2
    }
}

回顾一下函数的语法糖(单表达式函数):当一个函数的函数体只有一行代码时,可以省略函数体部分,直接将这一 行代码使用等号连在函数定义的尾部。

代码能够进一步精简:

fun largeNumber(num1: Int, num2: Int): Int = if (num1 > num2) {
    num1
} else {
    num2
}

看到这里,你可能会觉得,if-else 语句不是有多行代码?怎么这也能使用单表达式函数的语法糖?

非也,因为 if 语句连同它的其他分支都只算作一个表达式,只有一个执行结果。

最后,上述代码还不是最精简的,当 if 的条件代码块中只有一行代码时,可以省略大括号,所以最终结果是:

fun largeNumber(num1: Int, num2: Int): Int = if (num1 > num2) num1 else num2 

when条件语句

when 语句类似于 Java 中的 switch,但在功能上远比它强大和灵活。

精确匹配

比如现在要编写一个函数,根据输入的星期名称,返回这天是工作日还是周末,我们先用 if 语句来完成:

fun getDayType(dayOfWeek: String): String = if (dayOfWeek == "Monday") {
    "Weekday"
} else if (dayOfWeek == "Tuesday") {
    "Weekday"
} else if (dayOfWeek == "Wednesday") {
    "Weekday"
} else if (dayOfWeek == "Thursday") {
    "Weekday"
} else if (dayOfWeek == "Friday") {
    "Weekday"
} else if (dayOfWeek == "Saturday") {
    "Weekend"
} else if (dayOfWeek == "Sunday") {
    "Weekend"
} else {
    "Unknown day"
}

虽然上述的代码能够实现我们想要的功能,但是有点冗余了,好多 if 和 else。

所以对于这种多判断条件的情况,我们可以考虑使用 when 语句,修改后的代码:

fun getDayType(dayOfWeek: String): String = when (dayOfWeek) {
    "Saturday", "Sunday" -> "Weekend"
    "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" -> "Weekday"
    else -> "Unknown day"
}

并且我们可以看到 when 语句和 if 语句一样,也是一个表达式有返回值,所以我们也可以使用单表达式函数的语法糖,就像上面那样。

when 语句允许传入一个任意类型的参数(称为“主体” subject),然后在 when 的内部定义每个分支条件的格式是:

匹配值 -> { 执行逻辑 }

当执行逻辑的代码只有一行(一个表达式)时,大括号可以省略。

类型匹配

除了上述的精确值匹配外,when 还可以进行类型匹配。比如编写一个判断变量是不是整型的函数:

fun checkInteger(num: Any) { // 参数类型为 Any,表示可以接收任何的类型
    when (num) {
        is Byte, is Short, is Int, is Long -> println("this is a integer")
        else -> println("this is not a integer")
    }
}

我们来在main函数中测试一下:

fun main() {
    val age: Short = 15
    checkInteger(age)

    val userId: Int = 1024
    checkInteger(userId)

    val timestamp: Long = System.currentTimeMillis()
    checkInteger(timestamp)

    val price: Float = 19.5f
    checkInteger(price)

    val message: String = "kotlin!"
    checkInteger(message)
}

运行结果:

image.png

不带参数的 when

when 语句还有一种不带参数的版本,比如下面这个函数会根据输入的百分制成绩,返回对应的成绩等级。

如果使用 if 语句使用:

fun getGradeLevelByScore(score: Float) = if (score > 90) {
    "A"
} else if (score > 80) {
    "B"
} else if (score > 70) {
    "C"
} else if (score > 60) {
    "D"
} else {
    "E"
}

如果使用不带参数的 when 语句来实现:

fun getGradeLevelByScore(score: Float): String = when {
    score >= 90 -> "A"
    score >= 80 -> "B"
    score >= 70 -> "C"
    score >= 60 -> "D"
    else -> "E"
}

它是将每一个分支的条件判断都写在了 when 的内部。

循环语句

Java中主要有两种循环语句:while 循环和 for 循环,Kotlin 中也提供了 while 循环和 for 循环。while 循环的用法和 Java 基本相同,就不再赘述了。

接下来,来看看 Kotlin 中的 for 循环。

Kotlin 中并没有 Java 中的 for-i 循环,而是对 for-each 循环进行了改进,形成了for-in循环。

区间

Kotlin 加入了区间的概念,我们先来了解一下,比如下面这就是一个区间:

val range: IntRange = 0..10

这行代码创建出了一个 0 到 10 的区间,并且是闭区间,在数学中的表示就是 [0,10]。

.. 是创建两端闭区间的关键字,在 .. 的两边指定区间的端点。

for-in 遍历区间

有了一个区间后,就可以通过 for-in 循环来遍历它:

fun main() {
    for (i in 0..10) {
        println(i)
    }
}

我们在 main() 函数中遍历了 [0,10] 这个区间中的每个元素,并将它们打印出来,运行结果:

很多时候,我们会使用单端区间,而不是双端区间。比如一个长度为 10 的数组,其下标的有效范围只是 0 到 9,我们不能也不需要访问下标为 10 的元素。

Kotlin 中使用 until 关键字就可以创建一个左闭右开的区间,比如:

val range: IntRange = 0 until 10

这行代码创建出了一个 0 到 10 的左闭右开区间,在数学中的表示就是 [0,10),并不包含 10。

循环遍历这个区间:

fun main() {
    for (i in 0 until 10) {
        println(i)
    }
}

再次运行一下:

区间跳跃

默认情况下,for-in 循环在遍历区间时,每次循环都会跳到下一个元素,也就是步长为 1。如果我们想要跳过一些元素,我们可以使用 step 关键字修改步长。像这样:

fun main() {
    for (i in 0 until 10 step 2) {
        println(i)
    }
}

运行结果:

image.png

降序区间

之前我们创建的都是升序的区间,如果想要一个降序区间,可以使用 downTo 关键字,像这样:

fun main() {
    for (i in 10 downTo 1) {
        println(i)
    }
}

这里我们创建了一个[10, 1]的降序闭区间。

运行结果:

image.png

for-in 循环除了可以对区间进行遍历之外,还可以用于遍历数组和集合等可迭代对象。

总结一下:Kotlin 中的 for-in 循环虽然没有传统的 for-i 循环灵活,但足以应对大部分场景了,如果使用 for-in 循环难以实现需求,可以换成 while 循环。