Kotlin 简要语法快速学习

前言

因为一些机缘巧合, 笔者需要研究一下 Android 应用程序的二次开发, 作为目标的软件恰好是使用 Kotlin 编写的, 而笔者尚无该编程语言的基础, 因此需要快速掌握 Kotlin 的一些重要语法.

笔者对 Kotlin 语法糖多早有所耳闻. 考虑到官方文档 (或者说, 笔者本次参考的资料) 未必对所有的语法特性都能面面俱到, 笔者在这篇文章中, 可能只会涉及到部分语法糖.

需要注意的是, 本文只会聚焦于 Kotlin 的语法规则, 并辅以一些具体例子, 并不会在这里谈 Kotlin 是如何编译源代码的, Kotlin 是如何应用到 Android 开发的, 诸如此类.

借用 Arch Linux 软件仓库 community/kotlin 包的描述: Statically typed programming language that can interoperate with Java. 可以预料的是, Kotlin 和 Java 的语法规则有一定相似之处, 但笔者同样没有 Java 基础, 因此对这两门语言都比较陌生的读者也不用担心笔者跳过二者相似的部分不谈.

准备

参考教程

笔者主要参考的教程即为 Kotlin 的官方文档, 并从 Basic syntax 章节开始.

同时, Kotlin 为已有变成基础的学习者提供了 Learning materials overview 的页面, 其中的第一章节同样是 Basic syntax.

开发环境

Kotlin 官方文档中建议使用 IntelliJ IDEAAndroid Studio 来进行 Kotlin 开发, 但对于语法学习则不必要使用这些 IDE (更何况后者从名字上就像是针对 Android 开发的). 本次学习过程中, 笔者选择在 Arch Linux 中直接安装 community/kotlin 的包 (版本号为 1.7.22) 作为编译环境, extra/jdk17-openjdk 包 (版本号为 17.0.5.u1) 作为生成的 JAR 包的运行环境. 读者可以搭配装载 Kotlin 相关插件Visual Studio Code 用以编辑 Kotlin 源代码.

编译运行

这里笔者略去 JVM 等细节, 仅说明命令行环境下如何编译单文件 Kotlin 源代码.

假设需要编译的源代码文件路径为 /path/to/kotlin-learning/helloworld.kt. 保证 Java 运行时环境和 Kotlin 编译器安装的情况下, 执行

1
2
3
cd /path/to/kotlin-learning
kotlinc helloworld.kt -include-runtime -d helloworld.jar
java -jar helloworld.jar

包的声明与导入

熟悉 Java 语法的读者应该很习惯诸如

1
package com.example.myapp;

的代码. 在 Kotlin 中一般仍然需要编写类似的结构, 这是因为 Kotlin 同样依赖”包” (package) 的结构来组织代码. 显而易见的是点分隔符 . 规定了包之间的层级关系. 暂时可以简单理解为这声明了代码文件的”ID”. 不过需要注意的是, Kotlin 中的包声明不必要与实际的文件系统结构一致, 也就是说, 声明了 com.example.myapp 的代码文件的路径不必要是 com/example/myapp.kt.

声明包名之后, 就可以通过 import 语句导入其他包, 乃至其他 (诸如顶级函数和属性, 枚举类型常数等):

package_example.kt
1
2
3
4
5
6
7
8
9
10
package ktlearning,package_example

// 导入包 `OneFile`
import ktlearning.one_package.OneFile
// 导入包 `another_package` 下的所有子包
import ktlearning.another_package.*
// 避免包名冲突
import ktlearning.another_another_package.OneFile as AnotherFile
// 导入顶级函数 `TopLevelFunc`
import ktlearning.another_another_package.AnotherFile.TopLevelFunc

主函数

Kotlin 需要主函数作为程序的入口. 主函数通常写作:

1
2
3
fun main(args: Array<String>) {
// 函数体
}

这类似于 Java 中的

1
2
3
public static void main(String[] args) {
// 函数体
}

从 Kotlin 的代码中, 可以看出这个主函数 main(args) 接受一个 String 类型的数组 (Array). 当然在当前版本的 Kotlin 中, 主函数可以省去 args 这一参数. 函数体中未使用该参数反而会触发警告.

对于这个参数, 一个常见的操作是直接将其转化为字符串并输出:

example.kt
1
println(args.contentToString())

例如 java -jar example.jar foo 1024 "bar baz" 的输出将为

1
[foo, 1024, bar baz]

有趣的是, 若将 .contentToString() 删去, Kotlin 依然可以编译, 但最终可能会输出诸如

1
[Ljava.lang.String;@4f47d241

的错误结果.

输出到标准输出 (standard output)

helloworld.kt
1
2
3
4
5
6
7
8
package ktlearning.helloworld

fun main() {
println("Hello world!")
print(1)
print(" more ")
print("hello world!\n")
}

输出:

1
2
Hello world!
1 more hello world!

Kotlin 支持 print(message)println(message) 两种输出到标准输出的方式. 需要注意的是这两个函数只接受单一变量参数.

属性

“属性” (property) 这个概念被归类于 Classes and objects 这一章节中. 这个术语可能更常在面向对象编程中提及. 当然, 本文之前涉及到的完整的可编译代码似乎并没有显式地涉及到面向对象 (例如, 没有出现 class 这一关键字), 不过 Kotlin 中依然将诸如其他编程语言中很早提及的 “不可变变量” (immutable variable) 和 “可变变量” (mutable variable) 都视为一种 “属性” - 这表明它们可以具有传统的 “变量” 所缺失的特性.

Kotlin 中同样有 “编译时常量” (compile-time constant) 的概念. 根据官方文档相关的表述, 这类属性若在编译时值已知, Kotlin 编译器则可以在编译期间将对其的引用替换为实际的值.

试图定义传统意义上的 “变量” 和 “常量” 时:

1
2
3
4
5
6
// 定义不可变变量
val a: Int = 1
// 定义可变变量
var b = "foo"
// 定义编译时常量
const val PI = 3.14159

getters 和 setters

如果可以提前涉及到一些面向对象的内容的话, 既然这些概念都可以被抽象为 “属性”, 那面向对象中 getter 和 setter 的存在就不可被略过了. Kotlin 中对可变属性的完整定义语法如下:

1
2
3
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]

不可变属性的则如下:

1
2
3
val <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
// 不允许定义 setter

一些简单的定义示例如下:

1
2
3
4
5
6
7
// 自动推断为 `Int` 类型, 具有默认的 getter 和 setter
var initialized = 1
// 为 `Int` 类型不可变属性设置自定义 getter
val area: Int
get() = this.width * this.height
// 同时根据 getter 自动推断
val area2 get() = this.width * this.height

backing fields

若涉及到在属性的 setter 中为属性本身赋值时, 为避免出现无限递归的情形, Kotlin 引入了一个 “内部属性” field 用以 “临时” 存储属性值:

1
2
3
4
5
6
var counter = 0
set(value) {
if (value >= 0) {
field = value
}
}

如果将上述代码块中的 field = value 改为 counter = value, 编译器则会抛出堆栈溢出的错误.

数据类型

Kotlin 中任何事物皆是对象. 这里仅讨论一些基本的数据类型.

整型

类型 字节数
Byte 1
Short 2
Int 4
Long 8

与一些编程语言相同, 在类型自动推断中, 可以在数字末尾添加 L (例如 1L) 来声明其类型为 Long 而不是默认的 Int.

参见 Integer types.

浮点型

类型 字节数 significant bits exponent bits decimal digits
Float 4 24 8 6-7
Double 8 53 11 15-16

同样, 在数字末尾添加 fF (例如 3.14f) 来声明其类型为 Float 而不是默认的 Double.

需要注意的是, 浮点型和整型之间不会自动转换. 例如如下的代码会抛出编译错误:

1
2
3
fun sum(a: Double, b: Double) = a + b
// `1` 不是 `Double` 类型
sum(1, 2.0)

参见 Floating-point types.

无符号整型

只需要在原有的数据类型前添加 U (例如 UByte) 即可.

同样, 在数字末尾添加 u (例如 1u) 来声明其类型为无符号类型. 类似的, ulUL 声明其类型为 ULong.

特殊的数字表示

除了上文提及的 f / FL, Kotlin 还支持其他特别的数字表示方法:

特殊表示 示例
二进制整型 0b00001111
八进制整型 不支持
十六进制整型 0x0F
科学计数法 1.0e24

同时, Kotlin 支持在数字之间增加下划线 (_) 来增加可读性:

1
2
3
4
5
val oneMillion = 1_000_000
val creditCardNumber = 1234_5678_9012_3456L
val socialSecurityNumber = 999_99_9999L
val hexBytes = 0xFF_EC_DE_5E
val bytes = 0b11010010_01101001_10010100_10010010

函数

上文中已经出现了主函数的定义和使用方法. 对于一个通常的函数, 例如实现两个整数的加法:

func_sum.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package ktlearning.func_sum

// 通常的函数定义
fun sum1(a: Int, b: Int): Int {
return a + b
}

// 直接定义其返回值的函数定义, 同时实现了类型推断
fun sum2(a: Int, b: Int) = a + b

// 定义无返回值的函数 (`Unit` 可以省略)
fun printSum(a: Int, b: Int): Unit {
println("Sum of $a and $b is ${a + b}")
}

fun main() {
val a = 2
var b = 3

println("Sum of $a and $b is ${sum1(a, b)}")
println("Sum of $a and $b is ${sum2(a, b)}")
printSum(a, b)
}

面向对象

直接用例子说明:

oop_shape.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package ktlearning.oop_shape

// 可继承的类前加 `open`
open class Shape(name: String) {
// the primary constructor
var name = name

open fun say() {
println("My name is ${this.name}!")
}

open val area: Double = 0.0
}

class Rectangle(name: String, width: Double, height: Double) : Shape(name) {
var width = width
var height = height

// 重载父类的属性
override val area: Double
get() = this.width * this.height
}

class Circle(name: String, radius: Double) : Shape(name) {
val PI = 3.1415926

var radius = radius

override val area: Double
get() = PI * this.radius * this.radius
}

fun main() {
val rect = Rectangle("My Rectangle", 2.0, 3.5)
val circ = Circle("My Circle", 2.0)

rect.say()
circ.say()

println("The area of ${rect.name} is ${rect.area}")
println("The area of ${circ.name} is ${circ.area}")
}

输出为:

1
2
3
4
My name is My Rectangle!
My name is My Circle!
The area of My Rectangle is 7.0
The area of My Circle is 12.5663704

参见 Creating classes and instances.

注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 行注释

/*
块注释
*/

/*

下面是内层块注释.

/* 内层块注释 */

上面是内层块注释

*/

条件结构

if 语句块

1
2
3
4
5
if (a > b)
println(a)
else {
println(b)
}

条件表达式

1
fun max(a: Int, b: Int) = if (a > b) a else b

相对的, C 中为

1
2
3
int max(int a, int b) {
return (a > b) ? a : b;
}

Python 中为

1
2
def max(a: int, b: int) -> int:
return a if a > b else b

(个人认为 Python 的条件表达式的结构经常写错, 因为条件被插入在两个分支表达式之间.)

when

Kotlin 中的 when 类似于 C 中的 switch, 但其用法更加灵活:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
when (x) {
// 左侧可以是一个表达式, 用以匹配
1 -> println("x == 1")
2 -> println("x == 2")
// 不匹配上述所有表达式
else -> {
println("x is neither 1 nor 2")
}
}

// `when` 也可以作为表达式
fun describe(obj: Any): String =
when (obj) {
1 -> "One"
"Hello" -> "Greeting"
// 可以包含条件表达式
is Long -> "Long"
!is String -> "Not a string"
else -> "Unknown"
}

fun isPositive(x: Int) = when (x) {
// 当覆盖所有可能性时, 无需 `else` 分支
x > 0 -> true
x <= 0 -> false
}

循环结构

for 循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for (i in 1..3) {
println(i)
}

for (i in 6 downTo 0 step 2) {
println(i)
}

for (elem in array) {
println(elem)
}

for (i in array.indices) {
println(array[i])
}

for ((i, elem) in array.withIndex()) {
println("array[$i] = $elem")
}

while 循环

1
2
3
4
5
6
7
while (x > 0) {
x--
}

do {
val y = retrieveData()
} while (y != null)

可空值 (nullable values) 和空值检查 (null checks)

可空类型需要在类型后添加 ? 标识. null 则表示空值.

null_check.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 若字符串不能表示一个整型, 则转化为 `null`
fun parseInt(str: String): Int? {
return str.toIntOrNull()
}

fun printProduct(arg1: String, arg2: String) {
val x = parseInt(arg1)
val y = parseInt(arg2)

// 不能直接使用 `x * y`, 因为 `x` 和 `y` 可能为 `null`
if (x != null && y != null) {
// 空值检查后, 其类型自动转化为非空的整型
println(x * y)
}
else {
println("'$arg1' or '$arg2' is not a number")
}
}

fun main() {
printProduct("6", "7")
printProduct("a", "7")
printProduct("a", "b")
}

输出为:

1
2
3
42
'a' or '7' is not a number
'a' or 'b' is not a number
Posted on

2022-12-07

Updated on

2022-12-07

Licensed under

Comments