第 4 章 Kotlin语法基础

第 4 章 Kotlin语法基础

本章主要为大家介绍Kotlin的一些语法,其中包括标识符、关键字、常量、变量、表达式、语句、注释和包等内容。

4.1 标识符和关键字

任何一种计算机语言都离不开标识符和关键字,因此下面将详细介绍Kotlin标识符和关键字。

4.1.1 标识符

标识符就是变量、常量、函数、属性、类、接口和扩展等由程序员指定的名字。构成标识符的字符均有一定的规范,Kotlin语言中标识符的命名规则如下:

  1. 区分大小写:Myname与myname是两个不同的标识符。
  2. 首字符,可以是下划线(_)或字母,但不能是数字。
  3. 除首字符外其他字符,可以是下划线(_)、字母和数字。
  4. 硬关键字(Hard Keywords)不能作为标识符,软关键字(Soft Keywords)、修饰符关键字(Modifier Keywords)在它们的适用场景之外可以作为标识符使用。
  5. 特定标识符field和it。在Kotlin语言中有两个由编译器定义的特定标识符,它们只能在特定场景中使用有特定的作用,而在其他的场景中可以做标识符使用。

提示 field标识符用于属性访问器中,访问属性支持字段;it标识符用于Lambda表达式中,在省略参数列表时作为隐式参数,即不需要声明就可以使用的参数。

例如,身高、identifier、userName、User_Name、_sys_val等为合法的标识符,注意中文“身高”命名的变量是合法的;而2mail、room#、$Name和class为非法的标识符,注意#是非法字符;美元符($)不能构成标识符,这一点与Java不同;而class是硬关键字。

提示 如果一定要使用关键字作为标识符,可以在关键字前后添加反引号(`)。另外,Kotlin语言中字母采用的是双字节Unicode编码1。Unicode叫作统一编码制,它包含了亚洲文字编码,如中文、日文、韩文等字符。

1Unicode是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。

标识符示例如下:

//代码文件:chapter4/src/ch4.1.1.kt
public fun main(args: Array<String>) {

    val `class` = "舞蹈学习班"//class是硬关键字,前后添加反引号(`),可以用于声明标识符
    val `π` = 3.14159        //Unicode编码,可以用于声明标识符
    var 您好 = "世界"         //Unicode编码,可以用于声明标识符
    var public = "共有的"     //public是修饰符关键字,可以用于声明变量标识符
    println(`class`)
    println(π)

    val it = 100             //it是普通标识符 ①
    val ary = arrayListOf<String>("A", "B", "C") //创建一个数组
    ary.forEach { println(it) }  //it特定标识符   ②
}

其中class是关键字,事实上反引号(`)不是标识符的一部分,它也可以用于其他标识符,如π和`π`是等价的。代码第①行和第②行都使用it标识符,代码第①行的it标识符是普通标识符,是由程序员定义的,而代码第②行的it标识符是由编译器定义的,forEach { println(it) }中的{ println(it) }是一个Lambda表达式,it参数引用数组中元素。

4.1.2 关键字

关键字是类似于标识符的保留字符序列,由语言本身定义好的,Kotlin语言中有70多个关键字,全部是小写英文字母,以及!和?等字符构成。分为3个大类:

  1. 硬关键字(Hard Keywords),硬关键字如何情况下都不能作为关键字,具体包括如下关键字。

    as、as?、break、class、continue、do、else、false、for、fun、if、in、!in、interface、is、!is、null、object、package、return、super、this、throw、true、try、typealias、val、var、when和while。

  2. 软关键字(Soft Keywords),软关键字是在它适用场景中不能作为标识符,而其他场景中可以作为标识符,具体包括如下关键字。

    by、catch、constructor、delegate、dynamic、field、file、finally、get、import、init、param、property、receiver、set、setparam和where。

  3. 修饰符关键字(Modifier Keywords),修饰符关键字是一种特殊的软关键字,它们用来修饰函数、类、接口、参数和属性等内容,在此场景中不能作为标识符。而其他场景中可以作为标识符,具体包括如下关键字。

    abstract、annotation、companion、const、crossinline、data、enum、external、final、infix、inner、internal、lateinit、noinline、open、operator、out、override、private、protected、public、reified、sealed、suspend、tailrec和vararg。

4.2 常量和变量

上一章中介绍了如何使用编写一个Kotlin小程序,其中就用到了变量。常量和变量是构成表达式的重要组成部分。

4.2.1 变量

在Kotlin中声明变量,就是在标识符的前面加上关键字var,示例代码如下:

//代码文件:chapter4/src/ch4.2.1.kt

var _Hello = "HelloWorld"                //声明顶层变量   ①

public fun main(args: Array<String>) {
    _Hello = "Hello, World"
    var scoreForStudent: Float = 0.0f    ②
    var y = 20                           ③
    y = true   //编译错误                ④
}

代码第①行、第②行和第③行分别声明了三个变量。第①行是顶层变量。代码第②行在声明变量的同时指定数据类型是Float。代码第③行声明变量时,没有指定数据类型,Kotlin编译器会根据上下文环境自动推导出来变量的数据类型,例如变量y由于被赋值为20,20默认是Int类型,所以y变量被推导为Int类型,所以试图给y赋值true(布尔值)时,会发编译错误。

4.2.2 常量和只读变量

常量和只读变量一旦初始化后就不能再被修改。在Kotlin声明常量是在标识符的前面加上val或const val关键字,它们的区别如下。

  • val声明的是运行期常量,常量是在运行时初始化的。
  • const val声明的是编译期常量,常量是在编译时初始化,只能用于顶层常量声明或声明对象中的常量声明,而且只能是String或基本数据类型(整数、浮点等)。

给Java程序员的提示 编译期常量(const val)相当于Java中public final static所修饰的常量。而运行期常量(val)相当于Java中final所修饰的常量。

示例代码如下:

//代码文件:chapter4/src/ch4.2.2.kt
const val MAX_COUNT = 1000                     //声明顶层常量                  ①

const val _Hello1 = "Hello,world"             //声明顶层常量                  ②
const val _Hello2 = StringBuilder("HelloWorld")//编译错误                      ③

//声明对象
object UserDAO {
    const val MAX_COUNT = 100                  //声明对象中的声明常量          ④
}

public fun main(args: Array<String>) {
    _Hello1 = "Hello, World"  //编译错误       ⑤
    val scoreForStudent: Float = 0.0f          ⑥
    val y = 20                                 ⑦
    y = 30                 //编译错误          ⑧
    const val x = 10         //编译错误        ⑨
}

代码第①行和第②行分别声明了两个顶层常量,它们都是运行期常量。代码第③行业试图声明StringBuilder类型的运行期顶层常量,但是这里会发生编译错误,因为运行期顶层常量只能是String或基本数据类型。代码第④行是在对象中声明常量,object UserDAO{}是对象声明,有关对象声明将在第11章详细介绍,这里不再赘述。代码第⑨行试图在函数中运行期常量,会发生编译错误,因为运行期常量用于顶层常量或对象中常量声明。

代码第⑤行和第⑧行会发生编译错误,因为这里试图修改_Hello1常量值。代码第⑥行和第⑦行是声明运行时常量。当然,运行期常量也可以声明为顶层的。

约定 常量其实就是只读变量,编译期常量(const val)是更为彻底的常量,一旦编译之后就不能再修改了。而运行期常量(val)还可以根据程序的运行情况初始化。为了描述方便,本书将运行期常量称为“只读变量”。默认所说的常量是编译期常量。

4.2.3 使用var还是val?

在开发过程中,有的时选择var还是val都能满足需求,那么选择哪一个更好呢?例如:可以将count变量声明为var或val。

let count = 3.14159
var count = 3.14159

原则 如果两种方式都能满足需求情况下,原则上优先考虑使用val声明。因为一方面val声明的变量是只读,一旦初始化后不能修改,这可以避免程序运行过程中错误地修改变量内容;另一方面在声明引用类型使用val,对象的引用不会被修改,但是引用内容可以修改,这样会更加安全,也符合函数式编程的技术要求。

val声明的引用类型示例代码如下:

//代码文件:chapter4/src/ch4.2.3.kt

class Person(val name: String, val age: Int)           ①

public fun main(args: Array<String>) {

    val p1 = Person("Tony", 18)                   ②

    println(p1.name)
    println(p1.age)

    //p1 = Person("Tom", 20) //编译错误           ③
}

上述代码第①行定义了一个Person类,代码第②行是实例化Person类,实例化p1声明为val类型,不能改变p1的引用,代码第③行试图改变p1的引用,则会有编译错误,但是如果p1被声明为var的,则代码第③行可以编译通过。

4.3 注释

Kotlin程序有3类注释:单行注释(//)、多行注释(/*...*/)和文档注释(/**...*/)。注释方法与Java语言都类似,下面介绍一下单行注释和多行注释,文档注释将在第5章详细介绍。

  1. 单行注释

    单行注释可以注释整行或者一行中的一部分,一般不用于连续多行的注释文本;当然,它也可以用来注释连续多行的代码段。以下是两种注释风格的例子:

    if (x > 1)  {
    // 注释1
    } else {
    // 注释2
    }
    
    // if (x > 1)  {
    //    // 注释1
    // } else {
    //    // 注释2
    // }
    
    

    提示 在IntelliJ IDEA中对连续多行的注释文本可以使用快捷键:选择多行然后按住“Ctrl+ 斜杠”组合键进行注释。去掉注释也是按住“Ctrl+ 斜杠”组合键。

  2. 块注释

    一般用于连续多行的注释文本,但也可以对单行进行注释。以下是几种注释风格的例子:

    if (x > 1) {
        /* 注释1 */
    } else {
        /* 注释2 */
    }
    
    /*
    if (x > 1) {
    
    } else {
    
    }
    */
    
    /*
    if (x > 1)  {
    /* 注释1 */       ①
    } else {
    /* 注释2 */       ②
    }
    */
    
    

    Kotlin块注释可以嵌套,见代码第①行和第②行实现了块注释嵌套。

    提示 在IntelliJ IDEA中添加块注释可以快捷键是“Ctrl+Shift+斜杠”组合键,相反操作去掉块注释也是按住“Ctrl+Shift+斜杠”组合键。

    在程序代码中,对容易引起误解的代码进行注释是必要的,但应避免对已清晰表达信息的代码进行注释。需要注意的是,频繁的注释有时反映了代码的低质量。当觉得被迫要加注释的时候,不妨考虑一下重写代码使其更清晰。

4.4 语句与表达式

Kotlin代码是由关键字、标识符、语句和表达式等内容构成,语句和表达式是代码的重要组成部分。

4.4.1 语句

语句关注的代码执行过程,如for、while和do-while等。在Kotlin语言中,一条语句结束后可以不加分号,也可以加分号,但是有一种情况必须加分号,那就是多条语句写在一行的时候,需要通过分号来区别语句:

var a1: Int = 10; var a2: Int = 20;

4.4.2 表达式

表达式是一般位于赋值符(=)的右边,并且会返回明确的结果。下列代码中10和20是最简单形式的表达式。

var a1 = 10
val a2 = 20

在上述代码中,直接将表达式(10和20)赋值给变量或常量,并没有指定数据类型,这因为在Kotlin编译器可以根据上下文自动推断数据类型。上述代码也可以指定数据类型。

var a1: Int = 10
val a2: Int = 20

在上述代码中在变量标识符后面“冒号+数据类型”是为变量或常量指定数据类型,本例中是指定a1和a2为Int类型。

提示 原则上在声明变量或常量时不指定数据类型,因为这样可使代码变得简洁,但有时需要指定特殊的数据类型除外,例如var a3: Long = 10。另外,语句结束后的分号(;)不是必须情况下也不要加。

为了使代码更加简洁Kotlin将Java中一些语句进行简化,使之成为一种表达式,这些表达式包括:控制结构表达式、try表达式、表达式函数体和对象表达式。示例代码如下:

//代码文件:chapter4/src/ch4.4.2.kt

public fun main(args: Array<String>) {

    val englishScore = 95
    val chineseScore = 98

    //if控制结构表达式
    val result = if (englishScore < 60) "不及格" else "及格"        ①
    println(result)

    val totalScore = sum(englishScore, chineseScore)                     ②
    println(totalScore)

    //try表达式
    val score = try {             ③
        //TODO
    } catch (e: Exception) {
        return
    }
}

fun sum(a: Int, b: Int): Int = a + b    //表达式函数体        ④

上述代码第①行赋值使用了if控制结构表达式,在Kotlin中除了for、do和do-while等控制结构是语句外,大多数控制结构都是表达式,如when等。

代码第②行是调用代码第④行声明的sum函数,在sum函数中函数体,即a + b表达式没有放到一对大括号中,而是直接赋值给函数,这就是“表达式函数体”,表达式函数体会在10.2节详细介绍。

代码第③行是调用try表达式,try是用来捕获异常的,有关使用try表达式具体细节会在第18章详细介绍。

4.5 包

在程序代码中给类或函数起一个名字是非常重要的,但是有时候会出现非常尴尬的事情,名字会发生冲突,例如:项目中自定义了一个日期类,取名为Date,但是你可能会发现Kotlin核心库中还有多个Date,例如:位于java.util包和java.sql包中。

4.5.1 包作用

在Kotlin与Java一样为了防止类、接口、枚举、注释和函数等内容命名冲突引用了包(package)概念,包本质上命名空间(namespace)2。在包中可以定义一组相关的内容(类、接口、枚举、注释和函数),并为它们提供访问保护和命名空间管理。

2命名空间,也称名字空间、名称空间等,它表示着一个标识符(identifier)的可见范围。一个标识符可在多个命名空间中定义,它在不同命名空间中的含义是互不相干的。这样,在一个新的命名空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的定义都处于其他命名空间中。 ——引自于 维基百科 https://zh.wikipedia.org/wiki/命名空间

在前面提到的Date类名称冲突问题,很好解决,将不同Date类放到不同的包中,自定义Date类可以放到自己定义的com.a51work6包中,这样就不会与java.util包和java.sql包中Date发生冲突问题了。

4.5.2 包定义

Kotlin中使用package语句定义包,package语句应该放在源文件的第一行,在每个源文件中只能有一个包定义语句。定义包语法格式如下:

package pkg1[.pkg2[.pkg3…]]

pkg1~ pkg3都是组成包名一部分,之间用点(.)连接,它们命名应该是合法的标识符,其次应该遵守Kotlin包命名规范,即全部小写字母。

定义包示例代码如下:

//代码文件:chapter4/src/com/a51work6/Date.kt
package com.a51work6

//定义Date类
class Date {

    override fun toString(): String {
        return "公元2028年8月8日8时8分8秒"
    }
}
//定义函数
fun add(a: Int, b: Int): Int = a + b    //表达式函数体

com.a51work6是自定义的包名,包名一般是公司域名的倒置。

提示 我们公司的域名是51work6.com,倒置后是com.51work6,其中51work6是非法标识符(不能用数字开头),所以com.51work6包名是非法的,于是将包名改为com.a51work6。

如果在源文件中没有定义包,那么文件中定义的类、接口、枚举、注释和函数等内容将会被放进一个无名的包中,也称为默认包。

定义好包后,包采用层次结构管理这些内容,如图4-1所示是在IntelliJ IDEA文件视图中查看包,可见有默认包和com.a51work6包。如果Windows资源管理器中查看这些包,会发现如图4-2所示的层次结构,<源文件目录>是根目录,也是默认包目录,可见其中有多个.kt文件。com是文件夹,a51work6子文件夹,在a51work6中包含一个Date.kt文件。Kotlin编译器把包对应于操作系统的文件目录来管理,不仅是源文件,编译之后的字节码文件也采用操作系统的文件目录管理的。

{%}

图4-1 IntelliJ IDEA文件视图中查看包

{%}

图4-2 Windows资源管理器中查看包

4.5.3 包引入

为了能够使用一个包中内容(类、接口、枚举、注释和函数),需要在Kotlin程序中明确引入该包。使用import语句引入包,import语句应位于package语句之后,所有类的声明之前,可以有0~n条import语句,其语法格式为:

import package1[.package2…].(内容名|*)

“包名.内容名”形式只引入具体特定内容名,“包名.*”采用通配符,表示引入这个包下所有的内容。但从编程规范的角度提倡明确引入特定内容名,即“包名.内容名”形式可以提高程序的可读性。

如果需要在程序代码中使用com.a51work6包中Date类。示例代码如下:

//代码文件:chapter4/src/ch4.5.kt

import com.a51work6.Date     //引入该包下Date类    ①
import com.a51work6.add      //引入该包下add函数   ②
//import com.a51work6.*      //引入该包下所有内容  ③
//import java.util.Date      //引入该包下Date类    ④

public fun main(args: Array<String>) {

    val date = Date()               ⑤
    System.out.println(date)
    val now = java.util.Date()      ⑥
    println(now)

    val totalScore = add(100, 97)
    println(totalScore)
}

上述代码第①行是引入com.a51work6中的Date类,代码第②行是引入com.a51work6中的add函数,而代码第③行是引入com.a51work6中的所有内容,它可以替代上面的两条import语句。

提示 代码第⑤行和第⑥行中Date类来自于不同的包,如果这两包个都引入,见代码第①行和第④行,则会发生编译错误。为避免这个编译错误,只能明确引入一个包,另一个不能引入,当使用该包中的内容是,需要指定“全名”,即“包名+内容名”,见代码第⑥行中的java.util.Date。

本章小结

本章主要介绍了Kotlin语言中最基本的语法,首先介绍了标识符和关键字,读者需要掌握标识符构成,了解Kotlin关键字的分类。然后介绍了Kotlin中的变量和常量,注意var、val和const val的区别。再然后介绍了注释,注释分为单行注释和块注释。接着介绍了语句和表达式,注意Kotlin中多种形式的表达式。最后介绍了包,其中理解包的作用,熟悉包的定义和引入。

目录

  • 目录
  • 简介
  • 前言
  • 第 1 章 开篇综述
  • 第 2 章 开发环境搭建
  • 第 3 章 第一个Kotlin程序
  • 第 4 章 Kotlin语法基础
  • 第 5 章 Kotlin编码规范
  • 第 6 章 数据类型
  • 第 7 章 字符串
  • 第 8 章 运算符
  • 第 9 章 程序流程控制
  • 第 10 章 函数
  • 第 11 章 面向对象编程
  • 第 12 章 继承与多态
  • 第 13 章 抽象类与接口
  • 第 14 章 函数式编程基石——高阶函数和Lambda表达式
  • 第 15 章 泛型
  • 第 16 章 数据容器——数组和集合
  • 第 17 章 Kotlin中函数式编程API
  • 第 18 章 异常处理
  • 第 19 章 线程
  • 第 20 章 协程
  • 第 21 章 Kotlin与Java混合编程
  • 第 22 章 Kotlin I/O与文件管理
  • 第 23 章 网络编程
  • 第 24 章 Kotlin与Java Swing图形用户界面编程
  • 第 25 章 轻量级SQL框架——Exposed
  • 第 26 章 反射
  • 第 27 章 注解
  • 第 28 章 项目实战1:开发PetStore宠物商店项目
  • 第 29 章 项目实战2:开发Kotlin版QQ2006聊天工具