[关闭]
@ltlovezh 2016-04-26T15:16:01.000000Z 字数 13084 阅读 1322

Kotlin语法

Kotlin


Kotlin是一门与Swift类似的静态类型JVM语言,由JetBrains设计开发并开源,号称“Android世界的Swift”。最近花时间看了下这门语言,整理了一些和Java区别比较大的方面,仅作备忘(这里仅是一部分)。

控制流

if表达式

在Kotlin 中,if是一个带有返回值的表达式(也可以是一般性语句)。因此,在Kotlin中没有三元操作符(condition ? then : else),因为if表达式就能胜任这项工作。

  1. //定义变量
  2. val a = 1
  3. val b = 2
  4. //if作为表达式,此时必须有else分支
  5. val max = if (a > b) a else b
  6. //if作为表达式,此时必须有else分支(此时分支是代码块,且代码块的最后一个表达式的值是当前代码块的值)
  7. val min = if (a > b) {
  8. print("Choose a")
  9. b
  10. } else {
  11. print("Choose b")
  12. a
  13. }

作为表达式时,一般是将if表达式(的值)赋值为变量,或者在函数中returnif表达式(的值)。在这种情况下,if表达式必须包含else分支。

除了作为表达式之外,if也可以实现一般性语句,此时则不必包含else分支。
例如:

  1. fun max(a: Int, b: Int): Int {
  2. if (a > b) {
  3. println("a")
  4. return a
  5. }
  6. return b
  7. }

When表达式

在Kotlin中,when是switch的替代品。when既可以作为表达式,也可以是一般性语句。
作为when表达式时,else分支是不可缺少的(除非编译器检测到前面的条件分支覆盖了所有的情况)。同时,最终匹配的条件分支的值就是整个when表达式的值。(和if表达式一样,when表达式中的分支也可以是代码块,代码块的值则是最后一条表达式的值)。
作为一般性语句时,则忽略每个分支的值。

when表达式中比较多变是分支条件(branch condition),可以是普通字面量、常量、表达式、范围判断以及类型判断等。例如:

  1. val x = 4
  2. when (x) {
  3. //字面量
  4. 3 -> println("常量")
  5. //表达式
  6. if (x > 0) 2 else -1 -> println("表达式")
  7. //范围匹配
  8. !in 1..10 -> println("范围匹配")
  9. //类型判断
  10. is Int -> println("类型判断")
  11. else -> println("else")
  12. }

除此之外,when能够取代if-else if链。例如:

  1. when {
  2. a > b -> print("a > b")
  3. a < b -> print("a < b")
  4. else -> print("a == b")
  5. }

这种情况下,没有when参数,若分支条件为true,则匹配成功。

for循环

for操作符可以对任何提供迭代器的对象进行遍历(这点和Java类似)。例如:

  1. for (item in collection)
  2. println(item)

但是对数组的遍历,会被编译器优化成基于索引值来遍历,而不会创建迭代器对象。
当然,我们也可以显示的通过索引值来遍历一个数组,例如:

  1. for (i in array.indices)
  2. println(array[i])

另外,针对数组,还有更方便的遍历方式,例如:

  1. val array = arrayOf("leon", "lt", "zh")
  2. for ((index, value) in array.withIndex()) {
  3. println("the element at $index is $value")
  4. }

while循环

whiledo..while的使用方式和Java中相同,例如:

  1. do {
  2. var a = 0;
  3. println("a = $a")
  4. } while (a < 0)//注意,这里变量a可以被访问,这点和java当中不同。

Null安全性

在Java中,出现频率最高的运行时异常应该就是NullPointerException。Kotlin致力于消除空指针异常,Kotlin类型系统将引用(reference)分为可空和不可空。

默认情况下,变量是不可空类型,例如:

  1. var a: String = "abc"
  2. a = null // compilation error

但是,我们可以通过在类型后面添加?,来明确声明一个可空的变量。

  1. //此时变量的类型为String?,和String是不同,不可直接赋值
  2. var b: String? = "abc"
  3. b = null // ok
  4. //因为变量b可能为null,所以下面的语句是编译不过的
  5. val l = b.length //error: variable 'b' can be null

针对可空类型的变量,有两种方式来访问他们的属性和方法。
1. 明确地检测变量是否为Null

  1. val str: String? = "abc"
  2. println(str.length) //编译不通过,str可能为Null
  3. if (str != null) {
  4. //编译器检测到进行了非空检测,所以允许直接访问字符串的legth属性
  5. println(str.length)
  6. }
  7. println(str.length)//编译不通过,str可能为Null
  1. ?.安全的调用
  1. val str: String? = "abc"
  2. //如果str不为Null,就会返回字符串的长度,否则返回null。变量len的类型是Int?
  3. val len = str?.length

针对可空变量的特殊操作符

  1. ?:操作符
  1. val str: String? = "abc"
  2. //str不为Null,则返回字符串长度,否则返回-1
  3. val len = str?.length ?: -1

假如?:操作符左边的表达式是非空的,那么?:操作符就返回左边表达式的值, 否则就返回右边的内容。并且,仅在左侧值为Null时,右侧表达式才会进行计算。

因为throwreturn也是表达式,所以都可以使用在?:操作符的右侧,这在函数头部检查参数的合法性时,很有用。

  1. fun foo(node: Node): String? {
  2. //若node.getParent()为Null,那么函数直接返回Null
  3. val parent = node.getParent() ?: return null
  4. //若node.getName()为Null,那么函数直接抛出异常
  5. val name = node.getName() ?: throw IllegalArgumentException("name expected")
  6. // ...
  7. }

2.!!操作符
如果你想要强制访问在一个可空变量的属性,那可以通过!!操作符来操作。但是这可能导致空指针异常,请谨慎使用!

  1. val str: String? = "abc"
  2. //str不为Null,则返回字符串长度,否则抛出KotlinNullPointerException异常
  3. val len = str!!.length

字符串

和Java中一样,kotlin中的字符串也是不可变的,但是我们可以直接通过索引运算符[]来访问字符串中的字符。

在kotlin中,支持两种类型的字符串字面值:

  1. val str = "Hello, world!\nLeon"
  1. val text = """
  2. for (c in "foo")
  3. print(c)
  4. """

字符串模板

kotlin中的两种字符串字面量都可以包含模板表达式,这里会计算出模板表达式的值,并插入在对应的字符串索引处(这对于在Java中拼接字符串来说简直是福音啊)。所谓模板表达式就是以$开头的表达式,包含两种形式:

  1. val i = 10
  2. val s = "i = $i" // s为"i = 10"
  1. val str = "abc"
  2. //result为 "abc.length is 3"
  3. val result = "$str.length is ${str.length}"

此外,因为原生字符串不支持转义字符\,因此可以使用${'$'}在原生字符串中表示$

函数

在kotlin中,声明函数使用fun关键词。函数的定义可以概括如下:

  1. fun 函数名(参数名 : 参数类型 = 默认参数 , ...) : 返回类型 { 代码块 }
  2. or
  3. fun 函数名(参数名 : 参数类型 = 默认参数 , ...) : [返回类型] = 单个表达式

函数体可以是代码块,此时必须显示指定函数返回值,除非返回类型为Unit);也可以是单个表达式,此时可以省略函数返回值,kotlin可以推断出返回值。

函数参数可以指定默认值,对于这种参数,调用时可以省略该参数值。(很像C++语法)

根据函数的特点,可以分为以下几类:

函数字面量 (Function Literal)

未被声明,直接作为表达式被传递的函数称为函数字面量,包括Lambda表达式和匿名函数。函数字面量经常作为参数传递给高阶函数使用。

Lambda表达式

在Kotlin中,Lambda表达式的格式如下所示:

  1. {参数 : [参数类型] -> 代码块}

若高阶函数的最后一个参数是函数,那么在使用Lambda表达式作为参数值时,可以把Lambda表达式放在参数括号外面(若高阶函数只有一个函数参数,那参数括号就可以完全省略了)。

  1. //高阶函数1
  2. fun lambda1(a: Int, hanshu: (Int) -> Int) {
  3. println(hanshu(a))
  4. }
  5. //调用高阶函数1
  6. lambda1(1) {args -> args * 2}
  7. //假如Lambda表达式只有一个参数,则可以使用默认的参数it
  8. lambda1(1) {it * 2}
  9. //高阶函数2
  10. fun lambda2(hanshu: () -> Int) {
  11. println(hanshu())
  12. }
  13. //调用高阶函数2
  14. lambda2 { 2 }

上面的(Int) -> Int表示函数类型,即一个接收Int类型参数,返回Int类型值的函数。

匿名函数

使用Lambda表达式作为高阶函数参数值存在一个缺点:无法指定函数返回值。虽然多数情况下,编译器可以推断出返回值。但是特殊情况下,若一定需要指定返回值,那么就可以使用匿名函数作为高阶函数参数值。

匿名函数和普通函数只有2点不同
1. 匿名函数没有函数名(废话)
2. 参数类型可以省略(在可以被推断出来的情况下)

当把匿名函数作为高阶函数的参数时,不能写在高阶函数参数括号外面(即:此规则只适用于Lambda表达式)。
因此,针对上面高阶函数的调用,可以写成这样:

  1. lambda1(3, fun(a): Int {
  2. return a * 2
  3. })

除此之外,Lambda表达式和匿名函数在处理return语句时,也存在差异。
在Kotlin中,我们能够通过return语句,直接从命名函数或者匿名函数退出。因此,如果想从Lambda表达式中退出,必须使用带标签的return。例如:

  1. fun innerFun(hanshu: () -> Int) {
  2. println("innerFun")
  3. hanshu()
  4. }
  5. fun foo(): Int {
  6. innerFun { return@innerFun 3 }
  7. println("foo fun")
  8. return 0
  9. }
  10. foo()
  11. 输出:
  12. innerFun
  13. foo fun

那么若我们想从当前函数退出那(即foo函数),理论上应该去掉标签就OK了,但是Kotlin却不允许这么做,这很容易导致误解。
除非调用Lambda表达式的函数是内联函数,即innerFun是内联函数,此时return语句会被内联到foo函数,那么return语句直接从foo函数退出,也就很好理解了。例如:

  1. inline fun innerFun(hanshu: () -> Int) {
  2. println("innerFun")
  3. hanshu()
  4. }
  5. fun foo(): Int {
  6. innerFun { return 3 }
  7. println("foo fun")
  8. return 0
  9. }
  10. foo()
  11. 输出:
  12. innerFun

闭包

闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)。
在kotlin中,函数字面量(Lambda表达式和匿名函数)能够访问它的闭包。并且可以修改闭包中的变量,这点和java不同。例如:

  1. var count = 0
  2. var ints = listOf(1, 2, 3)
  3. ints.filter { it > 0 }.forEach {
  4. count += it
  5. }
  6. println("count = $count")
  7. 输出:
  8. count = 6

指定接收者的函数字面量(Function Literals with Receiver)

指定接收者的函数字面量和类的扩展函数很像,在函数字面量函数体内,可以直接访问Receiver的方法。

  1. //参数sum的类型就是一种Function Literals with Receiver
  2. fun hello(str: String, sum: String.(String) -> Unit) {
  3. str.sum("World")
  4. }
  5. //以Lambda表达式作为参数,此时Receiver是由编辑器推断出来的
  6. hello("Hello") {
  7. //this表示接收者字符串,即"Hello",it表示参数,即"World"
  8. println(this + it + " length = " + (this.length + it.length))
  9. }
  10. //以匿名函数作为参数,此时Receiver是由匿名函数明确指定的
  11. hello("Hello", fun String.(it: String) {
  12. //this表示接收者字符串,即"Hello",it表示参数,即"World"
  13. println(this + it + " length = " + (this.length + it.length))
  14. })
  15. 输出:
  16. HelloWorld length = 10

上述String.(String) -> Unit就是指定接收者的函数字面量的类型,表示只有字符串类型对象才可以调用这个以字符串为参数且无返回值的函数。

在kotlin中,类的定义包含类名、类头和类体。

  1. class 类名 修饰符[public,internal,protected,private] constructor(参数:参数类型){
  2. init{
  3. initializer blocks
  4. }
  5. //二级构造函数,调用了主构造函数
  6. constructor(参数:参数类型) : this(参数) {
  7. parent.children.add(this)
  8. }
  9. 类体
  10. }
  11. 其中,类头和类体都是可选的。

kotlin中的Class有两种构造函数:主构造函数和二级构造函数。上述constructor(参数:参数类型)就指定了主构造函数,它是类头的一部分。

主构造函数不能包含任何代码。任何初始化操作只能在initializer blocks中进行(可以直接引用主构造函数中的参数值)。

二级构造函数通过constructor关键字进行定义,所有的二级构造函数必须直接或者间接的调用主构造函数(通过this关键字)。

我们在java代码中,经常在类中定义多个属性变量,然后在构造函数中分别初始化它们。对于这种行为,kotlin提供了更加简单的方式:

  1. class User(val name: String = 默认参数值, var age: Int = 默认参数值) {
  2. // ...
  3. }

上述User类定了两个成员变量,并在主构造函数中完成初始化。


在kotlin中,一个Class可以包含以下几类成员:
1. Constructors and initializer blocks
2. Properties
3. Functions
4. Nested and Inner Classes
5. Object Declarations

第一项是构造函数和初始化代码块,上面已经介绍过了。下面介绍以下几类成员。

Properties (属性)

类属性就相当于java中的实例成员属性,它的完整定义如下所示:

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

上述的<getter><setter>分别在读取和设置属性时被调用(编译器会默认生成)。val属性只有<getter>函数。
当然,我们可以定制属性的set和get函数,就像定义普通的函数一样。

  1. class Leontli() {
  2. var isEmpty: Boolean = false
  3. get() {
  4. return field
  5. }
  6. set(value) {
  7. field = true
  8. }
  9. }

其中,filed称为backing field,由编译器编译生成,仅仅可在get()和set(value)方法中被访问(若只是重写了get和set方法,而没有主动访问field字段,那么编译器也不会主动生成该字段)。

属性的延迟初始化

在kotlin中,一般情况下,类中的成员属性,必须直接初始化或者在构造函数中初始化。但是有时这是非常不方便的。比如:我们在Activity中经常需要获取各个View的引用,然后进行各种操作。在Java中一般是先定义View变量,然后在onCreate方法中对他们初始化,如下所示:

  1. public class MainActivity extends Activity{
  2. Bubtton mButton;
  3. public void onCreate(Bundle savedInstanceState){
  4. super.OnCreate(savedInstanceState);
  5. setContentView(R.layout.main);
  6. mButton = (Button) findViewById(R.id.ok);
  7. aTextView.setText("Click Me");
  8. // ...
  9. }
  10. }

但是在kotlin中,这样是行不通的,因为非空属性必须被初始化一个非空值。

  1. class MainActivity : Activity() {
  2. var mButton : Button //error : Property must be initialized or be abstract
  3. }

但是如果使用可Null属性,那么后续的操作也会很繁琐,同时也失去了kotlin提供的不可Null属性的便利,可能会导致NPE的频现(又回到了java的老路)。

  1. class MainActivity : Activity() {
  2. var mButton : Button? = null
  3. override fun onCreate(savedInstanceState: Bundle?) {
  4. super.onCreate(savedInstanceState)
  5. setContentView(R.layout.main)
  6. mButton = findViewById(R.id.ok) as Button
  7. //因为mButton可Null,所以只能通过下面的方式来赋值
  8. mButton!!.text = "Click Me"
  9. mButton?.text = "Click Me"
  10. }
  11. }

明明知道mButton不会为Null,但是每次调用都要添加!! or ?操作符,岂不是很不爽!

针对这种情况,kotlin也给出了可选的解决方案Late-Initialized PropertiesDelegated Properties。每种方案各有其使用场景,下面一一介绍。

Late-Initialized Properties(延迟初始化属性)

通过lateinit关键字可以定义延迟初始化属性。例如:

  1. class MainActivity : Activity() {
  2. lateinit var mButton : Button
  3. override fun onCreate(savedInstanceState: Bundle?) {
  4. super.onCreate(savedInstanceState)
  5. setContentView(R.layout.main)
  6. mButton = findViewById(R.id.ok) as Button
  7. //可直接调用
  8. mButton.text = "Click Me"
  9. }
  10. }

这样就可以满足我们的需求了(和java的写法很类似)。但是使用这种属性存在一些条件:
1. 只适用于可变属性,即var修饰的属性
2. 只适用于Class body内的属性,不能是在主构造函数中声明的属性。
3. 延迟初始化属性不能包含自定义的set和get方法
4. 属性不可为Null,且属性类型不能是原始类型。

毕竟存在这些限制,也就意味着在特殊情况下,无法使用延迟初始化属性。(强迫症...)

Delegated Properties (委托属性 or 代理属性)

Delegated Properties为属性的初始化提供了另外一种方案,即把属性委托给一个表达式。其语法如下所示:

  1. val/var <property name>: <Type> by <expression>

by后面的表达式称为属性代理(delegate)。针对这个delegate,存在一些限制规则:

  1. operator fun getValue(receiver: Any?, metadata: KProperty<*>): 被代理属性的类型 {
  2. //返回被代理属性的值
  3. }

其中,关键字operator是必不可少的。参数receiver表示持有被代理属性的类对象,其参数类型必须是被代理属性持有者的类型或者其父类型。metadata表示被代理属性的元数据,其参数类型必须是KProperty<*>或者其父类型。

  1. operator fun setValue(receiver: Any?, metadata: KProperty<*>, value: 被代理属性的类型) {
  2. //设置被代理属性的值
  3. }

其中,receiver和metadata的含义和getValue方法类似,value则表示对被代理属性设置的新值。

这两个函数既可以是类成员函数,也可以是扩展函数。其中,对被代理属性的访问会被委托给delegate的getValue函数,对被代理属性的赋值会被委托给delegate的setValue函数。下面来看一个例子:

  1. //代理类
  2. class Delegate<T>(arg: T) {
  3. var mSave: T = arg //存储被代理属性的值
  4. operator fun getValue(receiver: Any?, metadata: KProperty<*>): T {
  5. println("$receiver, thank you for delegating '${metadata.name}' to me!")
  6. return mSave
  7. }
  8. operator fun setValue(receiver: Any?, metadata: KProperty<*>, value: T) {
  9. println("$value has been assigned to '${metadata.name} in $receiver.'")
  10. mSave = value
  11. }
  12. }
  13. //被代理属性的持有者
  14. class Example {
  15. //把属性p委托给Delegate类对象
  16. var p: String by Delegate<String>("init")
  17. }
  18. 调用
  19. var example = Example();
  20. println(example.p)
  21. example.p = "Hello"
  22. println(example.p)
  23. 输出:
  24. Example@74f2794a, thank you for delegating 'p' to me!
  25. init
  26. Hello has been assigned to 'p in Example@74f2794a.'
  27. Example@74f2794a, thank you for delegating 'p' to me!
  28. Hello

从上述代码中可以看出,访问属性p时,调用了getValue方法,接收者就是Example对象,而通过元数据metadata,则可以访问被代理属性的名称等属性。而对属性p赋值时,调用了setValue方法,value则表示将要被赋值的新值。

上面代码中,我们用mSave属性存储了被代理对象的值,这是因为我们不能在getValue和setValue函数中访问被代理属性p,道理很简单,因为访问被代理属性又会触发getValue和setValue函数,导致死循环。

当然,上面仅仅是一个简单的例子,通过属性代理(delegate),我们可以实现任何对属性的取值和赋值操作。

针对委托属性,kotlin类库提供了一些现成的属性代理(delegate)。这里简单介绍下其中两种。

Lazy

lazy其实是一个接收函数字面量,且返回Lazy对象的高阶函数。Lazy对象是一个delegate,实现了对被代理属性的懒加载(Lazy对象的getValue函数就是通过扩展函数形式来提供的)。只有属性在第一次被访问时,才会执行函数字面量对属性进行赋值。且后续属性值不可变更,即懒加载属性必须是不可变的(val修饰)。看下例子:

  1. val lazyValue: String by lazy {
  2. println("computed!")
  3. "Hello"
  4. }
  5. //调用
  6. println(lazyValue)
  7. println(lazyValue)
  8. //输出
  9. computed!
  10. Hello
  11. Hello
Observable

Delegates是一个Standard property delegates。它提供了observablevetoable函数,分别用来生成不同的属性代理。它们的函数签名如下所示:

  1. //接收一个初始值和一个函数(包含属性、旧值和新值共3个参数),返回一个ObservableProperty属性代理对象
  2. public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit) : ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
  3. //重写了afterChange函数,在被代理属性被修改后,才会被调用。
  4. override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
  5. }
  6. //接收一个初始值和一个函数(包含属性、旧值和新值共3个参数),返回一个ObservableProperty属性代理对象
  7. public inline fun <T> vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean) : ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
  8. //重写了beforeChange函数,在被代理属性被修改前,会被调用,若onChange返回了false,则赋值会被终止掉。
  9. override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onChange(property, oldValue, newValue)
  10. }

前者observable接收的参数函数会在赋值之后被调用,后者vetoable接收的参数函数会在赋值之前被调用,并且有权中断赋值。我们可以想一下,这种控制逻辑应该是在ObservableProperty属性代理的setValue方法中实现的。果不其然:

  1. public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
  2. val oldValue = this.value
  3. //真正赋值之前的调用,若beforeChange返回了false,则中断赋值
  4. if (!beforeChange(property, oldValue, value)) {
  5. return
  6. }
  7. this.value = value //真正赋值
  8. //赋值之后的调用,若beforeChange返回了false,则不会走到这里
  9. afterChange(property, oldValue, value)
  10. }

但是总感觉有些遗憾,为什么没有函数既可以在赋值前被调用,又可以在赋值后被调用那?其实这也是可以理解的,若是beforeChange函数返回了false,那么afterChange便不会被调用;若是beforeChange函数返回了true,那么afterChange的参数和前者完全是一样的,就没啥意义了。

但是,出于练习目的,我们完全可以实现这种函数,基本思路就是为Delegates类提供扩展函数,返回同时实现了beforeChangeafterChange的ObservableProperty属性代理对象。

  1. //initialValue是初始值,onBefore在赋值前被调用,onAfter在赋值后被调用
  2. inline fun <T> Delegates.all(initialValue: T,
  3. crossinline onBefore: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean,
  4. crossinline onAfter: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
  5. ReadWriteProperty<Any?, T> = object : ObservableProperty<T>(initialValue) {
  6. //重写了两个函数
  7. override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onBefore(property, oldValue, newValue)
  8. override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onAfter(property, oldValue, newValue)
  9. }

上面我们通过扩展函数all,综合了observablevetoable的功能。传入的参数onBefore会在赋值前被调用,onAfter会在赋值后被调用(赋值不被中断的前提下)。下面看下具体使用:

  1. class Example {
  2. //isPass也是被代理属性,在使用前必须先赋值
  3. var isPass: Boolean by Delegates.notNull<Boolean>()
  4. var name: String by Delegates.all("<no name>", { prop, old, new ->
  5. println("before $old -> $new")
  6. isPass //闭包哈
  7. }) {
  8. prop, old, new ->
  9. println("after $old -> $new")
  10. }
  11. }
  12. //调用
  13. var example = Example();
  14. example.isPass = true
  15. println(example.name)
  16. example.name = "leon"
  17. println(example.name)
  18. example.isPass = false
  19. example.name = "android"
  20. println(example.name)
  21. //输出:
  22. <no name>
  23. before <no name> -> leon
  24. after <no name> -> leon
  25. leon
  26. before leon -> android
  27. //这里赋值被阻断后,after没有执行哈
  28. leon

异常:
Kotlin中没有Checked Exceptions。
see Effective Java, Item 65: Do not ignore exceptions

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注