变量#
常用数据类型#
数据类型 | 可包含的数据类型 | 字面量值示例 |
---|---|---|
String | 文本 | “Add contact” |
Int | 整数 | 1293490、-59281 |
Double | 小数 | 501.0292、-31723.99999 |
Float | 小数,数字末尾带有 f 或 F | -1630.209f、1.2940278F |
Boolean | true 或 false,这俩属于关键字 | true、false |
更多数据类型和范围等信息,参阅 Basic types - Kotlin官网
声明和使用#
在声明变量时,需要立即为其赋值。数据类型可以不写,会自动确定。用 val
声明不能改变值的变量,用 var
声明可改值的变量。
val 不变的变量: 数据类型 = 值
var 变量名: 数据类型 = 值
var x = 6
以下代码作为实例,演示了变量声明、两种变量调用方式、自增、注释、转义字符等:
val count: Int = 10
println("你有 $count 条信息未读")
// 可以使用自增
count++
count--
/**
* 小驼峰命名
* 数据类型不写出也行,会自动确定
*/
val unreadCount = 5
val readCount = 100
println("你一共有 ${unreadCount + readCount} 条信息")
// 转义
println("Say \"hello\"")
// 拼接
val nextMeeting = "Next meeting is:"
val date = "January 1"
val reminder = nextMeeting + date
println(reminder)
// 输出效果 Next meeting is:January 1
函数#
没有返回值的函数不用写第一行的返回值类型。
形参的值不能改,它相当于用 val
开头声明的。
fun 函数名(abc: Int, 参数2: 参数2类型): 返回值类型 {
abc = 6 // 这么写会报错,因为形参的值不能改
函数体
return 返回值 //没返回值的函数不用写
}
函数签名#
形参(以英文逗号分隔)有时称为形参列表。
函数名称及其输入(形参)统称为「函数签名」。函数签名包含返回值类型前面的所有内容,如以下代码段所示。
fun birthdayGreeting(name: String, age: Int)
默认实参#
fun birthdayGreeting(name: String = "Rover", age: Int): String {
return "Happy Birthday, $name! You are now $age years old!"
}
具名实参#
调用函数时添加了形参名称,该名称就称为具名实参。
println(birthdayGreeting(name = "Rex", age = 2))
条件控制#
比较运算符:==
、<
、>
、<=
、>=
、!=
if#
if (abc == "Red") {
// ***
} else if (i >= 0) {
// ***
} else {
// ***
}
when#
如果需要考虑的分支数量超过两个,应首选使用 when 语句。
val trafficLightColor = "Black"
when (trafficLightColor) {
"Red" -> println("Stop")
"Yellow" -> println("Slow")
"Green" -> println("Go")
// when 支持 else 语句
else -> println("Invalid traffic-light color")
}
逗号分隔处理多个条件#
val x = 3
when (x) {
2, 3, 5, 7 -> println("质数")
else -> println("不是")
}
使用 in 关键字处理一系列条件#
val x = 4
when (x) {
2, 3, 5, 7 -> println("质数")
in 1..10 -> println("不是")
else -> println("不在 10 以内")
}
使用 is 关键字检查数据类型#
以下内容会输出 整数,但不在 10 以内
。
val x: Any = 20
when (x) {
2, 3, 5, 7 -> println("质数")
in 1..10 -> println("不是")
is Int -> println("整数,但不在 10 以内")
else -> println("不是整数")
}
使用 if/else 和 when 作为表达式#
val aaaaa =
if (x == 1) "壹"
else if (x == 5) "贰"
else "其它"
val bbbbb = when(x) {
1 -> "壹"
2, 3 -> "贰、叁"
else -> "其它"
}
null 相关#
在 Kotlin 中,有可为 null 类型与不可为 null 类型之分:
- 可为 null 类型是指可以存储
null
值的变量。 - 不可为 null 类型是指不能存储
null
值的变量。
var x: String = "abc123"
x = null // 报错
如需声明可为 null 的变量,需要在相应类型的末尾添加 ?
运算符。否则,Kotlin 编译器会推断该变量属于不可为 null 类型。例如,String?
类型可以存储字符串或 null
,而 String
类型只能存储字符串。
var x: String? = "abc123"
println(x) // 输出 abc123
x = null
println(x) // 输出 null
使用 ?. 安全调用运算符#
在此示例中,系统不允许直接引用 x
变量的 length
属性,因为该变量有可能是 null
,因此代码在编译时失败。可以使用 ?.
安全调用运算符访问可为 null 变量的方法或属性。
var x: String? = "abc123"
println(x.length) // 报错
println(x?.length) // 不报错
使用 !! 非 null 断言运算符#
使用 !!
非 null 断言运算符,即表示您断言变量的值不是 null
,无论变量是否为该值都是如此。
与 ?.
安全调用运算符不同,当可为 null 的变量确实为 null
时,使用 !!
非 null 断言运算符可能会导致系统抛出 NullPointerException
错误。
var x: String? = null
println(x!!.length) // 报错,NullPointerException
除非确定变量不为 null
,否则不建议使用 !!
非 null 断言运算符。只有在变量始终为不可为 null 或设置了适当的异常处理时,才应使用该断言运算符。
使用 if/else 执行 null 检查#
如果有多行代码使用可为 null 的变量,那么将 null
检查与 if
条件搭配使用会更方便。相比之下,?.
安全调用运算符更适用于对可为 null 变量的单次引用。
var x: String? = null
if (x != null) {
println("字符串长度为${x.length}")
} else {
println("null")
}
val length = if(x != null) {
x.length
} else {
0
}
println("字符串长度为$length")
使用 ?: Elvis 运算符#
?:
Elvis 运算符可以与 ?.
安全调用运算符搭配使用。如果搭配使用 ?:
Elvis 运算符,您便可以在 ?.
安全调用运算符返回 null
时添加默认值。这与 if/else
表达式类似,但更为常用。
val x: String? = "abc123"
val length = x?.length ?: 0
println("字符串长度为$length")
类和对象#
class Box {
// 类属性
val name = "a black box"
var status = "close"
// 类方法
fun openBox() {
println("打开盒子")
}
fun test() {
openBox() // 调用类方法
println("test")
}
}
fun main() {
val box = Box() // 创建类的实例
println("name = ${box.name}") // 属性
box.openBox() // 调用类方法
}
属性中的 getter 和 setter 函数#
定义可变属性的完整语法是以变量定义开头,后跟可选的 get()
和 set()
函数。
如果没有为属性定义 getter 和 setter 函数,Kotlin 编译器会在内部创建这些函数。
val
类型的变量为只读变量,因此不含 set()
函数。
var myAge = 18
get() = field
set(value) {
field = value
}
Kotlin 属性使用后备字段在内存中存储值。从根本上来讲,后备字段是在属性内部定义的类变量。后备字段的作用域限定为相应属性,这意味着您只能通过 get()
或 set()
属性函数访问该字段。
如果想读取 get()
函数中的属性值或更新 set()
函数中的值,您需要使用对应属性的后备字段。该字段是由 Kotlin 编译器自动生成,并通过 field
标识符来引用。
set()
函数中,如果您尝试将 value
形参赋给 myAge
属性本身,代码就会进入无限循环,因为 Kotlin 运行时会尝试更新 myAge
属性的值,而不断触发对 setter 函数的调用。构造函数#
Kotlin 编译器会自动生成默认构造函数。默认构造函数不含形参。定义默认构造函数如下:
class Box constructor() {
...
}
代码可简化。如果构造函数中没有任何注解或可见性修饰符,可以移除 constructor
关键字。如果构造函数中没有任何形参,您还可以移除圆括号。如下:
class Box {
...
}
形参化构造函数#
class Box(val name: String) {
...
}
辅助构造函数#
Kotlin 中的构造函数主要有两类:
主要构造函数:一个类只能有一个主要构造函数(在类标头中定义)。主要构造函数可以是默认构造函数,也可以是形参化构造函数。主要构造函数没有函数体,这意味着其中不能包含任何代码。
// 使用主要构造函数初始化类标头中的属性 // 传递给构造函数的实参会赋给属性 class 类名 constructor(参数列表) { // 主要构造函数没有函数体,这里写的是类的主体,比如属性和方法什么的 }
辅助构造函数:一个类可以有多个辅助构造函数。您可以定义包含形参或不含形参的辅助构造函数。辅助构造函数可以初始化类,具有包含初始化逻辑的主体。如果类有主要构造函数,则每个辅助构造函数都需要初始化该主要构造函数。
辅助构造函数包含在类的主体中,其语法包括以下三个部分:
- 辅助构造函数声明:定义以
constructor
关键字开头,后跟圆括号,根据需要填形参。 - 主要构造函数初始化:初始化以冒号开头,后面依次跟
this
关键字和一对圆括号,根据需要填形参。 - 辅助构造函数主体:在主要构造函数的初始化后跟一对花括号,写辅助构造函数的主体。
语法如下图所示:
class Box(val name: String) {
var status = "close"
// 根据传入的 statusCode,给 status 赋对应的字符串
constructor(name: String, statusCode: Int) : this(name) {
status = when (statusCode) {
0 -> "close"
1 -> "open"
else -> "unknown"
}
}
...
}
继承#
在 Kotlin 中,所有类默认都是最终类,无法扩展。因此必须定义类之间的关系。
// 前面添加 open 关键字
open class Box(val name: String) {
...
}
// boxName 开头没写 val 或 var
// 也就是说,没有指定可变的还是不可变的
// 因此它不是类属性,只是 constructor 形参
// 只能用于传给父类
class SmallBox(boxName: String) :
Box(name = boxName) {
var size = 0
set(value) {
if (value in 0..100) {
field = value
}
}
fun check() {
println("size = $size")
}
}
类之间的关系#
IS-A 关系:是继承关系。比如,每个鸭子「都是」鸟类。而且,如果对象 phone 是继承自类 Device,则也是类 Device 的实例。
class SmallBox : Box() {
}
不要只为了实现代码的可重用性而使用继承。在做出决定之前,请检查这两个类彼此是否相关。
HAS-A 关系:不是继承关系。比如,住宅中包含智能设备,即住宅「拥有」智能设备。两个类之间的 HAS-A 关系也称为「组合」。在 HAS-A 关系中,对象可以拥有其他类的实例,且实际上不必是该类本身的实例。
// 住宅 HAS-A 智能电视设备
class TinyBox(val sb: SmallBox) {
// 实例化后,实例内包含一个 sb 变量
fun checkSize() {
sb.check()
}
}
// 能同时包含多个类的对象
class AAA(
val bbb: BBB,
val ccc: CCC
) {
...
}
重载,替换子类中的父类方法#
class Box(val name: String) {
var status = "close"
// 在父类方法前添加 open 关键字
open fun openBox() {
println("open")
}
}
class SmallBox(val boxName: String) :
Box(name = boxName) {
// 在子类中,定义同名方法,添加 override 关键字
override fun openBox() {
status = "open"
println("open in child class")
}
}
使用 super 关键字在子类中重用父类代码#
open class Box(val name: String) {
var status = "close"
open fun openBox() {
status = "open"
}
}
class SmallBox(boxName: String) :
Box(name = boxName) {
override fun openBox() {
super.openBox()
println("$name 已打开")
}
}
重载,替换子类中的父类属性#
与方法重载类似。
open class Box(val name: String) {
open val type = "unknown"
...
}
class SmallBox(boxName: String) :
SmartDevice(name = boxName) {
override val type = "Smart Box"
...
}
可见性修饰符#
作用:
- 在类中隐藏自己的属性和方法,防止在类外未经授权的访问。
- 在软件包中隐藏类和接口,防止在软件包外未经授权的访问。
Kotlin 提供了以下四种可见性修饰符,默认为 public
:
public
:可在任何位置访问声明。用于想在类外部使用的属性和方法。private
:可在相同类或源文件中访问声明。某些属性和方法可能仅在类的内部使用,而且您不一定想让其他类使用。protected
:可让子类访问声明。internal
:可在相同模块中访问声明。与private
类似,但可以从类的外部访问内部属性和方法,只要是在相同模块中进行访问即可。
修饰符 | 相同类中访问 | 子类中访问 | 相同模块中访问 | 模块之外访问 |
---|---|---|---|---|
private | ✔ | ❌ | ❌ | ❌ |
protected | ✔ | ✔ | ❌ | ❌ |
internal | ✔ | ✔ | ✔ | ❌ |
public | ✔ | ✔ | ✔ | ✔ |
模块 是源文件和构建设置的集合,可让您将项目划分为独立的功能单元。您的项目可以包含一个或多个模块。您可以独立构建、测试和调试每个模块。
软件包就像是用来对相关类进行分组的目录或文件夹,模块则是用来为应用的源代码、资源文件和应用级设置提供容器。一个模块可以包含多个软件包。
属性
方法
您不会在 set()
函数中执行任何操作或检查,只需将 value
形参赋给 field
变量即可。如您之前所学,这与属性 setter 的默认实现类似。在本例中,您可以省略 set()
函数的圆括号和主体:
class SmartDevice {
var deviceStatus = "online"
protected set // 函数体可省略
// protected set(value) {
// field = value
// }
}
构造函数
类
定义属性委托#
以变量声明开头,后面依次跟 by
关键字以及用于为属性处理 getter 和 setter 函数的委托对象。
var 变量名 by 委托对象
若要实现您可以委托实现的目标类,您必须熟悉接口。接口是实现它的类必须遵循的协议,侧重于操作的「内容」,而不是操作的「方式」。简而言之,接口可帮助您实现抽象。
例如,在建造房子之前,您会告知建筑师自己想要什么。这可能包括卧室、儿童房、起居室、厨房和几间浴室。简而言之,在您指定「需求」后,建筑师会指定「满足需求的方式」。
为 var
类型创建委托类,需要实现 ReadWriteProperty
接口。为 val
类型创建委托类,需要实现 ReadOnlyProperty
接口。
例如,为 var
类型创建委托:
// 导入 ReadWriteProperty 和 KProperty 接口
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
// 创建 RangeRegulator 类,用于实现 ReadWriteProperty<Any?, Int> 接口
// 不必担心尖括号以及其中的内容。它们属于通用类型,会在以后进行了解。
class RangeRegulator(
initialValue: Int,
private val minValue: Int,
private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {
// 定义 fieldData 属性,并使用 initialValue 形参对其进行初始化
var fieldData = initialValue // 此属性会充当变量的后备字段
// 替换 getValue() 和 setValue() 方法
// 用于充当属性的 getter 和 setter 函数
override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
return fieldData
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
// 检查要赋予的 value 形参是否处于 minValue..maxValue 范围内
if (value in minValue..maxValue) {
fieldData = value
}
}
}
class Phone {
// 使用以上编写的接口
private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)
}
使用函数类型和 lambda 表达式#
将函数存储在变量中#
将函数作为值引用,使用函数引用运算符 ::
fun main() {
val t = ::test
t() // 相当于调用 test()
}
fun test() {
println("666")
}
使用 lambda 表达式重新定义函数#
fun main() {
val t = test
t()
}
val test = {
println("666")
}
将函数用作数据类型#
如果要指定函数参数的类型或返回值类型,则需要了解用于表达函数类型的语法。函数类型由一组圆括号组成,其中包含可选的参数列表、->
符号和返回值类型。语法如下图所示:
将函数用作返回值类型#
val trick = {
println("No treats!")
}
val treat = {
println("Have a treat!")
}
fun trickOrTreat(isTrick: Boolean): () -> Unit {
if (isTrick) {
return trick
} else {
return treat
}
}
fun main() {
val treatFunction = trickOrTreat(false)
val trickFunction = trickOrTreat(true)
treatFunction()
trickFunction()
}
将一个函数作为参数传递到另一个函数#
声明函数类型时,参数不会带有标签。只需指定各个参数的数据类型即可。语法如下:
为接受参数的函数编写 lambda 表达式时,系统会按参数出现的先后顺序为参数命名。参数名称列在左大括号后面,各名称之间以英文逗号隔开。->
将参数名称与函数正文隔开。语法如下:
// 在 isTrick 参数后面,添加类型为 (Int) -> String 的 extraTreat 参数
fun trickOrTreat(isTrick: Boolean, extraTreat: (Int) -> String): () -> Unit {
if (isTrick) {
return trick
} else {
println(extraTreat(5))
return treat
}
}
fun main() {
// coins() 函数为 Int 参数指定名称 quantity 并返回 String
val coins: (Int) -> String = { quantity ->
"$quantity quarters"
// lambda 表达式不能用 return 关键字
// 函数中最后一个表达式的结果将成为返回值
}
val cupcake: (Int) -> String = {
"Have a cupcake!"
}
val treatFunction = trickOrTreat(false, coins)
val trickFunction = trickOrTreat(true, cupcake)
treatFunction()
trickFunction()
}
val trick = {
println("No treats!")
}
val treat = {
println("Have a treat!")
}
可为 null 的函数类型#
与其他数据类型一样,函数类型可声明为可为 null
。在这些情况下,变量可以包含函数,也可以为 null
。
例如,如果想让 () -> String
类型可为 null
,则将其声明为 (() -> String)?
类型。语法如下:
// 将 extraTreat 参数设置为可为 null
// 这样就不必在每次调用 trickOrTreat() 函数时都提供 extraTreat() 函数
fun trickOrTreat(isTrick: Boolean, extraTreat: ((Int) -> String)?): () -> Unit {
if (isTrick) {
return trick
} else {
if (extraTreat != null) {
println(extraTreat(5))
}
return treat
}
}
fun main() {
val coins: (Int) -> String = { quantity ->
"$quantity quarters"
}
val treatFunction = trickOrTreat(false, coins)
val trickFunction = trickOrTreat(true, null)
treatFunction()
trickFunction()
}
使用简写语法编写 lambda 表达式#
省略参数名称#
如果函数只有一个参数,且未提供名称,Kotlin 会隐式为其分配 it
名称,因此您可以省略参数名称和 ->
。
val coins: (Int) -> String = { quantity ->
"$quantity quarters"
}
简化后:
val coins: (Int) -> String = {
"$it quarters"
}
将 lambda 表达式直接传入函数#
fun main() {
val coins: (Int) -> String = { quantity ->
"$quantity quarters"
}
val treatFunction = trickOrTreat(false, coins)
val trickFunction = trickOrTreat(true, null)
treatFunction()
trickFunction()
}
简化后:
fun main() {
// 还可以将 lambda 表达式压缩为一行代码
val treatFunction = trickOrTreat(false, { "$it quarters" })
val trickFunction = trickOrTreat(true, null)
treatFunction()
trickFunction()
}
使用尾随 lambda 语法#
当函数类型是函数的最后一个参数时,可以使用另一个简写选项来编写 lambda。在这种情况下,可以将 lambda 表达式放在右圆括号后面以调用函数。
这会使代码的可读性更强,因为它将 lambda 表达式与其他参数隔开,但没有改变代码的作用。
val treatFunction = trickOrTreat(false, { "$it quarters" })
简化后:
val treatFunction = trickOrTreat(false) { "$it quarters" }
使用 repeat() 函数#
如果一个函数会返回或接受另一个函数作为实参,该函数就称为高阶函数。上文的 trickOrTreat()
函数是一个高阶函数示例,因为它接受 ((Int) -> String)?
类型的函数作为参数,并返回 () -> Unit
类型的函数。
repeat()
函数是使用函数表达 for
循环的简洁方式。