Day 2:Kotlin基础(一)
昨天搭好了 Android Studio,今天正式进入 Kotlin 语言基础。写 Android 应用本质上就是用 Kotlin 描述界面长什么样、数据怎么流转、用户操作怎么响应,这些最终都会落到变量、类型、函数和字符串拼接这四个基本功上。今天把它们逐个搞清,明天就能直接用 Kotlin 写出有逻辑的程序代码了。
变量:val和var
Kotlin 声明变量用val或var。二者的区别不在于"有没有值",而在于赋值之后还能不能改。
val声明的变量只能赋值一次,之后不能再指向其他对象。这跟 Java 里的final是同一个意思。var(variable 的缩写)声明的变量则可以反复赋值。举个例子:
funmain(){// val 声明后不可变——适合作为常量或一次性计算结果valappName="HelloAndroid"// 应用名确定之后不需要改// var 声明后可以重新赋值——适合表示会变化的状态varclickCount=0// 每次点击都要累加clickCount=clickCount+1}如果把appName声明成var,代码也能跑,但意图就模糊了,读代码的人会预期这个值后面可能被修改,就会额外留意赋值的位置。反过来,如果clickCount用val,编译器直接报错Val cannot be reassigned,逼你在声明时就明确它的角色。
Kotlin 支持类型推断,多数情况下不需要手写类型,编译器能从右边的值反推出来:
valversion=1// 推断为 Intvalratio=0.618// 推断为 Double需要显式声明时,把类型写在变量名后面,用冒号分隔:
valuserId:Int=1varmessage:String="Hello"类型必须显式声明的场景主要有三种,一是声明时不赋值,稍后才初始化,编译器没法猜类型,二是类型推断出的不是你想要的那个,比如你想要Long但编译器推成Int,三是作为类属性或函数参数,Kotlin 要求写明类型。
还有一种常见情况是先声明再初始化:
valcontent: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_000与1000000完全等价。
Kotlin 有一个新手很容易踩到的坑,整数除法会丢掉小数部分,不会自动升级成浮点数。Java 开发者尤其要小心,这个行为两种语言一样,但在 Kotlin 里因为类型系统更严格,出错时定位更明显:
valhalf=5/2// 结果是 2,不是 2.5——两个 Int 相除,结果还是 Intvalcorrect=5/2.0// 结果是 2.5——其中一个操作数是 Double第二个坑是 Kotlin 不做隐式类型转换。不能直接把Int赋给Long变量,也不能把Float传给参数类型为Double的函数:
valcount:Int=42valbigCount:Long=count// 编译错误:type mismatchvalbigCount:Long=count.toLong()// 正确做法——显式调用转换函数对于每种数字类型都提供了toByte()、toShort()、toInt()、toLong()、toFloat()、toDouble()六个转换函数。设计上之所以不做隐式转换,是为了避免精度丢失时悄无声息,凡是转换,必须在代码里写出来,让 review 的人能看见。
Boolean类型只有true和false两个值,用在条件判断中。Char表示单个字符,用单引号括起来,val letter = 'A'。String是字符串,用双引号,不可变,所有对字符串的操作都返回新字符串,不会修改原值:
valoriginal="Kotlin"valupper=original.uppercase()// 返回 "KOTLIN",original 依然是 "Kotlin"函数
声明函数用fun关键字,后面跟函数名、括号内的参数列表、冒号后的返回值类型,然后是花括号内的函数体:
funadd(a:Int,b:Int):Int{returna+b}Kotlin 中函数可以定义在文件顶层,不需要先写一个 class 把它包起来,这一点和 Java 不同。对于 Android 开发来说,你可能会把一个页面里的辅助逻辑,比如格式化时间、校验手机号等写成顶层函数,放在对应的包下面,外部直接按包名 import 就能用,不需要套一层 Util 类。
如果函数体只有一条表达式,可以把花括号和return都省掉,换成等号连接:
funadd(a:Int,b:Int):Int=a+b这种单表达式函数连返回值类型都可以省略,编译器自动推断:
funadd(a:Int,b:Int)=a+b// 推断返回 Int函数没有有意义返回值时,返回类型是Unit,相当于 Java 的void,可以省略不写:
funshowResult(value:Int){println("计算结果:$value")// 不需要 return 语句}参数可以有默认值,这在实际项目中非常有用。比如一个网络请求函数,默认超时可以设成 5 秒,特殊场景下才传入自定义值:
fundivide(a:Double,b:Double=1.0):Double=a/bdivide(10.0)// 10.0,b 使用默认值 1.0divide(10.0,2.0)// 5.0,覆盖默认值调用函数时可以按参数名传值,不按顺序,这就是命名参数。当一个函数有多个带默认值的参数,你只想改其中一个时,命名参数就省去了逐个补位的麻烦:
funcreateUser(name:String,age:Int=0,city:String="未知"){println("$name,$age,$city")}createUser("Tom",city="上海")// age 用默认值 0,city 覆盖为 上海传入函数的参数在函数体内部是只读的——相当于隐式val,不能对参数重新赋值。这个约束在函数体较长、逻辑复杂时能防止无意中改掉了上游传进来的值,排错时少一个变量污染源。
字符串模板
字符串模板是 Kotlin 最顺手的语法之一,在双引号包裹的字符串里,$加变量名就能直接把变量的值嵌入到字符串中,不用再写+拼接:
valname="Tom"valscore=95println("学生:${name}, 成绩:${score}")// 学生: Tom, 成绩: 95花括号里可以放任意 Kotlin 表达式,函数调用、算术运算甚至条件判断都可以。如果不加花括号,$只会把紧跟在它后面的那一个标识符当作变量名,所以$score等价于${score},但$score分会被解析成变量名叫score分而不是score:
valbase=100println("两倍是${base*2}")// 两倍是 200println("大写:${name.uppercase()}")// 大写:TOMprintln("得分$score分")// 编译错误,找不到变量 score分println("得分${score}分")// 正确写法需要输出一个实际的美金符号$时,写${'$'},内层是一对单引号括起来的 Char:
println("价格:${'$'}99")// 价格:$99多行字符串用三引号"""包裹,里面也可以嵌入模板表达式,换行会原样保留,不需要\n转义:
valhtml=""" <div class="card"> <h1>${name}</h1> <p>分数:${score}</p> </div> """.trimIndent()trimIndent()会统一去除每行前面的公共缩进,让输出看起来紧凑整齐。
字符串模板把变量、表达式和文本自然地写在一起,代码和最终输出的对应关系一目了然。用+拼接好几个变量时很容易漏掉空格或搞混引号闭合,模板写法则直接杜绝了这类低级错误。
练习
理解了这四个概念之后,用两个练习来巩固。
练习一:写计算器函数。定义加减乘除四个函数,每个接收两个Double参数,返回Double结果。在main里分别调用并打印输出。
参考实现:
funadd(a:Double,b:Double)=a+bfunsubtract(a:Double,b:Double)=a-bfunmultiply(a:Double,b:Double)=a*b// 除数为 0 时直接返回 0,避免运行时异常fundivide(a:Double,b:Double)=if(b!=0.0)a/belse0.0funmain(){valx=15.0valy=4.0println("$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 及以上肥胖。
funcalculateBMI(weight:Double,height:Double):Double{returnweight/(height*height)}fungetBMICategory(bmi:Double):String{returnwhen{bmi<18.5->"偏瘦"bmi<24.0->"正常"bmi<28.0->"偏重"else->"肥胖"}}funmain(){valweight=70.0// 千克valheight=1.75// 米// 保留一位小数,避免输出一长串数字valbmi=String.format("%.1f",calculateBMI(weight,height))valcategory=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定义带返回值和默认参数的函数,用字符串模板把计算结果整洁地拼进输出信息。把代码敲一遍、改几个参数跑一跑,这四个概念就真正内化了。
