Day 2:Kotlin基础(一)

昨天搭好了 Android Studio,今天正式进入 Kotlin 语言基础。写 Android 应用本质上就是用 Kotlin 描述界面长什么样、数据怎么流转、用户操作怎么响应,这些最终都会落到变量、类型、函数和字符串拼接这四个基本功上。今天把它们逐个搞清,明天就能直接用 Kotlin 写出有逻辑的程序代码了。

变量:valvar

Kotlin 声明变量用 valvar。二者的区别不在于"有没有值",而在于赋值之后还能不能改。

val 声明的变量只能赋值一次,之后不能再指向其他对象。这跟 Java 里的 final 是同一个意思。var(variable 的缩写)声明的变量则可以反复赋值。举个例子:

kotlin 复制代码
fun main() {
    // val 声明后不可变------适合作为常量或一次性计算结果
    val appName = "HelloAndroid" // 应用名确定之后不需要改

    // var 声明后可以重新赋值------适合表示会变化的状态
    var clickCount = 0 // 每次点击都要累加
    clickCount = clickCount + 1
}

如果把 appName 声明成 var,代码也能跑,但意图就模糊了,读代码的人会预期这个值后面可能被修改,就会额外留意赋值的位置。反过来,如果 clickCountval,编译器直接报错 Val cannot be reassigned,逼你在声明时就明确它的角色。

Kotlin 支持类型推断,多数情况下不需要手写类型,编译器能从右边的值反推出来:

kotlin 复制代码
val version = 1     // 推断为 Int
val ratio = 0.618   // 推断为 Double

需要显式声明时,把类型写在变量名后面,用冒号分隔:

kotlin 复制代码
val userId: Int = 1
var message: String = "Hello"

类型必须显式声明的场景主要有三种,一是声明时不赋值,稍后才初始化,编译器没法猜类型,二是类型推断出的不是你想要的那个,比如你想要 Long 但编译器推成 Int,三是作为类属性或函数参数,Kotlin 要求写明类型。

还有一种常见情况是先声明再初始化:

kotlin 复制代码
val content: String // 只声明,不赋值
// ... 一些逻辑之后 ...
content = "result"  // 唯一一次赋值机会

这里需要注意,val 的"不可变"指的是变量指向的引用不可变,不是说对象内部不可变。比如 val 指向一个可变的列表,你仍然能往里添加删除元素,只是不能让这个变量再指向另一个列表。

数据类型

Kotlin 里的所有东西都是对象,数字、布尔值、字符,没有 Java 那种"基本类型"和"包装类型"的区分。

日常开发中最常用的数值类型就四种,Int 整数、Long 大整数、Double 高精度小数、Float 低精度小数。

类型 大小 字面量示例 说明
Int 32 位 val a = 42 默认整数类型
Long 64 位 val a = 42L 注意末尾的 L
Double 64 位 val a = 3.14 默认浮点类型
Float 32 位 val a = 3.14f 注意末尾的 f

数字字面量可以用下划线分隔,提高大数可读性,1_000_0001000000 完全等价。

Kotlin 有一个新手很容易踩到的坑,整数除法会丢掉小数部分,不会自动升级成浮点数。Java 开发者尤其要小心,这个行为两种语言一样,但在 Kotlin 里因为类型系统更严格,出错时定位更明显:

kotlin 复制代码
val half = 5 / 2      // 结果是 2,不是 2.5------两个 Int 相除,结果还是 Int
val correct = 5 / 2.0 // 结果是 2.5------其中一个操作数是 Double

第二个坑是 Kotlin 不做隐式类型转换。不能直接把 Int 赋给 Long 变量,也不能把 Float 传给参数类型为 Double 的函数:

kotlin 复制代码
val count: Int = 42
val bigCount: Long = count     // 编译错误:type mismatch
val bigCount: Long = count.toLong() // 正确做法------显式调用转换函数

对于每种数字类型都提供了 toByte()toShort()toInt()toLong()toFloat()toDouble() 六个转换函数。设计上之所以不做隐式转换,是为了避免精度丢失时悄无声息,凡是转换,必须在代码里写出来,让 review 的人能看见。

Boolean 类型只有 truefalse 两个值,用在条件判断中。Char 表示单个字符,用单引号括起来,val letter = 'A'String 是字符串,用双引号,不可变,所有对字符串的操作都返回新字符串,不会修改原值:

kotlin 复制代码
val original = "Kotlin"
val upper = original.uppercase() // 返回 "KOTLIN",original 依然是 "Kotlin"

函数

声明函数用 fun 关键字,后面跟函数名、括号内的参数列表、冒号后的返回值类型,然后是花括号内的函数体:

kotlin 复制代码
fun add(a: Int, b: Int): Int {
    return a + b
}

Kotlin 中函数可以定义在文件顶层,不需要先写一个 class 把它包起来,这一点和 Java 不同。对于 Android 开发来说,你可能会把一个页面里的辅助逻辑,比如格式化时间、校验手机号等写成顶层函数,放在对应的包下面,外部直接按包名 import 就能用,不需要套一层 Util 类。

如果函数体只有一条表达式,可以把花括号和 return 都省掉,换成等号连接:

kotlin 复制代码
fun add(a: Int, b: Int): Int = a + b

这种单表达式函数连返回值类型都可以省略,编译器自动推断:

kotlin 复制代码
fun add(a: Int, b: Int) = a + b  // 推断返回 Int

函数没有有意义返回值时,返回类型是 Unit,相当于 Java 的 void,可以省略不写:

kotlin 复制代码
fun showResult(value: Int) {
    println("计算结果:$value")  // 不需要 return 语句
}

参数可以有默认值,这在实际项目中非常有用。比如一个网络请求函数,默认超时可以设成 5 秒,特殊场景下才传入自定义值:

kotlin 复制代码
fun divide(a: Double, b: Double = 1.0): Double = a / b

divide(10.0)      // 10.0,b 使用默认值 1.0
divide(10.0, 2.0) // 5.0,覆盖默认值

调用函数时可以按参数名传值,不按顺序,这就是命名参数。当一个函数有多个带默认值的参数,你只想改其中一个时,命名参数就省去了逐个补位的麻烦:

kotlin 复制代码
fun createUser(name: String, age: Int = 0, city: String = "未知") {
    println("$name, $age, $city")
}

createUser("Tom", city = "上海") // age 用默认值 0,city 覆盖为 上海

传入函数的参数在函数体内部是只读的------相当于隐式 val,不能对参数重新赋值。这个约束在函数体较长、逻辑复杂时能防止无意中改掉了上游传进来的值,排错时少一个变量污染源。

字符串模板

字符串模板是 Kotlin 最顺手的语法之一,在双引号包裹的字符串里,$ 加变量名就能直接把变量的值嵌入到字符串中,不用再写 + 拼接:

kotlin 复制代码
val name = "Tom"
val score = 95
println("学生: ${name}, 成绩: ${score}") // 学生: Tom, 成绩: 95

花括号里可以放任意 Kotlin 表达式,函数调用、算术运算甚至条件判断都可以。如果不加花括号,$ 只会把紧跟在它后面的那一个标识符当作变量名,所以 $score 等价于 ${score},但 $score分 会被解析成变量名叫 score分 而不是 score

kotlin 复制代码
val base = 100
println("两倍是 ${base * 2}")      // 两倍是 200
println("大写:${name.uppercase()}") // 大写:TOM

println("得分$score分")  // 编译错误,找不到变量 score分
println("得分${score}分") // 正确写法

需要输出一个实际的美金符号 $ 时,写 ${'$'},内层是一对单引号括起来的 Char:

kotlin 复制代码
println("价格:${'$'}99")  // 价格:$99

多行字符串用三引号 """ 包裹,里面也可以嵌入模板表达式,换行会原样保留,不需要 \n 转义:

kotlin 复制代码
val html = """
    <div class="card">
        <h1>${name}</h1>
        <p>分数:${score}</p>
    </div>
""".trimIndent()

trimIndent() 会统一去除每行前面的公共缩进,让输出看起来紧凑整齐。

字符串模板把变量、表达式和文本自然地写在一起,代码和最终输出的对应关系一目了然。用 + 拼接好几个变量时很容易漏掉空格或搞混引号闭合,模板写法则直接杜绝了这类低级错误。

练习

理解了这四个概念之后,用两个练习来巩固。

练习一:写计算器函数。 定义加减乘除四个函数,每个接收两个 Double 参数,返回 Double 结果。在 main 里分别调用并打印输出。

参考实现:

kotlin 复制代码
fun add(a: Double, b: Double) = a + b
fun subtract(a: Double, b: Double) = a - b
fun multiply(a: Double, b: Double) = a * b

// 除数为 0 时直接返回 0,避免运行时异常
fun divide(a: Double, b: Double) = if (b != 0.0) a / b else 0.0

fun main() {
    val x = 15.0
    val y = 4.0

    println("$x + $y = ${add(x, y)}")
    println("$x - $y = ${subtract(x, y)}")
    println("$x * $y = ${multiply(x, y)}")
    println("$x / $y = ${divide(x, y)}")
}

divide 里加了对除数为零的保护------这在实际项目中是必要的,因为 Double / 0.0 会返回 Infinity 而不会直接抛异常,代码能继续执行但后续逻辑可能因此出错。更稳妥的做法是判断之后抛一个有意义的异常,这里为了让示例保持简单,直接返回零。

练习二:写 BMI 计算器。 接收身高(单位米)和体重(单位千克)两个参数,计算 BMI 并打印判定结果。BMI 公式是体重除以身高的平方。判定标准参考中国标准:低于 18.5 偏瘦,18.5~23.9 正常,24.0~27.9 偏重,28.0 及以上肥胖。

kotlin 复制代码
fun calculateBMI(weight: Double, height: Double): Double {
    return weight / (height * height)
}

fun getBMICategory(bmi: Double): String {
    return when {
        bmi < 18.5 -> "偏瘦"
        bmi < 24.0 -> "正常"
        bmi < 28.0 -> "偏重"
        else -> "肥胖"
    }
}

fun main() {
    val weight = 70.0  // 千克
    val height = 1.75  // 米

    // 保留一位小数,避免输出一长串数字
    val bmi = String.format("%.1f", calculateBMI(weight, height))
    val category = getBMICategory(bmi.toDouble())

    println("身高:${height}m,体重:${weight}kg")
    println("BMI:${bmi}(${category})")
}

这段代码把计算和分类拆成了两个函数,职责单一,各自独立可测。String.format("%.1f", ...) 把 BMI 值截断到一位小数,打印结果干净------70.0 / (1.75 * 1.75) 精确结果是 22.857...,格式化后输出 22.9,刚好落在正常范围内。when 表达式的条件是从上到下依次匹配的,把区间的下界从小到大排好,就不需要写 bmi >= 18.5 && bmi < 24.0 这种双重条件了。

这两个练习覆盖了今天所有内容:用 val 声明不变的入参和计算结果,用 Double 做小数运算并处理类型转换,用 fun 定义带返回值和默认参数的函数,用字符串模板把计算结果整洁地拼进输出信息。把代码敲一遍、改几个参数跑一跑,这四个概念就真正内化了。