Android 10 Kotlin编程通俗演义
上QQ阅读APP看书,第一时间看更新

1.4 新式语法特征

下面是对新式语法共有特征的一个总结,虽然并不全面,但是用于快速理解和学习是没有问题的。

(1)不需要分号。

只有想在同一行内放多条语句时,才需要用分号来分隔。

(2)使用明确的关键字定义方法,而不是根据语法来辨别。

比如:“fun sum(a: Int, b: Int):Int { return a+b}”,Kotlin使用“fun”关键字定义函数和方法。

(3)使用明确的关键字定义变量,而不是根据语法来辨别,并且对常量(也可叫只读变量)和变量用关键字进行明确的区分:

  • var表示定义变量,比如:“var param1: Int = 12”。
  • val表示定义常量,比如:“val param2: String? = null”。

(4)方法或函数的返回值类型放在后面,使用“:”分隔。

比如:“fun onOptions(): Boolean {return true}”,其中Boolean是函数的返回值类型。

(5)变量和常量的类型放在后面,使用“:”分隔。

比如:“var param1: Int = 12”,其中“Int”是变量类型。

(6)不再完全忠诚于面向对象。

支持全局函数。既可以在类外面定义函数,也可以把函数保存在变量中,还可以定义函数类型,跟C/C++一样。

(7)支持Lambda表达式,使语法精简再精简。

可以发挥想象力,试着写出最少的代码,只要编译器能把它识别出来即可。

(8)定义变量时可省略类型。

前提是编译器可以根据其他内容推导出来,如果推导不出来,就不能省略。

(9)可以在字符串中嵌入表达式,比字符串的格式化函数更方便。

比如Java中这样输出格式化字符串:

     String strName = "老王";
     String str = String.format("Hi,%s",strName);

而在Kotlin中这样写即可:

     val strName = "老王"
     val str = String.format("Hi,${strName}")

在Kotlin中,以“${}”的形式嵌入表达式。

(10)将可为空和不可为空作为两种类型对待,赋值时需要类型转换。

可空类型与不可空类型以“?”来区分。

这样做主要是为了消除“空指针异常”。编译器无法自动消除,“空指针异常”但会时刻提醒“这个变量是不能为空的,不要给它赋空值……”,最终还是靠人去保证避免空指针异常。

比如定义非空变量:

     var strName1:String = "老王"
     strName1 = null

定义非空变量后,立即把它的值改成null,会导致编译错误:“Null can not be a value of a non-null type String”,意思是说“null不能作为非null类型的值”。所以,要保证赋给非空类型变量的值不为null。

可以向可空变量赋任何值,比如:

     var strName2:String? = null
     strName2 = "老空"

类型后面加上问号后,就可以放空值了,但由于“String”与“String?”不是同一类型,因此在两种类型之间赋值时需要进行转换,比如:

     var strName1:String = "老王"
     var strName2:String? = "老空"
     strName1 = strName2

第三句是将可为空的变量值赋给不可为空的变量,此时编译器会报错误“Type mismatch:inferred type is String? but String was expected”,意思是“类型不匹配:推导出的类型是String?,但是期望的类型是String”,也就是说期望strName2是String类型而不是String?类型。这时就需要明确的类型转换了,比如:

     strName1 = strName2!!

两个叹号用于把可为空类型转成不可空类型。使用两个叹号就是为了警示开发者:“要考虑一下,strName2中的值能确保不为null吗?”此时可能有人要问:“strName2虽然是可为空类型,但其值不是空啊,我都看到了!”你的确看到了,但是编译器看不到,因为一般情况下这些语句不会靠得这么近,比如strName1和strName2可以是类的字段,而“strName1=strName2”这一句在类的某个方法中,此种情况下编译器无法知道strName2的值是否为null(是运行时才能确定的)。

也可以先判断strName2是否为null,只有在它不为null时才赋值给strName1,比如:

     var strName1:String = "老王"
     var strName2:String? = "老空"
     if (strName2 != null) {
         strName1 = strName2
     }

注意,strName2的两个叹号被省略了。为什么可以省略呢?因为判断条件为True时,strName2的值必然不是null,所以编译器就自动将它转成了非空类型。

(11)具有表示范围的语法。

比如:“for (i in 1..4 step 2)”,用“..”表示范围。

(12)所有类型都是对象,不存在基础类型,或者说所有类型都在箱子里。

没有Java中的int、long、char等,只有Int、Long、Char等。也就是说,可以这样写代码:

     110.equals(33)
     "老李".get(1)

(13)增加“===”操作符,用于确定两个变量是不是引用同一个对象。

“==”比较两个对象的值是否相等,相当于调用equals(),而“===”相当于C语言中的直接比较指针。

(14)显式类型转换。

编译器一般不帮我们自动转换类型,因为开发者应该明确知道每一个转换的后果,所以大多数情况下都需要开发者自己进行类型转换。

(15)创建对象时不再用“new”,而是直接调用构造方法。

这个就不用解释了,原因很简单:可以少打字。

(16)支持if ... else ...表达式。

也就是说if语句可以有返回值,其返回值就是子语句中最后一句的值。比如:

max会保存if表达式的值,如果a大于b,max就等于a,否则max就等于b。要实现同样的效果,Java就要写得复杂一点。另外,有了这样的语法,就不必支持三目运算符了,比如“a > b ? c : d”的效果可以用Kotlin实现:“if( a > b) c else d”。

(17)用when代替switch...case。

when比switch...case简洁一些,比如:

每个判断不需带“case”关键字,每个子语句中也不用写break。注意,else对应switch...case中的default。

when比switch...case强大得多,switch...case只能比较是否相等,而when还可以判断目标变量的值是否在一个范围内,比如:

when后也可以不带目标变量,此时它判断每个case是否为真,比如:

     val a = 100
     when {
         a.isOdd() -> print("a是奇数")
         a.isEven() -> print("a是偶数")
         else -> print("a是啥?")
     }

也就是说,“->”前面的部分为真时,其子语句就会被执行。

when语句也可以像if语句那样作为表达式。

(18)在类中定义的成员变量其实是属性,而不是字段。

比如:

     class Message{
          var title:String? = null
          var content:String? = null
          var timestamp:Long = 0
     }

此类有三个属性,既然叫属性,也就是说它们对应Java的getter和setter方法。当然,也可以定制getter和setter。注意,在getter和setter中不能再访问属性(比如不能在title的getter或setter代码中使用title),这样会引起无限递归调用,那要使用这个对应属性的值时怎么办呢?每个属性都有一个不可见的字段存储属性的值,这个字段可以用“field”访问,比如:

可以看到getter或setter不需要定制时可以省略。

(19)有默认构造方法。

默认构造方法是直接在类名后加小括号来定义,比如:

     class Message(var title: String="通知", var content: String?){
          var timestamp:Long = 0
     }

title和content不仅仅是默认构造方法的参数,同时也是类的属性。类的构造方法没有方法主体(body),如果需要定制其内部的程序逻辑怎么办呢?很简单,实现init代码块,示例如下:

(20)非默认构造方法必须调用默认构造方法。

非默认构造器不再与类名同名,其名字固定,叫作constructor。可以有多个constructor方法,它们之间以不同的参数来区分。

对默认构造器,可以直接调用,也可以间接调用,总之得调用一下。例如:

非默认构造方法调用默认构造方法的语法有点像类的继承。

(21)枚举也是类。

枚举类中的每个枚举都是这个类的一个实例,在定义类的同时把实例也定义出来,并且不能再通过类创建新的实例,也就是说其实例的数量是固定的。看下面的例子:

这个跟我们常见的枚举没有太大区别,但它是类,所以可以带属性和方法,比如:

此枚举带有一个属性rgb,所以在定义枚举实例(RED、GREEN、BLUE)时,为它们的构造方法传入了参数。

(22)属性也可以被覆盖(Override)。

属性的本质是函数,当然可以被覆盖。

(23)接口中可以定义属性。

属性的本质是函数,当然可以定义属性,但是属性是抽象的,子类必须覆盖(Override)这个属性。

(24)可以在不继承类的情况下为类添加方法。

这叫“扩展”。属性的本质是方法,支持扩展,但是扩展出来的属性没有对应的字段,所以只能实现getter方法。它只能是val,不能赋初始值。

此特性一般用在别人写的类上。对于自己写的类,想怎么改就怎么改,没有必要用扩展。

(25)类型转换使用“as”关键字。

看一个例子:

     val a:String = Color.RED as String

这里借用了前面定义的枚举。注意,这个转换会返回null,因为两种类型不匹配。

(26)当连续调用某个对象的多个方法时,可以让对象只出现一次。

Kotlin的做法是放在“with”开头的代码块中,看以下示例:

     with(fab){
        clearCustomSize()
        setCompatElevationResource(100)
        show()
     }

fab是一个对象,大括号中三个方法都是它的成员函数,这种方式就相当于如下代码:

     fab.clearCustomSize()
     fab.setCompatElevationResource(100)
     fab.show()

很明显,就是为了少打点字,但有时也少不了太多。

(27)支持全局函数。