0%

第2章:基础语法

不一样的类型声明

Kotlin 采用的是与 Java 相反的类型声明方式,类型名通常在变量名的后面: val a: String = "I am Kotlin"为什么采用这种风格呢?Kotlin官方的FAQ的回答是这样的:

我们相信这样可以使得代码的可读性更好。同时,这也有利于使用一些良好的语法特性,比如省略类型声明。Scala的经验表明,这不是一个错误的选择。

所以,类型放在变量后面的其中一个原因是为了类型省略,这个类型省略其实就是类型推导。

增强的类型推导

类型推导是Kotlin在Java的基础上增强的语言特性之一,即编译器可以在不显式声明类型的情况下,自动推导出它所需要的类型,如下:

1
2
3
4
5
6
7
8
9
val string = "I am Kotlin"
val int = 1314
val long = 1314L
...

//如果我们打印以上的变量类型,如: println(int.javaClass.name),将会获得如下结果:
//java.lang.String
//int
//long

类型推导很大程度上提高了Kotlin这种静态类型语言的开发效率,虽然静态类型语言有很多优点,然而在编码过程中却需要书写大量的类型。

声明函数返回值类型

虽然支持类型推导,但是函数返回值类型必须要显式声明,比如:

fun sum(x: Int, y: Int): Int { return x + y }

此时也许与Java的区别不大,其实Kotlin进一步增强了函数语法,我们可以把 {} 去掉,用等号来定义一个函数:

fun sum(x: Int, y: Int) = x + y

Kotlin支持的这种单行表达式与等号的语法定义的函数,叫做表达式函数体,作为区分,普通的函数声明叫做代码块函数体。但是别高兴太早,我们再来看一段递归程序:

fun foo(n: Int) = if(n == 0) 1 else n * foo(n - 1)

这种情况下,编译器并不能针对递归函数的情况推导类型,因此这里会报错。所以,在一些诸如递归等复杂条件下,及时用表达式定义函数,我们也必须显式声明类型,才能让程序正常工作,代码如下:

fun foo(n: Int): Int = if(n == 0) 1 else n * foo(n - 1)

val 和 var 的使用规则

Kotlin声明变量时,引入了 val 和 var 的概念。var 容易理解,就是变量,在JavaScript 中也有用到,但是 val 是什么呢?如果在 IDEA 中反编译 val 的实现成Java代码就能发现,它是通过 final 这一特性实现的

优先使用val避免副作用

Kotlin支持一开始不定义 val 变量的取值,随后再进行赋值,然而,因为引用不可变,所以val声明的变量只能被赋值一次,并且声明时不能省略变量类型,如下所示:

1
2
3
4
5
fun main(args: Array<String>) {
val a: Int
a = 1
println(a)//输出1
}

由于不可变性,我们可以直到 val 变量在并发环境更安全。

var 的适用场景

既然 val 那么好,为什么要 var 呢?首先,Kotlin 要兼容Java ,这就注定 必须有 var 的存在;其次有一些场景如果不适用 var 就必须得用到 递归 才能实现了,所以var需要存在。

高阶函数和Lambda

Kotlin 天然支持了部分函数式特性,函数式语言的一个典型特征在于函数式头等公民——我们不仅可以像类一样在顶层直接定义一个函数,也可以在函数内部定义一个局部函数!如下所示:

1
2
3
4
5
6
fun foo(x: Int) {
fun double(y: Int): Int {
return y * 2
}
println(double(x))
}

此外,Kotlin还能直接将函数像普通变量一样传递给另一个函数,或在其他函数中被返回,如何理解这个特性?

抽象和高阶函数

概念东西,略

函数的类型

在Kotlin中,函数类型的格式非常简单,举个例子:(Int) -> Unit,我们可以发现,Kotlin中的函数类型需要遵循以下几点:

  • 通过 -> 符号来组织参数类型和返回值类型,左边是参数类型,右边是返回值类型
  • 必须用一个括号来包裹参数类型,如果多个参数,可以用逗号分割,如: (Int, String?) -> Unit
  • 返回值即使是 Unit ,也必须显式声明

此外,Kotlin 还支持为声明参数指定名字:(errCode: Int, errMsg: String?) -> Unit 这还没完,高阶函数还支持返回另一个函数,所以还能这么做:

1
2
3
(Int) -> ((Int) -> Unit)
//如果把后半部分括号省略,可以写成:
(Int) -> Int -> Unit

方法和成员引用

Kotlin 存在一种特殊的语法,通过两个冒号来四号线对于某个类的方法进行引用。假如有一个CountryTest 类的对象实例 countryTest ,如果要引用它的 isBigEuropeanCountry 方法,就可以这么写:

countryTest::isBigEuropeanCountry

此外,我们还可以直接通过这种语法,来定义一个类的构造方法引用变量:

1
2
3
4
5
6
class Book(val name: String)

fun main(args: Array<String>) {
val getBook = ::Book
println(getBook("Dive into Kotlin").name)
}

可以发现,getBook 的类型为 (name: String) -> Book 。类似的道理,如果我们要引用某个类中的成员变量,比如Book类中的name,就可以这样引用: Book:name ,以上 Book::name 的类型为 (Book) -> String 。当我们在对Book 类对象的集合应用一些函数式API的时候,就会显得格外有用,比如:

1
2
3
4
5
6
fun main(args: Array<String>) {
val bookNames = listOf(
Book("Thinking in Java")
Book("Dive into Kotlin")
).map(Book::name)
}

匿名函数

Lambda 是语法糖

Kotlin 在JVM 层设计了 Function 类型 (Function0,Function1…Function22)来兼容Java的Lambda表达式,其中后缀数字代表了 Lambda 参数的数量。比如,Function1在源码中就是如下表示的:

1
2
3
interface Function1<in P1, out R>: kotlin.Function<R> {
fun invoke(p1: P1): R
}

可见每个Function 类型都有一个invoke方法,设计Function类型的目的之一就是要兼容Java ,实现在Kotlin 中也能调用Java的Lambda。在 Java 中,实际上不支持把函数作为参数,而是通过函数式接口来实现这一特性。

函数、Lambda和闭包

“柯里化”风格、扩展函数

柯里化略

在我们介绍的Lambda的表达式中,还存在一种特殊的语法,如果一个函数只有一个参数,且该参数为函数类型,那么在调用该函数时,外面的括号就可以省略,例子如下:

1
2
3
4
5
6
7
8
fun omit(block: () -> Unit) {
block
}

//那么我们在调用的时候,可以写成
omit {
println("parentheses is omitted")
}

另一项特性 扩展函数,允许我们在不修改已有类的前提下,给它增加新的方法,示例如下:

1
2
3
fun View.invisible() {
this.visibility = View.INVISIBLE
}

在上述例子中,类型View被称为接收者类型,this对应的是这个类型锁创建的接收者对象,this也能被省略,就像这样:

1
2
3
fun View.invisible() {
visibility = View.INVISIBLE
}

面向表达式编程

现在,罗列下我们已经提及的表达式概念:

  • if表达式
  • 函数体表达式
  • Lambda表达式
  • 函数引用表达式

Unit类型:让函数调用皆为表达式

之所有不能说Java中的函数调用皆是表达式,是因为存在特例 void,在Java中如果声明的函数没有返回值,那么它就要用void修饰:

1
2
3
void foo () {
System.out.println("hahah");
}

所以foo就不具有值和类型信息,就不能算作一个表达式。函数式语言在所有的情况下都具有返回类型,所以kotlin引入了 Unit 来替代 void 关键字。如何理解 Unit ?其实与 int 一样,都是一种类型,然而它不代表任何信息,它就是一个单例,它的实例只有一个 ,可以写为 () 。

for循环和范围表达式

在Java中,经常在for来构建循环体:

1
2
3
for (int i = 0; i < 10; i ++) {
System.out.println(i);
}

但是kotlin会简单很多:

1
2
3
4
5
for (i in 1..10) println(i)  
//当然也能把大括号和i的类型加上
for (i:Int in 1..10) {
println(i)
}

范围表达式,1..10 这种语法是范围表达式(range) 。

官网的表述是:Range表达式是通过rangeTo 函数实现的,通过 .. 操作符与某种类型的对象组成,除了整形的基本类型外,该类型需要实现 java.lang.Comparable 接口

举个例子,由于 String类实现了 Comparable 接口,字符串之间可以比较大小,所以我们可以创建一个字符串区间,如 "abc".."xyz"

另外,kotlin 还提供了步长和倒序以及半开区间:

1
2
3
4
5
6
7
8
//步长
for (i in 1..10 step 2) print(i) //输出 1 3 5 7 9

//倒序
for (i in 10 downTo 1 step 2) print(i) //输出: 10 8 6 4 2

//半开区间
for(i in 1 until 10) print(i) //输出 123456789

用 in 来检查成员关系,,在Kotlin中我们可以用 in 关键字来检查一个元素是否是一个区间或者集合中的成员,比如:"a" in listOf ("b" , "c") ,会返回 false ;在 in 之前加上叹号就是相反结果: "a" !in listOf ("b" , "c") 返回true。更多的应用场景如下:

1
2
3
4
5
6
7
//结合范围表达式
"kot" in "abc".."xyz"

//还能通过withIndex 提供键值元祖
for ((index,value) in array.withIndex) {
println("the element at $index is $value")
}

中缀表达式

前面见识过 in、step、downTo、until 这些写法,都不需要通过点号,而是用中缀表达式来被调用,从而语法更直观。这是如何实现的呢?看下标准库中类似的方法 to 的设计:

1
infix fun <A,B> A.to(that: B): Pair<A, B>

函数可变参数

Java 中采用 “…” 来表示可变参数,Kotlin中通过 varargs 关键字实现可变参数…。需要注意的是,Java 中的可变参数必须是最后一个参数,Ktolin中没有这个限制,但两者都可以在函数体中以数组方式来使用可变参数变量:

1
2
3
4
5
6
7
8
9
fun printLetters(varargs letters: String, count: Int) {
print("${count} letters are ")
for (letter in letters) print(letter) // 输出 3 letters are abc
}

//此外,我们还能使用星号(*)来传入外部的变量作为可变参数的变量:

val letters = arrayOf("a", "b", "c")
printLetters(*letters, count = 3) //同样会输出 3 letters are abc

由于to会返回 Pair 这种键值对的结构数据,因此我们经常会把它与map结合在一起使用,如下:

1
2
3
4
5
mapOf(
1 to "one",
2 to "two",
3 to "three"
)

字符串的定义和操作

kotlin 中有丰富的API,比如: "abcdefg".filter {c -> c in 'a'..'d'} //输出 abcd

定义原生字符串

Java 对原生字符串只能通过转义字符的方法支持。然而,在Kotlin中已经支持直接写原生字符串,使用3个引号的方式(“””),体验下:

1
2
3
4
5
6
7
8
9
val rawString = """
\n Kotlin is awesonme.
\n Kotlin is a better Java. """

print(rawString)

//会打印:
\n Kotlin is awesonme.
\n Kotlin is a better Java.

可以看到非常简洁,如果用Java 来表示会非常复杂,尤其是 Html 代码。

字符串模板

字符串判等

Kotlin 中判等性有两种类型:

  • 结构相等。 通过 == 来判定两个对象的内容是否相等
  • 引用相等。通过 === 来判断两个对象的引用是否一样,与之相反的操作是 !== ,
谢谢你的鼓励