Kotlin 备忘录
基本语法
核心概念
- 包和导入 (Packages and Imports):将代码组织成命名空间。
main
函数:Kotlin 应用程序的入口点。可以接受一个字符串数组作为参数。- 打印到标准输出 (Printing to Standard Output):
print()
和println()
用于显示信息。println()
会在输出后添加换行符。 - 从标准输入读取 (Reading from Standard Input):
readln()
从控制台读取一行文本。 - 函数 (Functions):使用
fun
关键字定义。可以有表达式体和推断返回类型。Unit
返回类型用于不返回有意义值的函数。 - 变量 (Variables):
val
:不可变(只读)变量。var
:可变变量。- 类型推断:Kotlin 通常可以自动确定类型。
- 类和实例 (Classes and Instances):使用
class
关键字定义。属性可以在声明或主体中。继承使用冒号:
。类默认是final
的;使用open
允许继承。 - 注释 (Comments):单行 (
//
) 和多行 (/* ... */
)。多行注释可以嵌套。 - 字符串模板 (String Templates):使用
$variable
或${expression}
将变量和表达式直接嵌入到字符串中。 - 条件表达式 (Conditional Expressions):
if
语句。if
也可以用作表达式。 - 循环 (Loops):
for
循环(迭代集合或范围)和while
循环。 when
表达式:更强大的switch
语句。- 范围 (Ranges):使用
in
运算符检查值是否在范围内。使用for
迭代范围。 - 集合 (Collections):使用
for
迭代。使用in
检查成员资格。使用 lambda 表达式进行过滤、映射和其他操作。 - 可空值和空值检查 (Nullable Values and Null Checks):使用
?
将类型标记为可空。在使用可空值之前检查null
。 - 类型检查和自动转换 (Type Checks and Automatic Casts):使用
is
运算符检查变量的类型。Kotlin 在if
块中自动将变量转换为检查的类型。
常见语法
创建 DTOs (POJOs/POCOs)
data class Customer(val name: String, val email: String)
data class
自动生成getters
(和var
的setters
)、equals()
、hashCode()
、toString()
、copy()
以及componentN()
函数(用于解构)。极大地简化了数据类的创建。
函数参数的默认值
fun foo(a: Int = 0, b: String = "") { ... }
- 允许在函数定义中为参数指定默认值。调用函数时,可以省略具有默认值的参数。
过滤列表
val positives = list.filter { x -> x > 0 }
val positives = list.filter { it > 0 } // 更简洁
- 使用
filter
函数和一个 lambda 表达式来创建一个只包含满足特定条件的元素的新列表。it
是 lambda 表达式中单个参数的隐式名称。
检查集合中元素的存在性
if ("john@example.com" in emailsList) { ... }
if ("jane@example.com" !in emailsList) { ... }
- 使用
in
和!in
运算符可以轻松地检查集合中是否存在某个元素。
字符串插值
println("Name $name")
- 使用
$
符号将变量直接嵌入到字符串中,无需使用字符串连接。
安全读取标准输入
val wrongInt = readln().toIntOrNull() // 如果输入无法转换为整数,则返回 null
val correctInt = readln().toIntOrNull() // 如果输入可以转换为整数,则返回整数
toIntOrNull()
尝试将字符串转换为整数,如果转换失败,则返回null
,避免了NumberFormatException
。
实例检查
when (x) {
is Foo -> ...
is Bar -> ...
else -> ...
}
- 使用
is
运算符检查对象的类型。在when
表达式中,如果检查成功,则会自动将对象转换为该类型。
只读列表
val list = listOf("a", "b", "c")
listOf()
创建一个不可变的列表。
只读映射
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
mapOf()
创建一个不可变的映射。
访问映射条目
println(map["key"])
map["key"] = value // 对于可变映射
- 使用方括号
[]
访问映射中的值。
遍历映射或一对列表
for ((k, v) in map) {
println("$k -> $v")
}
- 使用解构声明来遍历映射或
Pair
列表。k
和v
可以是任何方便的名称。
遍历范围
for (i in 1..100) { ... } // 闭区间:包括 100
for (i in 1..<100) { ... } // 开区间:不包括 100
for (x in 2..10 step 2) { ... } // 步长为 2
for (x in 10 downTo 1) { ... } // 倒序
(1..10).forEach { ... } // 使用 forEach
- 使用
..
创建闭区间,使用..<
创建开区间。step
指定步长,downTo
创建倒序范围。
惰性属性
val p: String by lazy { // 仅在第一次访问时计算该值
// 计算字符串
}
lazy
委托确保属性只在第一次访问时初始化。这可以提高性能,特别是对于计算成本高的属性。
扩展函数
fun String.spaceToCamelCase() { ... }
"Convert this to camelcase".spaceToCamelCase()
- 允许向现有类添加新的函数,而无需继承或修改原始类。
创建单例
object Resource {
val name = "Name"
}
object
声明创建一个单例对象。Kotlin 自动处理单例的创建和管理。
使用内联值类来实现类型安全的值
@JvmInline
value class EmployeeId(private val id: String)
@JvmInline
value class CustomerId(private val id: String)
value class
(需要@JvmInline
在 JVM 上) 创建一个包装现有类型的类,提供编译时类型安全。防止错误地将EmployeeId
传递给需要CustomerId
的函数。
实例化一个抽象类
abstract class MyAbstractClass {
abstract fun doSomething()
abstract fun sleep()
}
fun main() {
val myObject = object : MyAbstractClass() {
override fun doSomething() {
// ...
}
override fun sleep() { // ...
}
}
myObject.doSomething()
}
- 使用匿名对象实例化抽象类,需要实现所有抽象方法。
如果非空简写
val files = File("Test").listFiles()
println(files?.size) // 如果 files 不为空,则打印 size
- 使用安全调用运算符
?.
如果files
为null
,则表达式的结果为null
,不会抛出NullPointerException
。
如果非空则执行,否则执行其他操作的简写
val files = File("Test").listFiles()
println(files?.size ?: "empty") // 如果 files 为 null,则打印 "empty"
val filesSize = files?.size ?: run {
val someSize = getSomeSize()
someSize * 2
}
println(filesSize)
- 使用 Elvis 运算符
?:
提供一个默认值,如果左侧表达式为null
,则返回该默认值。run
允许执行一个代码块来计算默认值。
如果为空则执行语句
val values = ...
val email = values["email"] ?: throw IllegalStateException("Email is missing!")
- 使用 Elvis 运算符
?:
抛出一个异常,如果values["email"]
为null
。
获取可能为空集合的第一个项目
val emails = ... // 可能为空
val mainEmail = emails.firstOrNull() ?: ""
firstOrNull()
返回集合的第一个元素,如果集合为空,则返回null
。Elvis 运算符提供一个默认值,如果firstOrNull()
返回null
。
如果不为 null 则执行
val value = ...
value?.let {
... // 如果 value 不为空,则执行此代码块
}
let
函数只在值不为null
时执行。it
是let
块中该值的隐式名称。
如果非空则映射可空值
val value = ...
val mapped = value?.let { transformValue(it) } ?: defaultValue
// 如果 value 或 transform 结果为空,则返回 defaultValue。
- 结合了安全调用运算符
?.
、let
函数和 Elvis 运算符?:
,以简洁地转换可空值,并在值或转换结果为空时提供默认值。
在 when
语句中返回值
fun transform(color: String): Int {
return when (color) {
"Red" -> 0
"Green" -> 1
"Blue" -> 2
else -> throw IllegalArgumentException("Invalid color param value")
}
}
when
表达式可以返回值,这使得代码更简洁。
try-catch
表达式
fun test() {
val result = try {
count()
} catch (e: ArithmeticException) {
throw IllegalStateException(e)
}
// 使用 result
}
try-catch
块可以返回值,允许您将异常处理逻辑嵌入到表达式中。
if
表达式
val y = if (x == 1) {
"one"
} else if (x == 2) {
"two"
} else {
"other"
}
if
语句可以返回值,允许您根据条件选择不同的值。
Builder 风格的使用返回 Unit 的方法
fun arrayOfMinusOnes(size: Int): IntArray {
return IntArray(size).apply { fill(-1) }
}
apply
函数允许您在对象上调用多个方法,并返回该对象本身。这使得可以以 Builder 风格配置对象。
单表达式函数
fun theAnswer() = 42
// 等价于
fun theAnswer(): Int {
return 42
}
fun transform(color: String): Int = when (color) {
"Red" -> 0
"Green" -> 1
"Blue" -> 2
else -> throw IllegalArgumentException("Invalid color param value")
}
- 如果函数只包含一个表达式,则可以省略花括号和
return
关键字。
在对象实例上调用多个方法 (with
)
class Turtle {
fun penDown()
fun penUp()
fun turn(degrees: Double)
fun forward(pixels: Double)
}
val myTurtle = Turtle()
with(myTurtle) { // 画一个 100 像素的正方形
penDown()
for (i in 1..4) {
forward(100.0)
turn(90.0)
}
penUp()
}
with
函数允许您在单个对象上调用多个方法,而无需重复对象名称。
配置对象的属性 (apply
)
val myRectangle = Rectangle().apply {
length = 4
breadth = 5
color = 0xFAFAFA
}
apply
函数允许您配置对象的属性,并返回该对象本身。这对于配置构造函数中不存在的属性很有用。
Java 7 的 try-with-resources
val stream = Files.newInputStream(Paths.get("/some/file.txt"))
stream.buffered().reader().use { reader ->
println(reader.readText())
}
use
函数确保资源(例如流)在使用后被正确关闭,即使发生异常。
需要泛型类型信息的通用函数
inline fun <reified T: Any> Gson.fromJson(json: JsonElement): T = this.fromJson(json, T::class.java)
reified
关键字允许您在运行时访问泛型类型信息。这对于需要知道泛型类型的函数很有用。
交换两个变量
var a = 1
var b = 2
a = b.also { b = a }
- 使用
also
函数在赋值之前执行一个操作。在这种情况下,also
函数将a
的值赋给b
,然后将b
的值赋给a
。
数据类型
类型列表
- 数字 (Numbers):
Byte
(8位)Short
(16位)Int
(32位)Long
(64位)Float
(32位)Double
(64位)- 无符号类型:
UByte
,UShort
,UInt
,ULong
- 布尔值 (Booleans):
Boolean
(true
/false
) - 字符 (Characters):
Char
(Unicode) - 字符串 (Strings):
String
(不可变) - 数组 (Arrays):
Array<T>
(可变) 特定类型数组:IntArray
,ByteArray
等
特殊类型
Any
:所有非空类型的超类 (类似 Java 的Object
)Nothing
: "永远不会返回" 的类型 (例如, 抛出异常的函数)Unit
: "没有有意义的返回值" 的类型 (类似 Java 的void
)
类型检查/转换
- 类型检查:
is
- 安全转换:
as?
(失败返回null
) - 不安全转换:
as
(可能抛出异常)
关键点
- Kotlin 没有原始类型 (primitive types)。
- 类型推断强大, 通常无需显式声明类型。
示例
val age: Int = 30
val name = "Alice" // 类型推断为 String
val pi = 3.14 // 类型推断为 Double
控制语句
if
表达式
if (条件) 结果 else 结果
(必须有else
,除非结果是Unit
)- 可返回值,类似三元运算符。
val max = if (a > b) a else b // 返回 a 或 b 中较大的值
when
语句/表达式
- 替代
switch
。 when (变量) { 值 -> 结果 ... else -> 结果 }
- 作为表达式必须覆盖所有情况 (或有
else
)。 - 可检查类型 (
is
),范围 (in
)。
when (x) {
1 -> println("x is 1")
in 2..5 -> println("x is between 2 and 5")
is String -> println("x is a String")
else -> println("x is something else")
}
for
循环
for (item in 集合) { ... }
- 迭代集合元素。
for (i in 范围) { ... }
迭代数字范围。
val numbers = listOf(1, 2, 3)
for (number in numbers) {
println(number)
}
for (i in 1..5) { // 1 到 5 (包含 5)
println(i)
}
while
循环
while (条件) { ... }
先判断后执行。do { ... } while (条件)
先执行后判断 (至少执行一次)。
var count = 0
while (count < 5) {
println("Count: $count")
count++
}
var input: String
do {
print("Enter a number (or 'q' to quit): ")
input = readLine() ?: "" // 安全处理空输入
} while (input != "q")
控制循环
break
:跳出循环。continue
:跳过本次循环剩余部分。
for (i in 1..10) {
if (i == 3) continue // 跳过 i=3 的迭代
if (i == 7) break // 循环在 i=7 时终止
println(i)
}
跳转表达式
return
- 默认从最近的封闭函数或匿名函数返回。
break
- 终止最近的封闭循环。
continue
- 继续最近的封闭循环的下一次迭代。
这些表达式的类型是 Nothing
。
标签 (Labels)
Kotlin 中可以使用标签标记表达式,形式为 identifier@
。标签可以用于更精确地控制 break
和 continue
的行为。
loop@ for (i in 1..100) {
for (j in 1..100) {
if (...) break@loop // 退出标记为 loop 的循环
}
}
break@label
:跳到标签label
标记的循环之后的执行点。continue@label
:跳到标签label
标记的循环的下一次迭代。
返回到标签 (Return to Labels)
在嵌套函数(函数字面量、局部函数、对象表达式)中,可以使用限定的 return
语句从外部函数返回。主要用于从 Lambda 表达式中返回。
从 Lambda 表达式返回
显式标签
在这个例子中, `return@lit` 从标记为 `lit@` 的 Lambda 表达式返回。fun foo() { listOf(1, 2, 3, 4, 5).forEach lit@{ if (it == 3) return@lit // 局部返回到 forEach 循环的调用者 print(it) } print(" done with explicit label") }
隐式标签:更简洁,标签名称与 Lambda 表达式传递到的函数名相同。
在这个例子中, `return@forEach` 从传递给 `forEach` 的 Lambda 表达式返回。fun foo() { listOf(1, 2, 3, 4, 5).forEach { if (it == 3) return@forEach // 局部返回到 forEach 循环的调用者 print(it) } print(" done with implicit label") }
匿名函数:可以使用匿名函数代替 Lambda 表达式。
return
语句将从匿名函数本身返回。使用匿名函数的效果与 `continue` 类似.fun foo() { listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) { if (value == 3) return // 局部返回到匿名函数的调用者 print(value) }) print(" done with anonymous function") }
模拟 break
行为
可以使用嵌套的 Lambda 表达式和非局部返回来模拟 break
的行为。
fun foo() {
run loop@{
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return@loop // 从传递给 run 的 lambda 表达式非局部返回
print(it)
}
}
print(" done with nested loop")
}
注意
当返回一个值时,解析器优先选择带标签的 return
语句,例如 return@a 1
被解析为“在标签 @a
处返回 1”,而不是“返回一个标签为 @a
的表达式 (1)
”。
类和继承
默认超类 (Default Superclass)
- Kotlin 中所有类都有一个共同的超类
Any
。 - 如果一个类没有声明超类型,则
Any
是默认的超类。
class Example // 隐式继承自 Any
Any
有三个方法:equals()
、hashCode()
和toString()
。这些方法对于所有 Kotlin 类都是定义的。
开放类 (Open Class)
- 默认情况下,Kotlin 类是
final
的,不能被继承。 - 要使一个类可继承,用
open
关键字标记它。
open class Base // 类可以被继承
显式超类型 (Explicit Supertype)
- 要在类头中声明显式超类型,将类型放在冒号之后。
open class Base(p: Int)
class Derived(p: Int) : Base(p)
- 主构造函数 (Primary Constructor):如果派生类具有主构造函数,则必须根据其参数在该主构造函数中初始化基类。
- 二级构造函数 (Secondary Constructor):如果派生类没有主构造函数,则每个二级构造函数必须使用
super
关键字初始化基类型,或者必须委托给另一个执行此操作的构造函数。不同的二级构造函数可以调用基类型的不同构造函数。
class MyView : View {
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
重写方法 (Overriding Methods)
- Kotlin 需要对可重写成员和重写使用显式修饰符。
open class Shape {
open fun draw() { /*...*/ }
fun fill() { /*...*/ }
}
class Circle() : Shape() {
override fun draw() { /*...*/ }
}
override
修饰符对于Circle.draw()
是必需的。如果缺少它,编译器会报错。- 如果一个函数没有
open
修饰符(如Shape.fill()
),则不允许在子类中声明具有相同签名的方法,无论是否带有override
。 - 当添加到
final
类的成员时,open
修饰符不起作用。 - 用
override
标记的成员本身是open
的,因此可以在子类中重写。 - 要禁止重新重写,使用
final
。
open class Rectangle() : Shape() {
final override fun draw() { /*...*/ }
}
重写属性 (Overriding Properties)
- 重写机制以与方法相同的方式作用于属性。
- 在超类上声明然后在派生类上重新声明的属性必须以
override
开头,并且它们必须具有兼容的类型。 - 每个声明的属性都可以被具有初始化器的属性或具有
get
方法的属性重写。
open class Shape {
open val vertexCount: Int = 0
}
class Rectangle : Shape() {
override val vertexCount = 4
}
- 可以使用
var
属性重写val
属性,但反之则不行。这是因为val
属性本质上声明了一个get
方法,并且将其作为var
重写还会在派生类中声明一个set
方法。 - 可以在主构造函数中使用
override
关键字作为属性声明的一部分。
interface Shape {
val vertexCount: Int
}
class Rectangle(override val vertexCount: Int = 4) : Shape // 始终有 4 个顶点
class Polygon : Shape {
override var vertexCount: Int = 0 // 稍后可以设置为任何数字
}
派生类初始化顺序 (Derived Class Initialization Order)
- 在构造派生类的新实例期间,基类初始化是作为第一步完成的(仅在计算基类构造函数的参数之前),这意味着它发生在派生类的初始化逻辑运行之前。
open class Base(val name: String) {
init { println("Initializing a base class") }
open val size: Int =
name.length.also { println("Initializing size in the base class: $it") }
}
class Derived(
name: String,
val lastName: String,
) : Base(name.replaceFirstChar { it.uppercase() }.also { println("Argument for the base class: $it") }) {
init { println("Initializing a derived class") }
override val size: Int =
(super.size + lastName.length).also { println("Initializing size in the derived class: $it") }
}
- 当执行基类构造函数时,尚未初始化在派生类中声明或重写的属性。在基类初始化逻辑中使用这些属性(直接或间接通过另一个重写的开放成员实现)可能会导致不正确的行为或运行时故障。
- 在设计基类时,应避免在构造函数、属性初始化器或
init
块中使用open
成员。
调用超类实现 (Calling the Superclass Implementation)
- 派生类中的代码可以使用
super
关键字调用其超类函数和属性访问器实现。
open class Rectangle {
open fun draw() { println("Drawing a rectangle") }
val borderColor: String get() = "black"
}
class FilledRectangle : Rectangle() {
override fun draw() {
super.draw()
println("Filling the rectangle")
}
val fillColor: String get() = super.borderColor
}
- 在内部类中,访问外部类的超类使用用外部类名限定的
super
关键字:super@Outer
。
class FilledRectangle: Rectangle() {
override fun draw() {
val filler = Filler()
filler.drawAndFill()
}
inner class Filler {
fun fill() { println("Filling") }
fun drawAndFill() {
super@FilledRectangle.draw() // 调用 Rectangle.draw()
fill()
println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // 使用 Rectangle 的 borderColor 的 get() 实现
}
}
}
重写规则 (Overriding Rules)
- 在 Kotlin 中,实现继承受以下规则约束:如果一个类从其直接超类继承同一成员的多个实现,则它必须重写此成员并提供自己的实现(可能使用继承的实现之一)。
- 要表示从中获取继承实现的超类型,请使用
super
并以尖括号括起来的超类型名称限定,例如super<Base>
。
open class Rectangle {
open fun draw() { /* ... */ }
}
interface Polygon {
fun draw() { /* ... */ } // 接口成员默认是 'open' 的
}
class Square() : Rectangle(), Polygon {
// 编译器要求重写 draw():
override fun draw() {
super<Rectangle>.draw() // 调用 Rectangle.draw()
super<Polygon>.draw() // 调用 Polygon.draw()
}
}
- 可以从
Rectangle
和Polygon
继承,但它们都有自己的draw()
实现,因此需要在Square
中重写draw()
并为其提供单独的实现以消除歧义。
泛型:in
, out
, where
泛型类
- 声明:使用尖括号
<>
声明类型参数。
class Box<T>(t: T) {
var value = t
}
- 实例化:提供类型参数,或者让编译器推断。
val box: Box<Int> = Box<Int>(1) // 显式指定类型
val box = Box(1) // 编译器推断类型为 Box<Int>
变型 (Variance)
- 不变性 (Invariance):默认情况下,Kotlin 中的泛型类型是不变的。这意味着
List<String>
不是List<Object>
的子类型。 - 协变 (Covariance):使用
out
关键字声明协变类型参数。协变类型参数只能出现在out
位置(只能作为返回值,不能作为参数)。如果类C
的类型参数T
声明为out
,C<Base>
可以安全地成为C<Derived>
的超类型,可以理解为C
是T
的生产者,不是T
的消费者。 - 逆变 (Contravariance):使用
in
关键字声明逆变类型参数。逆变类型参数只能出现在in
位置(只能作为参数,不能作为返回值)。可以调用List<? super String>
上将String
作为参数的方法,如果调用返回T
的方法,则会返回一个Object
,而不是String
。 - 声明点变型 (Declaration-site Variance):在类型参数声明处指定变型(使用
in
和out
关键字)。 - 使用点变型 (Use-site Variance):在类型使用处指定变型(使用类型投影)。
- PECS 原则 (Producer Extends, Consumer Super):
- 如果对象是生产者(只从中读取数据),则使用
out
(对应 Java 中的extends
)。 - 如果对象是消费者(只向其中写入数据),则使用
in
(对应 Java 中的super
)。
- 如果对象是生产者(只从中读取数据),则使用
out
- 只返回
T
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // This is OK, since T is an out-parameter
// ...
}
in
- 只获取
T
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
// Thus, you can assign x to a variable of type Comparable<Double>
val y: Comparable<Double> = x // OK!
}
类型投影 (Type Projections)
- 概念:限制泛型类型的用法,以确保类型安全。
out
投影:禁止写入,只允许读取(类似于Array<? extends Object>
)。in
投影:禁止读取,只允许写入(类似于Array<? super String>
)。
fun copy(from: Array<out Any>, to: Array<Any>) { ... } // 禁止写入 from
fun fill(dest: Array<in String>, value: String) { ... } // 禁止读取 dest
星号投影 (Star-projections)
- 概念:表示对类型参数一无所知,但仍要以安全的方式使用它。
Foo<out T : TUpper>
等价于Foo<out TUpper>
(可以安全地读取TUpper
类型的值)。Foo<in T>
等价于Foo<in Nothing>
(无法安全地写入)。Foo<T : TUpper>
等价于读取时Foo<out TUpper>
,写入时Foo<in Nothing>
。- 类似于 Java 的原始类型,但更安全。
泛型函数
- 声明:将类型参数放在函数名之前。
fun <T> singletonList(item: T): List<T> { ... }
fun <T> T.basicToString(): String { ... } // 扩展函数
- 调用:显式指定类型参数,或者让编译器推断。
val l = singletonList<Int>(1) // 显式指定类型
val l = singletonList(1) // 编译器推断类型
泛型约束 (Generic Constraints)
- 概念:限制可以替代类型参数的类型范围。
- 上界 (Upper Bounds):使用冒号
:
指定上界,类似于 Java 的extends
。
fun <T : Comparable<T>> sort(list: List<T>) { ... } // T 必须是 Comparable<T> 的子类型
where
子句:如果需要多个上界,则使用where
子句。
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>
where T : CharSequence,
T : Comparable<T> {
return list.filter { it > threshold }.map { it.toString() }
} // T 必须同时实现 CharSequence 和 Comparable
明确的非空类型 (Definitely Non-nullable Types)
- 使用
T & Any
声明类型参数为明确的非空类型。 - 通常用于与带有
@NotNull
注解的 Java 方法互操作。
interface ArcadeGame<T1> : Game<T1> {
override fun save(x: T1): T1
override fun load(x: T1 & Any): T1 & Any // T1 是明确的非空类型
}
类型擦除 (Type Erasure)
- Kotlin 在编译时执行泛型类型安全检查。
- 在运行时,泛型类型的实例不包含关于其实际类型参数的任何信息。
Foo<Bar>
和Foo<Baz?>
的实例在运行时都被擦除为Foo<*>
。
泛型类型检查和转换
- 由于类型擦除,无法在运行时检查泛型类型实例是否使用特定类型参数创建(例如,无法使用
ints is List<Int>
检查)。 - 可以针对星号投影类型检查实例(例如,
something is List<*>
)。 - 在已静态检查类型参数的情况下,可以执行不涉及类型参数的非泛型部分的类型检查或转换(例如,
list as ArrayList
)。 - 泛型函数调用的类型参数仅在编译时检查。在函数体内部,类型参数不能用于类型检查,并且到类型参数的类型转换是未经检查的。唯一的例外是具有具体化类型参数的内联函数,它们具有在每个调用站点内联的实际类型参数。
未经检查的转换 (Unchecked Casts)
- 无法在运行时检查到具有具体类型参数的泛型类型的类型转换(例如,
foo as List<String>
)。 - 这些未经检查的转换可以在类型安全由高级程序逻辑暗示但编译器无法直接推断时使用。
- 可以通过用
@Suppress("UNCHECKED_CAST")
注释语句或声明来禁止显示未经检查的转换警告。
下划线运算符 (Underscore Operator) 用于类型参数
- 可以使用下划线运算符
_
用于类型参数. 当其他类型被显式指定时,使用它来自动推断参数的类型.
数据类 (Data Classes)
核心概念
- Kotlin 中的数据类主要用于保存数据。对于每个数据类,编译器会自动生成额外的成员函数,允许将实例打印为可读的输出、比较实例、复制实例等。数据类用
data
标记。
data class User(val name: String, val age: Int)
自动生成的成员
- 对于在主构造函数中声明的所有属性,编译器会自动派生以下成员:
equals()/hashCode()
对。toString()
格式为 "User(name=John, age=42)"。- 与属性声明顺序相对应的
componentN()
函数。 copy()
函数。
数据类的要求
- 为确保生成代码的一致性和有意义的行为,数据类必须满足以下要求:
- 主构造函数必须至少有一个参数。
- 所有主构造函数参数必须标记为
val
或var
。 - 数据类不能是
abstract
、open
、sealed
或inner
。
成员继承规则
- 如果在数据类主体中显式实现了
equals()
、hashCode()
或toString()
,或者在超类中显式实现了最终实现,则不会生成这些函数,并且使用现有实现。 - 如果超类型具有
componentN()
函数(它们是open
并返回兼容类型),则会为数据类生成相应的函数并覆盖超类型的函数。如果由于签名不兼容或由于它们是final
而无法覆盖超类型的函数,则会报告错误。 - 不允许为
componentN()
和copy()
函数提供显式实现。 - 数据类可以扩展其他类。
JVM 参数为空的构造函数
- 在 JVM 上,如果生成的类需要具有无参数构造函数,则必须为属性指定默认值:
data class User(val name: String = "", val age: Int = 0)
在类主体中声明的属性
- 编译器仅使用在主构造函数内部定义的属性来自动生成函数。要从生成的实现中排除属性,请在类主体中声明它:
data class Person(val name: String) {
var age: Int = 0
}
- 在该示例中,默认情况下只有
name
属性在toString()
、equals()
、hashCode()
和copy()
实现中使用,并且只有一个组件函数component1()
。age
属性是在类主体中声明的,因此被排除在外。因此,具有相同name
但不同age
值的两个Person
对象被认为是相等的,因为equals()
仅评估来自主构造函数的属性:
val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20
println("person1 == person2: ${person1 == person2}")
// person1 == person2: true
println("person1 with age ${person1.age}: ${person1}")
// person1 with age 10: Person(name=John)
println("person2 with age ${person2.age}: ${person2}")
// person2 with age 20: Person(name=John)
复制 (Copying)
- 使用
copy()
函数复制对象,允许你在保持其余属性不变的情况下更改某些属性。上面User
类的这个函数的实现如下:
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
数据类和解构声明 (Destructuring Declarations)
- 为数据类生成的组件函数使它们可以在解构声明中使用:
val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age")
// Jane, 35 years of age
标准数据类
- 标准库提供了
Pair
和Triple
类。但是,在大多数情况下,命名的数据类是更好的设计选择,因为它们通过为属性提供有意义的名称来使代码更易于阅读。
枚举类 (Enum Classes)
核心概念
- Kotlin 的枚举类用于表示一组有限的不同值,每个值都是枚举类的一个实例。
基本用法
- 声明:使用
enum class
关键字声明枚举类。
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
- 常量:枚举常量是类的对象,以逗号分隔。
初始化
- 构造函数:枚举常量可以像类一样进行初始化,使用构造函数传递参数。
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
匿名类
- 定义:枚举常量可以声明自己的匿名类,包含各自的方法,以及覆写基类的方法。
enum class ProtocolState {
WAITING {
override fun signal() = TALKING
},
TALKING {
override fun signal() = WAITING
};
abstract fun signal(): ProtocolState
}
- 成员定义:如果枚举类定义了任何成员,则使用分号将常量定义与成员定义分隔开。
实现接口
- 实现接口:枚举类可以实现接口(但不能继承类),可以为所有条目提供接口成员的公共实现,也可以在每个条目的匿名类中提供单独的实现。
enum class IntArithmetics : BinaryOperator<Int>, IntBinaryOperator {
PLUS {
override fun apply(t: Int, u: Int): Int = t + u
},
TIMES {
override fun apply(t: Int, u: Int): Int = t * u
};
override fun applyAsInt(t: Int, u: Int) = apply(t, u)
}
- 默认情况下,所有枚举类都实现
Comparable
接口。枚举类中的常量以自然顺序定义,可以进行比较。
枚举常量的使用
Kotlin 中的枚举类具有用于列出定义的枚举常量和按名称获取枚举常量的合成属性和方法。
EnumClass.valueOf(value: String): EnumClass
:根据名称返回枚举常量。如果指定的名称与类中定义的任何枚举常量都不匹配,则valueOf()
方法会抛出IllegalArgumentException
。EnumClass.entries: EnumEntries<EnumClass>
:Kotlin 1.9 引入,返回一个专门的List<EnumClass>
类型的枚举条目。Kotlin 2.0 将替代enumValues<T>()
,它返回一个数组.enum class RGB { RED, GREEN, BLUE } fun main() { for (color in RGB.entries) println(color.toString()) // prints RED, GREEN, BLUE println("The first color is: ${RGB.valueOf("RED")}") // prints "The first color is: RED" }
每个枚举常量还有属性:
name
:获取枚举常量的名称。ordinal
:获取枚举常量在枚举类声明中的位置(从 0 开始)。println(RGB.RED.name) // prints RED println(RGB.RED.ordinal) // prints 0
泛型方式访问枚举常量
enumEntries<T>()
:Kotlin 2.0 引入,返回给定枚举类型T
的所有枚举条目的列表,推荐使用,性能好。
enum class RGB { RED, GREEN, BLUE }
inline fun <reified T : Enum<T>> printAllValues() {
println(enumEntries<T>().joinToString { it.name })
}
printAllValues<RGB>()
// RED, GREEN, BLUE
内联值类
核心概念
- 内联值类是一种特殊的类,它包装了一个单一的值,但在运行时不会创建实际的类实例,从而减少了内存占用和提高了性能。
声明和使用
- 声明:使用
value class
关键字声明内联值类。在 JVM 上,需要加上@JvmInline
注解。
@JvmInline
value class Password(val value: String)
- 特点:
- 只能有一个属性,且必须在构造函数中初始化。
- 性能优化:在运行时,内联值类的实例直接使用底层值,不会创建新的对象。
- 类型安全:虽然底层是
String
,但Password
类型不能直接当作String
使用,反之亦然。这与typealias
不同,typealias
只是给类型起个别名,可以互相替换。
使用场景
- 当你需要创建一个新的类型,但又不想牺牲性能时。
- 当你需要对现有的类型进行更强的约束时。
示例
@JvmInline
value class Email(val value: String)
fun sendEmail(email: String) { // 接收普通字符串
}
fun sendEmail(email: Email) { // 接收 Email 类型
}
val myEmail: Email = Email("test@example.com")
val myString: String = "test@example.com"
sendEmail(myEmail) // 调用接收 Email 类型的函数
sendEmail(myString) // 调用接收普通字符串类型的函数
object
关键字
单例对象 (Singleton Objects)
- 定义:
object
关键字用于声明一个单例类,即只存在一个实例的类。 - 特点:
- 单例对象在第一次被访问时创建,并且在整个应用程序生命周期内都存在。
- 单例对象不能有构造函数。
- 可以继承类或实现接口。
object MySingleton {
val name = "Singleton"
fun doSomething() {
println("Doing something in the singleton")
}
}
fun main() {
println(MySingleton.name) // 访问单例对象的属性
MySingleton.doSomething() // 调用单例对象的方法
}
伴生对象 (Companion Objects)
- 定义:
companion object
关键字用于在类中声明一个伴生对象。 - 特点:
- 伴生对象与类关联,但不是类的实例。
- 伴生对象可以访问类的私有成员。
- 伴生对象通常用于创建类的工厂方法或存储类的静态成员。
class MyClass {
companion object {
val constant = "Class Constant"
fun create(): MyClass {
return MyClass()
}
}
}
fun main() {
println(MyClass.constant) // 访问伴生对象的属性
val instance = MyClass.create() // 调用伴生对象的方法
}
匿名对象 (Anonymous Objects)
- 定义:
object : ClassName()
用于创建匿名对象,即没有名称的对象。 - 特点:
- 匿名对象通常用于实现接口或继承类,并提供一次性的实现。
- 可以访问外部作用域的变量。
- 立即执行和初始化。
interface MyInterface {
fun doSomething()
}
fun main() {
val myObject = object : MyInterface {
override fun doSomething() {
println("Doing something in the anonymous object")
}
}
myObject.doSomething()
}
对象声明 vs. 对象表达式
特性 | Object 声明 (Singleton) | Object 表达式 (匿名对象) |
---|---|---|
用途 | 创建单例 | 一次性使用 |
初始化时机 | 首次访问时懒加载 | 立即初始化 |
访问方式 | 通过名称直接访问 | 表达式结果 (变量引用) |
命名 | 必须有名称 | 无需命名 |
包 (Packages) 和导入 (Imports)
核心概念
- 包 (Package):用于组织 Kotlin 代码,类似于文件夹。每个源文件可以声明属于哪个包。
- 导入 (Import):允许在代码中使用其他包中的类、函数等,而无需写完整的包名。
关键信息
包声明 (Package Declaration)
- 在源文件的开头使用
package
关键字声明包名:package org.example
- 源文件中的所有内容(类、函数等)都属于该包。
- 例如:
org.example.printMessage
和org.example.Message
- 如果没有指定包,则属于默认包(没有名称)。
- 在源文件的开头使用
默认导入 (Default Imports)
- Kotlin 默认导入一些常用的包,无需手动导入。
- 包括
kotlin.*
、kotlin.annotation.*
、kotlin.collections.*
、等等... - 根据目标平台 (JVM, JS),还会额外导入一些包,例如
java.lang.*
(JVM) 或kotlin.js.*
(JS)。
自定义导入 (Custom Imports)
- 可以使用
import
关键字导入单个名称或整个包。 - 导入单个名称:
import org.example.Message
(可以直接使用Message
) - 导入整个包:
import org.example.*
(可以使用org.example
中的所有内容)
- 可以使用
别名 (Alias)
- 如果导入的名称发生冲突,可以使用
as
关键字重命名导入的实体,解决冲突。 示例:
import org.example.Message import org.test.Message as TestMessage // 现在可以用 TestMessage 代替 org.test.Message
- 如果导入的名称发生冲突,可以使用
可以导入的内容
- 顶层函数和属性
- 对象声明中的函数和属性
- 枚举常量
顶层声明的可见性
- 如果顶层声明(例如顶层函数、顶层变量)被标记为
private
,则它只能在该声明的文件中访问。
- 如果顶层声明(例如顶层函数、顶层变量)被标记为
接口 (Interfaces)
核心概念
- Kotlin 的接口与 Java 8+ 的接口类似,可以包含抽象方法的声明和方法的实现。关键区别在于接口不能存储状态。它们可以有属性,但这些属性必须是抽象的或提供访问器实现。
定义接口
- 使用
interface
关键字定义接口:
interface MyInterface {
fun bar() // 抽象方法,必须在实现类中实现
fun foo() { // 带有默认实现的方法,实现类可以选择重写
// 可选的方法体
}
}
实现接口
- 一个类或对象可以实现一个或多个接口:
class Child : MyInterface {
override fun bar() { // 必须实现 MyInterface 中的抽象方法 bar()
// 方法体
}
}
- 使用冒号 (
:
) 表示继承或实现关系。 - 必须使用
override
关键字重写接口中的方法。
接口中的属性
- 接口可以声明属性。
- 接口中的属性不能有幕后字段 (backing fields)。
- 接口中的属性可以是抽象的,或者提供访问器 (accessor) 实现:
interface MyInterface {
val prop: Int // 抽象属性,必须在实现类中实现
val propertyWithImplementation: String
get() = "foo" // 带有默认 getter 实现的属性
fun foo() {
print(prop)
}
}
class Child : MyInterface {
override val prop: Int = 29 // 必须实现 MyInterface 中的抽象属性 prop
}
接口继承
- 接口可以继承其他接口。
- 继承的接口可以提供成员的实现,也可以声明新的函数和属性。
- 实现类只需要定义缺失的实现:
interface Named {
val name: String
}
interface Person : Named {
val firstName: String
val lastName: String
override val name: String get() = "$firstName $lastName" // 提供了 name 属性的默认实现
}
data class Employee(
override val firstName: String,
override val lastName: String,
val position: Position
) : Person // 不需要实现 name 属性,因为 Person 接口已经提供了默认实现
解决覆盖冲突
- 当一个类实现多个接口时,可能会继承同一个方法的多个实现。此时,必须显式地指定如何实现该方法:
interface A {
fun foo() { print("A") }
fun bar()
}
interface B {
fun foo() { print("B") }
fun bar() { print("bar") }
}
class C : A {
override fun bar() { print("bar") }
}
class D : A, B {
override fun foo() {
super<A>.foo() // 调用 A 接口的 foo() 实现
super<B>.foo() // 调用 B 接口的 foo() 实现
}
override fun bar() {
super<B>.bar() // 调用 B 接口的 bar() 实现
}
}
- 如果继承了多个具有相同签名的方法,并且你想要调用特定接口的实现,可以使用
super<接口名>.方法名()
来指定。 - 即使只继承了一个实现(例如
bar()
),仍然需要重写并指定实现,以明确类的行为。这是因为编译器无法自动确定应该使用哪个接口的默认实现。如果不重写,会编译错误。
函数式 (SAM) 接口
核心概念
- 函数式接口(Functional Interface),也称为单一抽象方法 (Single Abstract Method, SAM) 接口,是指只有一个抽象成员函数的接口。它可以有多个非抽象成员函数,但只能有一个抽象成员函数。Kotlin 提供了
fun interface
关键字来声明函数式接口,并支持 SAM 转换,允许使用 lambda 表达式代替手动实现类,使代码更加简洁。
声明函数式接口
- 使用
fun interface
关键字声明:
fun interface KRunnable {
fun invoke() // 唯一的抽象方法
}
SAM 转换 (SAM Conversions)
- 概念:SAM 转换允许你使用 lambda 表达式代替手动创建实现函数式接口的类。Kotlin 会自动将与接口的单一抽象方法签名匹配的 lambda 表达式转换为接口的实例。
- 优点:简化代码,提高可读性。
- 示例:
fun interface IntPredicate {
fun accept(i: Int): Boolean // 唯一的抽象方法
}
// 不使用 SAM 转换 (传统方式)
val isEven = object : IntPredicate {
override fun accept(i: Int): Boolean {
return i % 2 == 0
}
}
// 使用 SAM 转换 (lambda 表达式)
val isEvenLambda: IntPredicate = IntPredicate { it % 2 == 0 }
fun main() {
println("Is 7 even? - ${isEven.accept(7)}") // 输出: Is 7 even? - false
println("Is 7 even? - ${isEvenLambda.accept(7)}") // 输出: Is 7 even? - false
}
在这个例子中,lambda 表达式
{ it % 2 == 0 }
被自动转换为了IntPredicate
接口的实现。Java 接口也支持 SAM 转换
从带构造函数的接口迁移到函数式接口
在 Kotlin 1.6.20 及更高版本中,支持对函数式接口构造函数的调用引用 (callable references)。这为从带有构造函数函数的接口迁移到函数式接口提供了一种源兼容的方式。
示例:
interface Printer {
fun print()
}
fun Printer(block: () -> Unit): Printer = object : Printer {
override fun print() = block()
}
// 可以替换为:
fun interface Printer {
fun print()
}
// 使用函数引用
// documentsStorage.addPrinter(::Printer)
- 保持二进制兼容性:使用
@Deprecated
注解,将传统函数Printer
标记为隐藏 (DeprecationLevel.HIDDEN
)。
@Deprecated(
message = "使用函数式接口 Printer 代替",
level = DeprecationLevel.HIDDEN
)
fun Printer(...) {...}
函数式接口 vs. 类型别名 (Type Aliases)
虽然类型别名也可以简化函数类型的使用,但函数式接口和类型别名有着不同的目的:
类型别名:只是现有类型的别名,不会创建新类型。
kotlin typealias IntPredicate = (i: Int) -> Boolean // IntPredicate 仅仅是函数类型 (i: Int) -> Boolean 的别名
函数式接口:创建一个新的类型。
kotlin fun interface IntPredicate { fun accept(i: Int): Boolean // IntPredicate 是一个真实的接口类型 }
区别总结
特性 | 类型别名 | 函数式接口 |
---|---|---|
类型创建 | 不创建新类型 | 创建新类型 |
成员 | 只能有一个成员(函数类型) | 可以有多个非抽象成员函数和一个抽象成员函数 |
接口扩展 | 不支持实现/继承其他接口 | 支持实现/继承其他接口 |
扩展函数 | 扩展函数会应用于所有相同签名的函数类型,不限于特定的别名。 | 扩展函数只适用于该函数式接口的实例,不会影响其他函数类型。 |
使用场景 | 用于简化函数类型,当 API 需要接受具有特定参数和返回类型的函数(任何函数)时。 | 用于定义更复杂的实体,例如具有非平凡的约定和/或操作,这些约定和/或操作无法用函数类型的签名表示。 |
性能 | 开销更小 | 可能有一定开销,例如转成 specific interface |
如何选择
- 类型别名:当 API 需要接受一个具有特定参数和返回类型的函数 (任何函数) 时,使用类型别名或简单的函数类型。
- 函数式接口:当 API 需要接受一个比函数更复杂的实体,例如,具有非平凡的约定和/或操作,并且无法用函数类型的签名来表示时,使用函数式接口。
可见性修饰符
核心概念
- 可见性修饰符控制类、对象、接口、构造函数、函数、属性及其 setter 的可见范围。Getter 的可见性始终与其属性相同。Kotlin 提供了四种可见性修饰符:
private
、protected
、internal
和public
。默认可见性是public
。
可见性修饰符概览
修饰符 | 描述 |
---|---|
public | (默认) 声明在任何地方都可见。 |
private | 声明只在声明它的文件或类中可见。 |
protected | 声明只在声明它的类及其子类中可见。 |
internal | 声明只在同一模块内可见。 模块是一组一起编译的 Kotlin 文件。 |
包级别声明 (Package-level Declarations)
在包内直接声明的函数、属性、类、对象和接口称为顶层声明。
public
(默认):任何地方都可见。private
:只在包含该声明的文件内可见。internal
:在同一模块内的任何地方都可见。protected
:不适用于顶层声明。
// 文件名: example.kt
package foo
private fun foo() { ... } // 只在 example.kt 文件内可见
public var bar: Int = 5 // 在任何地方都可见
private set // setter 只在 example.kt 文件内可见
internal val baz = 6 // 在同一模块内可见
- 要使用来自另一个包的可见顶层声明,需要导入它。
类成员 (Class Members)
对于在类内部声明的成员:
private
:只在该类内部可见(包括所有成员)。protected
:与private
具有相同的可见性,但在子类中也可见。internal
:同一模块内,看到该声明类的客户端都能看到其internal
成员。public
:看到该声明类的客户端都能看到其public
成员。
open class Outer {
private val a = 1 // 只在 Outer 类内部可见
protected open val b = 2 // 在 Outer 类及其子类中可见
internal open val c = 3 // 在同一模块内可见
val d = 4 // public (默认) 在任何地方都可见
protected class Nested {
public val e: Int = 5
}
}
class Subclass : Outer() {
// a 不可见
// b, c 和 d 可见
// Nested 和 e 可见
override val b = 5 // 'b' 是 protected
override val c = 7 // 'c' 是 internal
}
class Unrelated(o: Outer) {
// o.a, o.b 不可见
// o.c 和 o.d 可见 (同一模块)
// Outer.Nested 不可见,Nested::e 也不可见
}
- 外部类不能看到其内部类的
private
成员。 - 如果重写
protected
或internal
成员,并且没有显式指定可见性,则重写成员将具有与原始成员相同的可见性。
构造函数 (Constructors)
- 可以使用以下语法来指定类的主构造函数的可见性:
class C private constructor(a: Int) { ... } // 构造函数是 private
- 默认情况下,所有构造函数都是
public
,这实际上等同于它们在类可见的任何地方都可见(这意味着internal
类的构造函数仅在同一模块内可见)。 - 对于密封类 (sealed classes),构造函数默认为
protected
。
局部声明 (Local Declarations)
- 局部变量、函数和类不能具有可见性修饰符。
模块 (Modules)
internal
可见性修饰符表示成员在同一模块内可见。具体来说,模块是指一起编译的一组 Kotlin 文件,例如:- IntelliJ IDEA 模块。
- Maven 项目。
- Gradle 源码集(例外情况是测试源码集可以访问
main
的internal
声明)。 - 使用单个
<kotlinc>
Ant 任务调用编译的一组文件。
委托属性 (Delegated Properties)
核心概念
- 委托属性是一种强大的 Kotlin 特性,它允许将属性的
get()
和set()
方法委托给另一个对象(委托对象),从而实现代码复用、通用行为,并简化属性的实现逻辑。委托属性非常适合实现如延迟初始化、属性变更监听、数据存储等常见的编程模式。
基本语法
kotlin
复制
var <属性名>: <类型> by <表达式>
<属性名>
:你要声明的属性的名称。<类型>
:属性的类型,必须与委托对象getValue()
方法的返回值类型相匹配。<表达式>
:一个返回委托对象的表达式。这个对象负责实际的属性访问和修改逻辑。by
:关键字,指示 Kotlin 使用委托机制。
委托对象的要求
委托对象必须提供特定签名的
getValue()
函数(对于val
和var
属性)和setValue()
函数(仅对于var
属性)。这些函数需要声明为operator
。getValue() 函数 (必须):
kotlin
复制
operator fun getValue(thisRef: Any?, property: KProperty<*>): 返回值类型 { ... }
thisRef
:(Any?
) 对包含属性的对象的引用。如果该属性是顶层属性,则此值为null
。此参数允许委托访问其所属对象的状态(尽管通常不建议)。property
:(KProperty<*>
) 一个KProperty
实例,表示被委托的属性。它可以用于获取属性的元数据,如名称 (property.name
)。返回值类型
:委托属性的类型。getValue()
必须返回该类型或其子类型的值。
setValue() 函数 (仅用于 var 属性):
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: 新值类型) { ... }
thisRef
:(Any?
) 对包含属性的对象的引用(如同getValue()
)。property
:(KProperty<*>
) 表示被委托的属性的KProperty
实例(如同getValue()
)。value
:(类型与属性相同) 要分配给属性的新值。setValue()
应该执行将此值存储到支持存储中的操作。新值类型
:委托属性的类型。setValue()
必须接受该类型或其父类型的参数。
标准委托 (Standard Delegates)
Kotlin 标准库提供了一组有用的预定义委托,可以通过 kotlin.properties.Delegates
和 kotlin
包访问:
lazy()
:延迟初始化属性。属性的值仅在第一次访问时通过提供的 lambda 表达式计算。后续访问将返回缓存的值。val lazyValue: String by lazy { println("Computed!") "Hello" } fun main() { println(lazyValue) // 输出: Computed! \n Hello println(lazyValue) // 输出: Hello }
LazyThreadSafetyMode
:控制延迟初始化如何处理多线程访问。SYNCHRONIZED
(默认):保证只有一个线程初始化该值。这是线程安全的,但可能导致性能开销。PUBLICATION
:允许多个线程尝试初始化该值,但保证只有一个线程的计算结果将被使用。如果初始化 lambda 表达式是幂等的(多次执行产生相同的结果),则可以使用此选项。NONE
:不提供任何线程安全保证。如果属性只会在单个线程中访问,则可以使用此选项以获得更高的性能。
Delegates.observable()
:创建一个可观察的属性。每次属性值被分配新值时,都会调用提供的处理程序。import kotlin.properties.Delegates class User { var name: String by Delegates.observable("<no name>") { prop, old, new -> println("Property ${prop.name} changed: $old -> $new") } } fun main() { val user = User() user.name = "first" // 输出: Property name changed: <no name> -> first user.name = "second" // 输出: Property name changed: first -> second }
- 处理程序函数的签名是
(KProperty<*>, oldValue, newValue) -> Unit
。
- 处理程序函数的签名是
Delegates.vetoable()
:创建一个可否决的属性。提供的处理程序会在每次属性尝试被分配新值之前被调用。如果处理程序返回true
,则分配会发生;如果返回false
,则分配会被阻止。import kotlin.properties.Delegates class PositiveValueHolder { var value: Int by Delegates.vetoable(0) { property, oldValue, newValue -> newValue >= 0 // Only allow non-negative values } } fun main() { val holder = PositiveValueHolder() holder.value = 10 // OK println(holder.value) // Prints 10 holder.value = -5 // Vetoed! Value remains 10 println(holder.value) // Prints 10 }
- 处理程序函数的签名是
(KProperty<*>, oldValue, newValue) -> Boolean
。
- 处理程序函数的签名是
委托给另一个属性
Kotlin 允许一个属性将其 getter 和 setter 委托给同一类或不同类中的另一个属性。这对于实现向后兼容性重命名或同步多个属性非常有用。
var topLevelInt: Int = 0
class ClassWithDelegate(val anotherClassInt: Int)
class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
var delegatedToMember: Int by this::memberInt
var delegatedToTopLevel: Int by ::topLevelInt
val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt
class MyClass2 {
var newName: Int = 0
@Deprecated("Use 'newName' instead", ReplaceWith("newName"))
var oldName: Int by this::newName
}
- 在这个例子中,
delegatedToMember
的 getter 和 setter 都委托给了memberInt
。修改delegatedToMember
也会影响memberInt
, 反之亦然. @Deprecated
: 提示用户应该使用newName
代替oldName
将属性存储在 Map 中
可以使用 Map
或 MutableMap
作为委托对象,允许你使用字符串键从 Map 中动态检索属性值。这在处理 JSON 或其他动态数据结构时很有用。
class User(val map: Map<String, Any?>) {
val name: String by map
val age: Int by map
}
fun main() {
val user = User(mapOf(
"name" to "John Doe",
"age" to 25
))
println(user.name) // 输出: John Doe
println(user.age) // 输出: 25
}
- 在这个例子中,
User
类的构造函数接收一个Map<String, Any?>
.name
和age
属性的值是从这个 map 中使用字符串键"name"
和"age"
检索的。
局部委托属性
局部委托属性允许你在函数或代码块内部声明委托属性。它们对于缓存计算或实现仅在特定条件下才需要的局部变量的延迟初始化非常有用。
fun processData(data: String?) {
val processedData by lazy {
println("Processing data...")
data?.uppercase() ?: "Default Value"
}
if (data != null && data.isNotEmpty()) {
println("Using processed data: $processedData") // Triggers lazy initialization if 'data' is not null/empty
} else {
println("No data to process.")
}
}
- 在这个例子中,
processedData
仅当data
不是null
并且不是空字符串时才被初始化。如果data
为null
或为空,则不会执行初始化 lambda 表达式。
provideDelegate
函数
provideDelegate
函数允许你拦截委托属性的创建过程。这对于验证属性的正确使用、设置自定义委托实例或执行其他初始化逻辑很有用。
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): PropertyDelegate { ... }
thisRef
:对包含属性的对象的引用(如同getValue()
)。property
:表示被委托的属性的KProperty
实例(如同getValue()
)。- 返回值:作为属性委托的实际对象。它可以与原始委托对象不同。允许用自定义的 Delegate 替换用户提供的 Delegate。
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
class ResourceDelegate<T>(private val initializer: () -> T) : ReadOnlyProperty<Any?, T> {
private var value: T? = null
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
if (value == null) {
value = initializer()
}
return value!!
}
}
class ResourceLoader<T>(private val id: String) {
operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, T> {
println("Binding resource ${property.name} with ID $id")
// Perform validation or customization here, e.g., check if resource ID is valid
return ResourceDelegate {
println("Initializing resource ${property.name} with ID $id")
// Load resource based on ID (replace with actual resource loading logic)
"Loaded Resource for ${property.name} (ID: $id)" as T // Type cast is necessary as T is only known at compile time.
}
}
}
class MyUI {
val image: String by ResourceLoader("image_id")
val text: String by ResourceLoader("text_id")
}
fun main() {
val ui = MyUI()
println(ui.image)
println(ui.text)
}
provideDelegate
函数在属性创建时调用,允许在实际委托对象被使用之前执行逻辑。示例打印一条消息指示属性及其ID被绑定,然后才创建实际的ResourceDelegate
,它负责延迟加载资源。
委托属性的翻译规则
当 Kotlin 编译器遇到委托属性时,它会生成以下辅助代码:
- 隐藏的
$delegate
属性:对于var prop: Type by MyDelegate()
,编译器会创建一个隐藏的属性,例如prop$delegate
,类型为MyDelegate
。这个属性存储了实际的委托对象。 getter 和 setter 方法:编译器会生成
prop
的 getter 和 setter 方法。这些方法会调用委托对象的getValue()
和setValue()
方法。class C { private val prop$delegate = MyDelegate() var prop: Type get() = prop$delegate.getValue(this, this::prop) set(value: Type) = prop$delegate.setValue(this, this::prop, value) }
优化场景:在某些情况下,如直接委托给另一个属性,编译器为了优化性能,会直接访问委托属性的幕后字段,而省略
$delegate
属性的生成。
扩展 (Extensions)
核心概念
- Kotlin 扩展允许在不继承类或使用装饰器等设计模式的情况下,向类或接口添加新的功能。通过特殊声明(称为扩展)来实现。可以为第三方库中的类或接口编写新的函数,而无需修改它们。
扩展函数 (Extension Functions)
- 声明:在函数名前面加上接收者类型。
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1] // 'this' 指代列表
this[index1] = this[index2]
this[index2] = tmp
}
this
关键字在扩展函数中指代接收者对象(在点号之前传递的对象)。调用:像调用普通方法一样调用扩展函数。
val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // 'this' 在 'swap()' 内部将保存 'list' 的值
扩展的静态解析
- 扩展实际上并不修改它们所扩展的类。定义扩展并不会将新成员插入到类中,而只是使新的函数能够通过点号标记法在变量上调用。
- 扩展函数是静态分发的。在编译时根据接收者类型确定调用哪个扩展函数。
open class Shape
class Rectangle: Shape()
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"
fun printClassName(s: Shape) {
println(s.getName())
}
printClassName(Rectangle()) // 输出: Shape,因为 getName() 是根据 s 的声明类型 Shape 调用的
- 成员优先:如果一个类有一个成员函数,并且定义了一个具有相同接收者类型、相同名称并且适用于给定参数的扩展函数,则成员函数总是优先。
class Example {
fun printFunctionType() { println("Class method") }
}
fun Example.printFunctionType() { println("Extension function") }
Example().printFunctionType() // 输出: Class method
- 可以重载具有相同名称但不同签名的成员函数。
class Example {
fun printFunctionType() { println("Class method") }
}
fun Example.printFunctionType(i: Int) { println("Extension function #$i") }
Example().printFunctionType(1) // 输出: Extension function #1
可空接收者 (Nullable Receiver)
- 可以定义具有可空接收者类型的扩展。这些扩展可以在对象变量的值为
null
时调用。如果接收者为null
,则this
也为null
。建议在函数体内部执行this == null
检查,以避免编译器错误。
fun Any?.toString(): String {
if (this == null) return "null"
// 空检查后,'this' 自动转换为非空类型,因此 toString() 解析为 Any 类的成员函数
return toString()
}
扩展属性 (Extension Properties)
- Kotlin 支持类似于函数的扩展属性。
val <T> List<T>.lastIndex: Int
get() = size - 1
- 由于扩展实际上并没有将成员插入到类中,因此扩展属性没有幕后字段 (backing field) 的有效方法。这就是为什么不允许使用初始化器进行扩展属性的原因。扩展属性的行为只能通过显式提供 getter/setter 来定义。
class House
val House.number = 1 // 错误:不允许对扩展属性进行初始化
伴生对象扩展 (Companion Object Extensions)
- 如果一个类定义了一个伴生对象,你也可以为该伴生对象定义扩展函数和属性。就像伴生对象的常规成员一样,它们可以通过仅使用类名作为限定符来调用。
class MyClass {
companion object { } // 将被称为 "Companion"
}
fun MyClass.Companion.printCompanion() { println("companion") }
fun main() {
MyClass.printCompanion() // 输出: companion
}
扩展的作用域 (Scope of Extensions)
- 大多数情况下,你在顶层(直接在包下)定义扩展:
package org.example.declarations
fun List<String>.getLongestString() { /*...*/ }
- 要在其声明包外部使用扩展,请在使用点导入它:
package org.example.usage
import org.example.declarations.getLongestString
fun main() {
val list = listOf("red", "green", "blue")
list.getLongestString()
}
将扩展声明为成员 (Declaring Extensions as Members)
- 可以在一个类内部声明另一个类的扩展。在这样的扩展中,存在多个隐式接收者 - 可以访问其成员而无需限定符的对象。在其中声明扩展的类的实例称为分发接收者 (dispatch receiver),扩展方法的接收者类型的实例称为扩展接收者 (extension receiver)。
class Host(val hostname: String) {
fun printHostname() { print(hostname) }
}
class Connection(val host: Host, val port: Int) {
fun printPort() { print(port) }
fun Host.printConnectionString() {
printHostname() // 调用 Host.printHostname()
print(":")
printPort() // 调用 Connection.printPort()
}
fun connect() {
/*...*/
host.printConnectionString() // 调用扩展函数
}
}
fun main() {
Connection(Host("kotl.in"), 443).connect() //Host("kotl.in").printConnectionString() // 错误,扩展函数在 Connection 外部不可用
}
- 如果分发接收者的成员和扩展接收者的成员之间存在名称冲突,则扩展接收者优先。要引用分发接收者的成员,可以使用限定的
this
语法。
class Connection {
fun Host.getConnectionString() {
toString() // 调用 Host.toString()
this@Connection.toString() // 调用 Connection.toString()
}
}
- 声明为成员的扩展可以声明为
open
并在子类中重写。这意味着此类函数的调度对于分发接收者类型是虚拟的,但对于扩展接收者类型是静态的。
open class Base {
open fun Base.printFunctionInfo() {
println("Base extension function in BaseCaller")
}
open fun Derived.printFunctionInfo() {
println("Derived extension function in BaseCaller")
}
}
class Derived : Base() {
override fun Base.printFunctionInfo() {
println("Base extension function in DerivedCaller")
}
override fun Derived.printFunctionInfo() {
println("Derived extension function in DerivedCaller")
}
}
fun main() {
BaseCaller().call(Base()) // "Base extension function in BaseCaller"
DerivedCaller().call(Base()) // "Base extension function in DerivedCaller" - 分发接收者是虚拟解析的
DerivedCaller().call(Derived()) // "Base extension function in DerivedCaller" - 扩展接收者是静态解析的
}
可见性注意事项
- 扩展使用与在同一作用域中声明的常规函数相同的可见性修饰符。例如:
- 在文件顶层声明的扩展可以访问同一文件中的其他
private
顶层声明。 - 如果扩展在其接收者类型之外声明,则它无法访问接收者的
private
或protected
成员。
- 在文件顶层声明的扩展可以访问同一文件中的其他
类型别名 (Type Aliases)
核心概念
- 类型别名就像给一个类型起了个“外号”。你用“外号”和用真名效果完全一样,但“外号”可能更短、更好记。
用途
简化长类型名称:比如
Set<Network.Node>
这么长的类型,可以起个简单点的名字,比如NodeSet
。这在处理复杂的泛型类型时特别有用。给函数类型起别名:比如
(Int, String, Any) -> Unit
这个函数类型,可以起个别名MyHandler
,方便阅读和使用。给内部类/嵌套类起别名:如果多个类都有内部类,起个别名能让你更容易区分,例如
A.Inner
和B.Inner
可以分别用AInner
和BInner
来代替。
示例
// 简化集合类型
typealias NodeSet = Set<Network.Node>
// 简化泛型 Map 类型
typealias FileTable<K> = MutableMap<K, MutableList<File>>
// 简化函数类型
typealias MyHandler = (Int, String, Any) -> Unit
typealias Predicate<T> = (T) -> Boolean
// 简化内部类
class A { inner class Inner }
class B { inner class Inner }
typealias AInner = A.Inner
typealias BInner = B.Inner
重要:类型别名不是新类型!
typealias Predicate<T> = (T) -> Boolean
只是给(T) -> Boolean
起了个别名。- 编译器在编译时,会将所有类型别名替换成它对应的原始类型。
- 这意味着,你可以随意地用别名类型代替原始类型,反之亦然,kotlin编译器都认为完全一样。
示例说明
typealias Predicate<T> = (T) -> Boolean
fun foo(p: Predicate<Int>) = p(42) // 函数需要一个 Predicate<Int> 类型的参数
fun main() {
val f: (Int) -> Boolean = { it > 0 } // f 是一个 (Int) -> Boolean 类型的变量
println(foo(f)) // 可以直接把 f 传给 foo,因为 Predicate<Int> 和 (Int) -> Boolean 本质上是一样的
val p: Predicate<Int> = { it > 0 } // p 是一个 Predicate<Int> 类型的变量
println(listOf(1, -2).filter(p)) // 可以用 p 来过滤列表,效果和直接用 (Int) -> Boolean 一样
}
函数 (Functions)
核心概念
- 函数是代码的基本构建块,用于执行特定任务。Kotlin 提供了灵活的方式来定义和使用函数。
函数声明
- Kotlin 函数使用
fun
关键字声明。 - 声明格式:
fun 函数名(参数名: 参数类型): 返回类型 { 函数体 }
fun double(x: Int): Int {
return 2 * x
}
函数使用 (调用)
- 使用标准方式调用函数:
函数名(参数)
- 调用成员函数(类或对象中的函数)使用点表示法:
实例.函数名(参数)
val result = double(2) // 调用 double 函数
Stream().read() // 创建 Stream 类的实例并调用 read 函数
参数
- 函数参数使用 Pascal 符号定义:
参数名: 参数类型
- 多个参数用逗号分隔。
- 每个参数必须显式指定类型。
fun powerOf(number: Int, exponent: Int): Int { /* 函数体 */ }
- 可以使用尾随逗号来声明函数参数:
fun powerOf(
number: Int,
exponent: Int, // 尾随逗号
) { /* 函数体 */ }
默认参数
- 函数参数可以有默认值,当调用函数时省略相应的参数,则使用默认值。这减少了函数重载的数量。
- 默认值通过在类型后添加
=
来设置,例如参数名: 参数类型 = 默认值
fun read(
b: ByteArray,
off: Int = 0,
len: Int = b.size,
) { /* 函数体 */ }
- 重写的方法总是使用基类方法的默认参数值。当重写一个具有默认参数值的方法时,默认参数值必须从签名中省略:
open class A {
open fun foo(i: Int = 10) { /*...*/ }
}
class B : A() {
override fun foo(i: Int) { /*...*/ } // 不允许设置默认值.
}
- 如果一个默认参数在一个没有默认值的参数之前,则默认值只能通过使用命名参数来调用函数来使用:
fun foo(
bar: Int = 0,
baz: Int,
) { /*...*/ }
foo(baz = 1) // 使用默认值 bar = 0
- 如果默认参数之后的最后一个参数是一个 lambda 表达式,则可以将它作为命名参数或在括号外传递:
fun foo(
bar: Int = 0,
baz: Int = 1,
qux: () -> Unit,
) { /*...*/ }
foo(1) { println("hello") } // 使用默认值 baz = 1
foo(qux = { println("hello") }) // 使用默认值 bar = 0 和 baz = 1
foo { println("hello") } // 使用默认值 bar = 0 和 baz = 1
命名参数
- 可以在调用函数时命名一个或多个函数的参数。当函数有很多参数,并且很难将值与参数关联时,这很有帮助,尤其是当参数是布尔值或 null 值时。
- 当在函数调用中使用命名参数时,可以自由地更改它们在列表中列出的顺序。如果想使用它们的默认值,可以完全省略这些参数。
- 考虑
reformat()
函数,它有 4 个具有默认值的参数。
fun reformat(
str: String,
normalizeCase: Boolean = true,
upperCaseFirstLetter: Boolean = true,
divideByCamelHumps: Boolean = false,
wordSeparator: Char = ' ',
) { /*...*/ }
- 在调用此函数时,不必命名其所有参数:
reformat(
"String!",
false,
upperCaseFirstLetter = false,
divideByCamelHumps = true,
'_'
)
- 可以跳过所有具有默认值的参数:
reformat("This is a long String!")
- 也可以跳过具有默认值的特定参数,而不是省略所有参数。但是,在第一个跳过的参数之后,必须命名所有后续参数:
reformat("This is a short String!", upperCaseFirstLetter = false, wordSeparator = '_')
- 可以使用 spread 操作符 (用
*
作为数组的前缀) 通过名称传递可变数量的参数 (vararg):
fun foo(vararg strings: String) { /*...*/ }
foo(strings = *arrayOf("a", "b", "c"))
- 在 JVM 上调用 Java 函数时,不能使用命名参数语法,因为 Java 字节码并不总是保留函数参数的名称。
Unit 返回函数
- 如果函数不返回有用的值,则其返回类型为
Unit
。Unit
是一种只有一个值的类型——Unit
。这个值不必显式返回:
fun printHello(name: String?): Unit {
if (name != null)
println("Hello $name")
else
println("Hi there!")
// `return Unit` 或 `return` 是可选的
}
Unit
返回类型声明也是可选的。上面的代码等价于:
fun printHello(name: String?) { ... }
单表达式函数
- 当函数体由单个表达式组成时,可以省略花括号,并在
=
符号后指定函数体:
fun double(x: Int): Int = x * 2
- 当编译器可以推断出返回类型时,显式声明返回类型是可选的:
fun double(x: Int) = x * 2
显式返回类型
- 具有块状函数体的函数必须始终显式指定返回类型,除非打算让它们返回
Unit
,在这种情况下,指定返回类型是可选的。 - Kotlin 不会推断具有块状函数体的函数的返回类型,因为此类函数的主体可能具有复杂的控制流,并且返回类型对于读者(有时甚至对于编译器)来说并不明显。
可变数量的参数 (Varargs)
- 可以使用
vararg
修饰符标记函数的参数(通常是最后一个参数):
fun <T> asList(vararg ts: T): List<T> {
val result = ArrayList<T>()
for (t in ts) // ts 是一个 Array
result.add(t)
return result
}
- 在这种情况下,可以向函数传递可变数量的参数:
val list = asList(1, 2, 3)
- 在函数内部,类型
T
的vararg
参数显示为T
的数组,如上面的示例所示,其中ts
变量的类型为Array<out T>
。 - 只能将一个参数标记为
vararg
。如果vararg
参数不是列表中的最后一个参数,则必须使用命名参数语法传递后续参数的值,或者,如果参数具有函数类型,则通过在括号外传递 lambda 表达式来传递后续参数的值。 - 当调用一个
vararg
函数时,可以单独传递参数,例如asList(1, 2, 3)
。如果已经有一个数组并且想将它的内容传递给函数,请使用 spread 操作符(用*
作为数组的前缀):
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)
- 如果想将原始类型数组传递到
vararg
中,则需要使用toTypedArray()
函数将其转换为常规(类型化)数组:
val a = intArrayOf(1, 2, 3) // IntArray 是一个原始类型数组
val list = asList(-1, 0, *a.toTypedArray(), 4)
中缀符号 (Infix Notation)
- 用
infix
关键字标记的函数也可以使用中缀符号调用(省略调用中的点和括号)。中缀函数必须满足以下要求:- 它们必须是成员函数或扩展函数。
- 它们必须只有一个参数。
- 该参数不能接受可变数量的参数并且必须没有默认值。
infix fun Int.shl(x: Int): Int { /* 函数体 */ }
// 使用中缀符号调用函数
1 shl 2
// 与以下相同
1.shl(2)
中缀函数调用的优先级低于算术运算符、类型转换和
rangeTo
运算符。以下表达式是等效的:1 shl 2 + 3
等价于1 shl (2 + 3)
0 until n * 2
等价于0 until (n * 2)
xs union ys as Set<*>
等价于xs union (ys as Set<*>)
另一方面,中缀函数调用的优先级高于布尔运算符
&&
和||
、is
和in
检查以及一些其他运算符。这些表达式也是等效的:a && b xor c
等价于a && (b xor c)
a xor b in c
等价于(a xor b) in c
注意,中缀函数总是需要指定接收者和参数。当使用中缀符号在当前接收者上调用方法时,请显式使用
this
。这是为了确保明确的解析。
class MyStringCollection {
infix fun add(s: String) { /*...*/ }
fun build() {
this add "abc" // Correct
add("abc") // Correct
//add "abc" // Incorrect: the receiver must be specified
}
}
函数范围 (Function Scope)
- Kotlin 函数可以在文件的顶层声明,这意味着不需要创建一个类来保存一个函数,这是 Java、C# 和 Scala 等语言中必需的(自 Scala 3 起,顶层定义可用)。除了顶层函数之外,Kotlin 函数还可以局部声明为成员函数和扩展函数。
局部函数
- Kotlin 支持局部函数,局部函数是其他函数内部的函数:
fun dfs(graph: Graph) {
fun dfs(current: Vertex, visited: MutableSet<Vertex>) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v, visited)
}
dfs(graph.vertices[0], HashSet())
}
- 局部函数可以访问外部函数的局部变量(闭包)。在上面的例子中,
visited
可以是一个局部变量:
fun dfs(graph: Graph) {
val visited = HashSet<Vertex>()
fun dfs(current: Vertex) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v)
}
dfs(graph.vertices[0])
}
成员函数
- 成员函数是在类或对象内部定义的函数:
class Sample {
fun foo() { print("Foo") }
}
- 成员函数使用点表示法调用:
Sample().foo() // 创建 Sample 类的实例并调用 foo
泛型函数
- 函数可以具有泛型参数,这些参数使用尖括号在函数名称之前指定:
fun <T> singletonList(item: T): List<T> { /*...*/ }
尾递归函数
- Kotlin 支持一种称为尾递归的函数式编程风格。对于某些通常使用循环的算法,可以使用递归函数来代替,而没有堆栈溢出的风险。当一个函数用
tailrec
修饰符标记并且满足所需的正式条件时,编译器会优化掉递归,留下一个快速而高效的基于循环的版本:
val eps = 1E-10 // "足够好", 可以是 10^-15
tailrec fun findFixPoint(x: Double = 1.0): Double =
if (Math.abs(x - Math.cos(x)) < eps) x else findFixPoint(Math.cos(x))
- 要符合
tailrec
修饰符的条件,函数必须将自身调用作为它执行的最后一个操作。当递归调用之后有更多代码、在try/catch/finally
块中或在open
函数上时,不能使用尾递归。目前,Kotlin for the JVM 和 Kotlin/Native 支持尾递归。
高阶函数和 Lambda 表达式
核心概念
- 函数也是值:在 Kotlin 里,函数可以像数字、字符串一样,赋值给变量、存到数据结构里、当做参数传递给其他函数,或者从函数里返回。
- 高阶函数:简单说,就是能接收函数作为参数或者返回一个函数的函数。
- Lambda表达式:简单说,就是匿名函数,可以快速定义一个函数并使用。
什么是高阶函数?
- 定义:接收函数作为参数或返回一个函数的函数。
例子:
fold
函数就是一个典型的高阶函数。fold
函数接收一个初始值和一个组合函数。它会遍历集合中的每一个元素,使用组合函数将当前元素和累加值组合起来,最终返回一个结果。代码示例 (简化):
fun <T, R> Collection<T>.fold( initial: R, // 初始值 combine: (acc: R, nextElement: T) -> R // 组合函数 ): R { var accumulator: R = initial for (element: T in this) { accumulator = combine(accumulator, element) // 调用组合函数 } return accumulator }
combine
参数就是一个函数类型,它接收两个参数(R 和 T),返回一个 R 类型的值。
如何调用
fold
:使用 Lambda 表达式或者函数引用。例子:
val numbers = listOf(1, 2, 3) // 使用 Lambda 表达式 (最常见) val sum = numbers.fold(0) { acc, number -> acc + number } // acc是累加值,number是集合中的元素 println(sum) // 输出:6 // 使用函数引用 val product = numbers.fold(1, Int::times) // Int::times 是一个函数引用,指向 Int 类的 times 函数 println(product) // 输出:6
函数类型
- 作用:Kotlin 使用函数类型来表示函数,例如
(Int) -> String
表示一个接收Int
参数并返回String
的函数。 - 语法:
(参数类型1, 参数类型2, ...) -> 返回值类型
。()
表示没有参数,如() -> Unit
。Unit
相当于java中的void
。A.(B) -> C
表示一个带有接收者(receiver)的函数类型。它可以看作A类型的扩展函数。
- 可空函数类型:
((Int, Int) -> Int)?
表示这个函数类型可以为null
。 - 类型别名:可以使用
typealias
给函数类型起一个别名,方便使用。
typealias MyHandler = (String, Int) -> Unit
如何创建一个函数类型的实例?
有以下几种方法:
- Lambda 表达式:
{ a, b -> a + b }
- 匿名函数:
fun(s: String): Int { return s.toIntOrNull() ?: 0 }
- 函数引用:
::isOdd
(顶层函数),String::toInt
(成员函数) - 实现函数类型接口的类:
class IntTransformer: (Int) -> Int {
override operator fun invoke(x: Int): Int = TODO() // 实现 invoke 方法
}
调用函数类型实例
- 使用
invoke(...)
:f.invoke(x)
- 直接调用:
f(x)
- 如果函数类型有接收者,可以像调用扩展函数一样:
1.foo(2)
。
闭包
- 定义:Lambda 表达式或匿名函数可以访问外部作用域中定义的变量。
- 修改外部变量:Lambda 表达式可以修改外部作用域中的变量。
var sum = 0
numbers.forEach { sum += it }
println(sum) // 输出:numbers所有元素的总和
带有接收者的函数字面值
- 类型:
A.(B) -> C
- 作用:可以在 Lambda 表达式中像调用扩展函数一样调用接收者对象的方法。
this
关键字:在 Lambda 表达式中,可以使用this
关键字引用接收者对象。- 例子:
val sum: Int.(Int) -> Int = { other -> this + other }
println(1.sum(2)) // 输出:3
类型安全构建器
- 带有接收者的 Lambda 表达式常用于构建类型安全的 DSL (领域特定语言)。
Lambda 表达式
- 语法:
{ 参数 -> 函数体 }
- 参数类型推断:如果编译器可以推断出参数类型,可以省略参数类型。
- 尾随 Lambda:如果函数的最后一个参数是函数类型,可以将 Lambda 表达式放在括号外面。
numbers.filter { it > 0 }
it
关键字:如果 Lambda 表达式只有一个参数,可以使用it
关键字来引用该参数。numbers.map { it * 2 }
- 返回值:Lambda 表达式中最后一个表达式的值就是返回值。也可以使用
return@label
显式返回。 - 下划线
_
:如果 Lambda 表达式的参数没有使用,可以使用下划线_
代替参数名。map.forEach { (_, value) -> println(value) }
匿名函数
- 语法:
fun(参数: 类型): 返回值类型 { 函数体 }
- 特点:
- 可以显式指定返回值类型。
return
语句从匿名函数本身返回,而不是从包含它的函数返回 (与 Lambda 表达式不同)。
内联函数
核心概念
- 内联函数是一种特殊的函数,它允许编译器在调用点直接插入函数体,而不是像普通函数那样进行函数调用。这可以减少函数调用的开销,提高性能,特别是在函数调用非常频繁的情况下。
为什么需要内联函数?
- 性能优化:在某些情况下,函数调用的开销可能很大,尤其是在循环中频繁调用的函数。内联函数可以避免这种开销,提高代码的执行效率。
- 非局部返回:内联函数允许在 Lambda 表达式中使用
return
语句直接从包含它的函数返回,这在某些情况下非常有用。
如何使用内联函数?
- 在函数定义前加上
inline
关键字:
inline fun <T> lock(lock: Lock, body: () -> T): T { ... }
- 这会让编译器尝试把
lock()
函数的代码直接嵌入到调用它的地方。
内联函数的限制
- 性能问题:虽然内联函数可以提高性能,但过多地使用内联函数可能会导致生成的代码体积变大。因此,应该适度使用内联函数,避免内联大型函数。
- 非局部返回:在内联函数中,Lambda 表达式可以使用
return
语句直接从包含它的函数返回。这在某些情况下非常有用,但也可能导致代码逻辑变得复杂。
noinline
关键字
- 如果你只想内联部分 Lambda 表达式参数,可以使用
noinline
关键字:
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { ... }
inlined
参数会被内联,而notInlined
不会。noinline
类型的 Lambda 表达式可以像普通函数一样使用,可以存储在变量中或传递给其他函数。
非局部返回
- 在普通的 Lambda 表达式中,直接使用
return
是不允许的,因为 Lambda 表达式不能让外层函数返回。 - 但是,如果 Lambda 表达式被内联了,就可以使用
return
直接从包含 Lambda 表达式的外层函数返回。这种称为非局部返回。
fun foo() {
inlined {
return // OK: the lambda is inlined, 直接从 foo() 返回
}
}
crossinline
关键字
- 如果内联函数在另一个执行上下文(比如局部对象或者嵌套函数)中调用 Lambda 表达式,那么 Lambda 表达式就不能使用非局部返回。使用
crossinline
关键字来强制约束:
inline fun f(crossinline body: () -> Unit) {
val f = object: Runnable {
override fun run() = body() // 这里调用 body(),所以不能使用非局部返回
}
}
break
和 continue
- 在内联函数中,Lambda 表达式可以使用
break
和continue
来控制包含循环的控制流。
reified
关键字
- Kotlin 中,泛型在运行时会被擦除。也就是说无法在函数内部访问泛型的具体类型。
- 使用
reified
关键字可以让你在内联函数中访问泛型类型,就像它是一个普通的类一样。例如:
inline fun <reified T> TreeNode.findParentOfType(): T? { ... }
- 这样就可以使用
T::class
、T is Type
等操作,无需反射。 - 注意:只有内联函数才能使用
reified
关键字。
内联属性
inline
关键字可以用于属性的get()
和set()
方法。可以内联整个属性,也可以单独内联 getter 或 setter。
公共 API 内联函数的限制
- 如果你的内联函数是公共 API 的一部分(
public
或protected
),它会被其他模块调用,并且也会被内联。 - 为了避免潜在的二进制兼容性问题,公共 API 内联函数不能使用非公共 API 的声明(
private
或internal
)。 - 你可以使用
@PublishedApi
注解来允许internal
声明在公共 API 内联函数中使用。
操作符重载
核心概念
- 操作符重载允许你为自定义类型定义操作符的行为。这使得你可以使用标准的操作符(如
+
,-
,*
,/
等)来操作自定义类型,就像操作基本类型一样。
如何实现操作符重载?
- 在 Kotlin 中,操作符重载是通过在类中定义特定的函数来实现的。这些函数使用
operator
关键字标记。
一元操作符
操作符 | 对应的函数 | 例子 | 说明 |
---|---|---|---|
+ | unaryPlus() | +a | 正号 (通常不做任何改变) |
- | unaryMinus() | -a | 取负数 |
! | not() | !a | 逻辑非 (取反) |
示例
data class Point(val x: Int, val y: Int)
operator fun Point.unaryMinus() = Point(-x, -y) // 重载了负号操作符
val point = Point(10, 20)
fun main() {
println(-point) // 输出 "Point(x=-10, y=-20)"
}
自增/自减操作符 (++
, --
)
a++
(后置自增): 先返回a
的原始值, 然后再对a
进行自增。++a
(前置自增): 先对a
进行自增, 然后再返回a
的新值。a--
和--a
同理。重点:
inc()
和dec()
函数应该返回一个新值,赋值给变量,不要直接修改原始对象。
二元操作符
操作符 | 对应的函数 | 例子 | 说明 |
---|---|---|---|
+ | plus(b) | a + b | 加法 |
- | minus(b) | a - b | 减法 |
* | times(b) | a * b | 乘法 |
/ | div(b) | a / b | 除法 |
% | rem(b) | a % b | 取余数 (求模) |
.. | rangeTo(b) | a..b | 创建一个范围 (例如 1..10) |
..< | rangeUntil(b) | a..<b | 创建一个范围 (不包括最后一个元素) |
示例
data class Counter(val dayIndex: Int) {
operator fun plus(increment: Int): Counter {
return Counter(dayIndex + increment)
}
}
in
操作符
a in b
: 检查a
是否在b
中。对应函数:b.contains(a)
a !in b
: 检查a
是否不在b
中。对应函数:!b.contains(a)
索引访问操作符 ([]
)
a[i]
: 访问数组/集合中的元素。对应函数:a.get(i)
a[i] = b
: 设置数组/集合中元素的值。对应函数:a.set(i, b)
invoke
操作符 (()
)
a()
: 像调用函数一样调用对象。对应函数:a.invoke()
增强赋值操作符 (+=
, -=
, *=
, /=
, %=
)
a += b
: 相当于a = a + b
(如果plusAssign
函数不存在)。优先使用a.plusAssign(b)
- 其他类似。
相等和不等操作符 (==
, !=
)
a == b
: 检查a
和b
是否相等。对应函数:a?.equals(b) ?: (b === null)
a != b
: 检查a
和b
是否不相等。对应函数:!(a?.equals(b) ?: (b === null))
比较操作符 (>
, <
, >=
, <=
)
a > b
: 比较a
和b
的大小。对应函数:a.compareTo(b) > 0
- 其他类似。
注意
===
和!==
(检查是否是同一个对象) 不能重载。
this
表达式
核心概念
this
指的是 "当前对象"。就像你在跟别人说话时,用 "我" 来指代自己一样。
this
的用法
- 在类中:
this
指的是当前类的对象。比如,在一个Person
类中,this
指的就是当前这个Person
对象。 - 在扩展函数或带接收者的函数字面量中:
this
指的是接收者参数,也就是点号 (.
) 左边的那个对象。
this
的指向问题
如果只有一个 this
,它通常指向最里层的作用域。但如果想要访问外部作用域的 this
,就需要用到 限定的 this
。
限定的 this
(Qualified this)
当你想要从外部作用域 (比如外部类、扩展函数或带标签的函数字面量) 访问 this
时,可以使用 this@label
的形式。
@label
是作用域的标签。
示例
class A { // 隐式标签 @A
inner class B { // 隐式标签 @B
fun Int.foo() { // 隐式标签 @foo
val a = this@A // A 的 this (A 类的对象)
val b = this@B // B 的 this (B 类的对象)
val c = this // foo() 的接收者, 一个 Int (当前的 Int 值)
val c1 = this@foo // foo() 的接收者, 一个 Int (当前的 Int 值)
val funLit = lambda@ fun String.() {
val d = this // funLit 的接收者, 一个 String (当前的 String 值)
}
val funLit2 = { s: String ->
// foo() 的接收者, 因为闭合的 lambda 表达式
// 没有接收者
val d1 = this
}
}
}
}
隐式的 this
(Implicit this)
当你在 this
对象上调用成员函数时,可以省略 this.
部分。
注意
如果你有一个同名的非成员函数,要小心使用 this
,因为在某些情况下,可能会调用到非成员函数。
示例
fun main() {
fun printLine() { println("Local function") } // 局部函数
class A {
fun printLine() { println("Member function") } // 成员函数
fun invokePrintLine(omitThis: Boolean = false) {
if (omitThis) printLine() // 调用局部函数
else this.printLine() // 调用成员函数
}
}
A().invokePrintLine() // 输出: Member function
A().invokePrintLine(omitThis = true) // 输出: Local function
}
注解 (Annotations)
核心概念
- 注解是一种给代码添加额外信息的方式,就像给代码贴标签。这些信息可以被编译器、工具或者运行时环境使用。
如何声明注解?
- 在
class
关键字前面加上annotation
关键字。
annotation class Fancy
注解的元注解 (Meta-annotations)
元注解是用来修饰注解的注解,用于指定注解的行为。
@Target
:指定注解可以用于哪些代码元素上 (类、函数、属性等)。@Retention
:指定注解是否存储在编译后的 class 文件中,以及是否可以在运行时通过反射访问。默认情况下,两者都是true
。@Repeatable
:允许在同一个代码元素上多次使用同一个注解。@MustBeDocumented
:指定注解是公共 API 的一部分,应该包含在生成的 API 文档中。
示例
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
AnnotationTarget.TYPE_PARAMETER, AnnotationTarget.VALUE_PARAMETER,
AnnotationTarget.EXPRESSION) // 可以用于类、函数、类型参数等
@Retention(AnnotationRetention.SOURCE) // 只保留在源码中,编译后会被丢弃
@MustBeDocumented // 应该包含在 API 文档中
annotation class Fancy
如何使用注解?
@Fancy class Foo {
@Fancy fun baz(@Fancy foo: Int): Int {
return (@Fancy 1)
}
}
注解构造函数
- 注解可以有带参数的构造函数。
annotation class Special(val why: String)
@Special("example") class Foo {}
允许的参数类型
- Java 原始类型 (Int, Long 等)
- 字符串 (String)
- 类 (Foo::class)
- 枚举 (Enum)
- 其他注解
- 以上类型的数组
注意
- 注解参数不能是可空类型,因为 JVM 不支持将 null 作为注解属性的值。
注解作为参数
- 如果一个注解被用作另一个注解的参数,它的名字不需要加
@
符号。
annotation class ReplaceWith(val expression: String)
annotation class Deprecated(
val message: String,
val replaceWith: ReplaceWith = ReplaceWith("")
)
@Deprecated("This function is deprecated, use === instead", ReplaceWith("this === other"))
类作为参数
- 如果需要将一个类作为注解的参数,使用 Kotlin 的
KClass
。Kotlin 编译器会自动将其转换为 Java 类,以便 Java 代码可以正常访问注解和参数。
import kotlin.reflect.KClass
annotation class Ann(val arg1: KClass<*>, val arg2: KClass<out Any>)
@Ann(String::class, Int::class) class MyClass
实例化注解类
- 在 Kotlin 中,你可以像调用普通类的构造函数一样调用注解类的构造函数,并使用生成的实例。
annotation class InfoMarker(val info: String)
fun processInfo(marker: InfoMarker): Unit = TODO()
fun main(args: Array<String>) {
if (args.isNotEmpty())
processInfo(getAnnotationReflective(args))
else
processInfo(InfoMarker("default"))
}
Lambda 表达式上的注解
- 注解可以用于 Lambda 表达式。它们会被应用到 Lambda 表达式生成的
invoke()
方法上。
annotation class Suspendable
val f = @Suspendable { Fiber.sleep(10) }
注解的使用位置 (Use-site Targets)
- 当注解一个属性或主构造函数参数时,会生成多个 Java 元素,注解可以放在不同的位置。使用位置目标可以精确指定注解应该生成在哪个 Java 元素上。
class Example(@field:Ann val foo, // 注解 Java 字段
@get:Ann val bar, // 注解 Java getter
@param:Ann val quux) // 注解 Java 构造函数参数
常见的使用位置目标
file
:整个文件property
:属性 (Java 不可见)field
:字段get
:属性 getterset
:属性 setterreceiver
:扩展函数或属性的接收者参数param
:构造函数参数setparam
:属性 setter 参数delegate
:委托属性的委托实例字段
多个相同目标注解
- 如果多个注解具有相同的使用位置目标,可以使用方括号将它们括起来,避免重复指定目标。
class Example {
@set:[Inject VisibleForTesting]
var collaborator: Collaborator
}
Java 注解的兼容性
- Kotlin 与 Java 注解 100% 兼容。
Java 注解的参数
- 由于 Java 注解的参数顺序未定义,不能使用常规函数调用语法传递参数。必须使用命名参数语法。
// Java
public @interface Ann {
int intValue();
String stringValue();
}
// Kotlin
@Ann(intValue = 1, stringValue = "abc") class C
value
参数的特殊情况
- Java 中,如果注解只有一个名为
value
的参数,可以在 Kotlin 中省略参数名。
// Java
public @interface AnnWithValue {
String value();
}
// Kotlin
@AnnWithValue("abc") class C
数组作为注解参数
- 如果 Java 中注解的参数类型是数组,在 Kotlin 中会变成
vararg
参数。
// Java
public @interface AnnWithArrayValue {
String[] value();
}
// Kotlin
@AnnWithArrayValue("abc", "foo", "bar") class C
- 对于其他数组类型的参数,需要使用数组字面量语法或
arrayOf(...)
。
// Java
public @interface AnnWithArrayMethod {
String[] names();
}
@AnnWithArrayMethod(names = ["abc", "foo", "bar"])
class C
访问注解实例的属性
- 注解实例的值在 Kotlin 中作为属性暴露。
// Java
public @interface Ann {
int value();
}
// Kotlin
fun foo(ann: Ann) {
val i = ann.value // 直接访问 value 属性
}
可重复注解 (Repeatable Annotations)
- Kotlin 支持可重复注解,即可以在同一个代码元素上多次使用同一个注解。使用
@kotlin.annotation.Repeatable
元注解标记注解声明,使其在 Kotlin 和 Java 中都可重复。
@Repeatable
annotation class Tag(val name: String)
// 编译器会自动生成包含注解 @Tag.Container
- 可以使用
@kotlin.jvm.JvmRepeatable
元注解,并传递显式声明的包含注解类作为参数,为包含注解设置自定义名称。
@JvmRepeatable(Tags::class)
annotation class Tag(val name: String)
annotation class Tags(val value: Array<Tag>)
- 要通过反射提取 Kotlin 或 Java 可重复注解,使用
KAnnotatedElement.findAnnotations()
函数。
解构
核心概念
- 解构声明是一种方便的语法,可以将一个对象拆解成多个变量,方便使用。
语法
val (name, age) = person // 将 person 对象的属性赋值给 name 和 age 变量
println(name) // 可以直接使用 name 变量
println(age) // 也可以直接使用 age 变量
原理:componentN()
函数
- Kotlin 背后是通过调用一系列
componentN()
函数来实现解构的。 component1()
对应第一个变量component2()
对应第二个变量以此类推,
component3()
,component4()
等等所以,只要一个对象有
componentN()
函数,就可以用解构声明。
val name = person.component1() // 等价于 name = person 的第一个属性
val age = person.component2() // 等价于 age = person 的第二个属性
operator
关键字
componentN()
函数需要用operator
关键字标记,才能用于解构声明。
应用场景
for
循环:方便地从集合中提取元素。
for ((a, b) in collection) {
// a 和 b 分别是集合中每个元素的 component1() 和 component2() 的返回值
}
- 从函数返回多个值:使用
data class
可以方便地返回多个值,并且可以直接解构。
data class Result(val result: Int, val status: String)
fun function(): Result {
// ...
return Result(123, "Success")
}
val (result, status) = function() // 直接将函数返回的 Result 对象解构
println(result) // 输出 123
println(status) // 输出 Success
- 遍历 Map:可以直接在
for
循环中解构 Map 的键值对。
val map = mapOf("key1" to "value1", "key2" to "value2")
for ((key, value) in map) {
println("Key: $key, Value: $value")
}
忽略不需要的变量:下划线 _
- 如果解构时,你不需要某个变量,可以用下划线
_
代替变量名,Kotlin 不会调用对应的componentN()
函数,节省资源。
val (_, status) = getResult() // 只关心 status,忽略第一个返回值
Lambda 表达式中的解构
- 可以在 Lambda 表达式的参数中使用解构声明,使代码更简洁。
map.mapValues { (key, value) -> "$value!" } // 直接在 lambda 中解构键值对
map.mapValues { (_, value) -> "$value!" } // 如果不关心 key,可以用 _ 忽略
Lambda 中的类型声明
- 可以为整个解构参数或单个组件指定类型。
map.mapValues { (_, value): Map.Entry<Int, String> -> "$value!" } // 为整个解构参数指定类型
map.mapValues { (_, value: String) -> "$value!" } // 为 value 指定类型
类引用、函数引用、属性引用、构造器引用,以及绑定引用
Kotlin 提供了多种引用类型,允许我们在运行时操作程序的结构。理解这些引用类型对于编写更灵活、更强大的代码至关重要。
类引用 (Class References): ::class
- 作用:获取 Kotlin 类的运行时
KClass
对象。 - 语法:
ClassName::class
- 类型:
KClass<ClassName>
- 用途:
- 类型检查 (
is
运算符) - 获取类的信息 (例如,名称、构造函数、属性等)
- 与 Java 反射互操作 (
.java
属性获取java.lang.Class
对象)
- 类型检查 (
- 示例:
class MyClass {
fun myMethod() {}
}
fun main() {
val kClass: KClass<MyClass> = MyClass::class
println(kClass.simpleName) // 输出: MyClass
if (MyClass() is MyClass) {
println("It's a MyClass instance!")
}
val javaClass: Class<MyClass> = MyClass::class.java // 获取 Java Class 对象
}
函数引用 (Function References): ::
- 作用:获取函数的引用,可以像函数类型的值一样使用。
- 语法:
::functionName
- 类型:
(ParameterTypes) -> ReturnType
或KFunction<out R>
- 用途:
- 将函数作为参数传递给高阶函数 (例如,
filter
,map
,reduce
) - 创建函数组合
- 动态调用函数
- 将函数作为参数传递给高阶函数 (例如,
- 示例:
fun isEven(x: Int): Boolean {
return x % 2 == 0
}
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
val evenNumbers = numbers.filter(::isEven) // 将 isEven 函数作为参数传递
println(evenNumbers) // 输出: [2, 4]
val functionReference: (Int) -> Boolean = ::isEven // 显式声明类型
}
属性引用 (Property References): ::
- 作用:获取属性的引用,可以访问属性的值和元数据。
- 语法:
::propertyName
- 类型:
KProperty0<Type>
(顶层属性或无接收者的属性)KProperty1<Receiver, Type>
(具有接收者的属性)KMutableProperty...
(可变属性)
- 用途:
- 动态获取和设置属性值
- 访问属性的名称和类型
- 用于高阶函数 (例如,
map
可以提取对象的属性值)
- 示例:
val myTopLevelProperty = "Hello"
class MyClass(val myMemberProperty: Int)
fun main() {
println(::myTopLevelProperty.get()) // 输出: Hello
println(::myTopLevelProperty.name) // 输出: myTopLevelProperty
val myObject = MyClass(123)
println(MyClass::myMemberProperty.get(myObject)) // 输出: 123
}
构造器引用 (Constructor References): ::ClassName
- 作用:获取构造函数的引用,可以动态创建类的实例。
- 语法:
::ClassName
- 类型:
(ConstructorParameterTypes) -> ClassName
或KFunction<out ClassName>
- 用途:
- 依赖注入
- 动态创建对象
- 工厂模式
- 示例:
class MyClass(val message: String)
fun main() {
val constructorReference: (String) -> MyClass = ::MyClass
val myInstance = constructorReference("World") // 动态创建 MyClass 实例
println(myInstance.message) // 输出: World
}
绑定引用 (Bound References): instance::member
- 作用:将函数或属性绑定到特定的对象实例。
- 语法:
instance::memberName
(其中memberName
可以是函数或属性) - 类型:类型会根据绑定的成员和实例进行调整,不再包含接收者参数。
- 用途:
- 简化代码,避免每次都传递对象实例。
- 创建状态相关的函数引用。
- 在函数式编程中传递对象的特定行为。
- 示例:
class MyClass {
fun greet(name: String): String {
return "Hello, $name! I am ${this.hashCode()}"
}
val myProperty = 42
}
fun main() {
val myInstance = MyClass()
val boundFunction: (String) -> String = myInstance::greet // 绑定到 myInstance 的 greet 函数
println(boundFunction("Alice")) // 输出: Hello, Alice! I am ... (myInstance 的 hashCode)
val boundProperty: KProperty0<Int> = myInstance::myProperty // 绑定到 myInstance 的 myProperty 属性
println(boundProperty.get()) // 输出: 42
}
核心区别总结
特性 | 类引用 (::class ) | 函数引用 (:: ) | 属性引用 (:: ) | 构造器引用 (::ClassName ) | 绑定引用 (instance::member ) |
---|---|---|---|---|---|
引用对象 | 类 | 函数 | 属性 | 构造函数 | 实例 + 成员 (函数或属性) |
主要用途 | 类型信息、互操作 | 函数式编程 | 动态访问 | 动态创建对象 | 简化代码、状态相关 |
绑定 | 不绑定 | 不绑定 | 不绑定 | 不绑定 | 绑定到特定对象实例 |
选择合适的引用类型
- 如果需要操作类本身的信息,使用类引用 (
::class
)。 - 如果需要将函数作为参数传递或动态调用,使用函数引用 (
::functionName
)。 - 如果需要动态访问属性值或获取属性元数据,使用属性引用 (
::propertyName
)。 - 如果需要动态创建类的实例,使用构造器引用 (
::ClassName
)。 - 如果需要将函数或属性绑定到特定对象实例,使用绑定引用 (
instance::member
)。