带你读《Kotlin核心编程》之二:基础语法

简介: 本书不是一本简单介绍Kotlin语法应用的图书,而是一部专注于帮助读者深入理解Kotlin的设计理念,指导读者实现Kotlin高层次开发的实战型著作。书中深入介绍了Kotlin的核心语言特性、设计模式、函数式编程、异步开发等内容,并以Android和Web两个平台为背景,演示了Kotlin的实战应用。

点击查看第一章
点击查看第三章

第2章

基础语法
在明白Kotlin的设计哲学之后,你可能迫不及待地想要了解它的具体语言特性了。本章我们会介绍Kotlin中最基础的语法和特点,包括:

  • 程序中最基本的操作,如声明变量、定义函数以及字符串操作;
  • 高阶函数的概念,以及函数作为参数和返回值的作用;
  • Lambda表达式语法,以及用它来简化程序表达;
  • 表达式在Kotlin中的特殊设计,以及if、when、try等表达式的用法。

由于这是一门旨在成为更好的Java而被设计出来的语言,我们会在介绍它的某些特性的同时,与Java中相似的语法进行对比,这样可以让你更好地认识Kotlin。好了,我们现在就开始吧。

2.1 不一样的类型声明

当你学习Kotlin时,可能第一个感到与众不同的语法就是声明变量了。在Java中,我们会把类型名放在变量名的前面,如此来声明一个变量。
String a = "I am Java";
Kotlin采用的则是不同的做法,与Java相反,类型名通常在变量名的后面。
val a: String = "I am Kotlin"
为什么要采用这种风格呢?以下是Kotlin官方FAQ的回答:
我们相信这样可以使得代码的可读性更好。同时,这也有利于使用一些良好的语法特性,比如省略类型声明。Scala的经验表明,这不是一个错误的选择。
很好,我们发现Kotlin确实在简洁、优雅的语法表达这一目标上表现得言行一致。同时你也可能注意到了关于“省略类型声明”的描述,这是什么意思呢?

2.1.1 增强的类型推导

类型推导是Kotlin在Java基础上增强的语言特性之一。通俗地理解,编译器可以在不显式声明类型的情况下,自动推导出它所需要的类型。我们来写几个例子:
val string = "I am Kotlin"
val int = 1314
val long = 1314L
val float = 13.14f
val double = 13.34
val double2 = 10.1e6
然后在REPL中打印以上变量的类型,如println(string.javaClass.name),获得的结果如下:
java.lang.String
int
long
float
double
double
类型推导在很大程度上提高了Kotlin这种静态类型语言的开发效率。虽然静态类型的语言有很多的优点,然而在编码过程中却需要书写大量的类型。类型推导则可帮助Kotlin改善这一情况。当我们用Kotlin编写代码时,IDE还会基于类型推导提供更多的提醒信息。
在本书接下来展示的Kotlin代码中,你会经常感受到类型推导的魅力。

2.1.2 声明函数返回值类型

虽然Kotlin在很大程度上支持了类型推导,但这并不意味着我们就可以不声明函数返回值类型了。先来看看Kotlin如何用fun关键字定义一个函数:
fun sum(x: Int, y: Int): Int { return x + y }
与声明变量一样,类型信息放在函数名的后面。现在我们把返回类型声明去掉试试:
image.png
在以上的例子中,因为没有声明返回值的类型,函数会默认被当成返回Unit类型,然而实际上返回的是Int,所以编译就会报错。这种情况下我们必须显式声明返回值类型。
由于一些语言如Java没有Unit类型,你可能不是很熟悉。不要紧,当前你可以暂时把它当作类似Java中的void。不过它们显然是不同的,Unit是一个类型,而void只是一个关键字,我们会在2.4.2节进一步比较两者。
也许你会说,Kotlin看起来并没有比Java强多少嘛,Java也支持某种程度上的类型推导,比如Java 7开始已经支持泛型上的类型推导,Java 10则进一步支持了“局部变量”的类型推导。
其实,Kotlin进一步增强了函数的语法,我们可以把{}去掉,用等号来定义一个函数。
image.png
Kotlin支持这种用单行表达式与等号的语法来定义函数,叫作表达式函数体,作为区分,普通的函数声明则可叫作代码块函数体。如你所见,在使用表达式函数体的情况下我们可以不声明返回值类型,这进一步简化了语法。但别高兴得太早,再来一段递归程序试试看:
image.png

你可能觉察到了if在这里不同寻常的用法—没有return关键字。在Kotlin中,if是一个表达式,它的返回值类型是各个逻辑分支的相同类型或公共父类型。
表达式在Kotlin中占据了非常重要的地位,我们会在2.4节重点介绍这一特性。
我们发现,当前编译器并不能针对递归函数的情况推导类型。由于像Kotlin、Scala这类语言支持子类型和继承,这导致类型系统很难做到所谓的全局类型推导。
关于全局类型推导(global type inference),纯函数语言Haskell是一个典型的代表,它可以在以上的情况下依旧推导出类型。
所以,在一些诸如递归的复杂情况下,即使用表达式定义函数,我们也必须显式声明类型,才能让程序正常工作。
image.png
此外,如果这是一个表达式定义的接口方法,显式声明类型虽然不是必需的,但可以在很大程度上提升代码的可读性。
总结
我们可以根据以下问题的提示,来判断是否需要显式声明类型:

  • 如果它是一个函数的参数?

  必须使用。

  • 如果它是一个非表达式定义的函数?

  除了返回Unit,其他情况必须使用。

  • 如果它是一个递归的函数?

  必须使用。

  • 如果它是一个公有方法的返回值?

为了更好的代码可读性及输出类型的可控性,建议使用。
除上述情况之外,你可以尽量尝试不显式声明类型,直到你遇到下一个特殊情况。

2.2 val和var的使用规则

与Java另一点不同在于,Kotlin声明变量时,引入了val和var的概念。var很容易理解,JavaScript等其他语言也通过该关键字来声明变量,它对应的就是Java中的变量。那么val又代表什么呢?
如果说var代表了varible(变量),那么val可看成value(值)的缩写。但也有人觉得这样并不直观或准确,而是把val解释成varible+final,即通过val声明的变量具有Java中的final关键字的效果,也就是引用不可变。
我们可以在IntelliJ IDEA或Android Studio中查看val语法反编译后转化的Java 代码,从中可以很清楚地发现它是用final实现这一特性的。

2.2.1 val的含义:引用不可变

val的含义虽然简单,但依然会有人迷惑。部分原因在于,不同语言跟val相关的语言特性存在差异,从而容易导致误解。
我们先用val声明一个指向数组的变量,然后尝试对其进行修改。
image.png

因为引用不可变,所以x不能指向另一个数组,但我们可以修改x指向数组的值。
如果你熟悉Swift,自然还会联想到let,于是我们再把上面的代码翻译成Swift的版本。
image.png

这下连引用数组的值都不能修改了,这是为什么呢?
其实根本原因在于两种语言对数组采取了不同的设计。在Swift中,数组可以看成一个值类型,它与变量x的引用一样,存放在栈内存上,是不可变的。而Kotlin这种语言的设计思路,更多考虑数组这种大数据结构的拷贝成本,所以存储在堆内存中。
因此,val声明的变量是只读变量,它的引用不可更改,但并不代表其引用对象也不可变。事实上,我们依然可以修改引用对象的可变成员。如果把数组换成一个Book类的对象,如下编写方式会变得更加直观:
image.png
首先,这里展示了Kotlin中的类不同于Java的构造方法,我们会在第3章中介绍关于它具体的语法。其次,我们发现var和val还可以用来声明一个类的属性,这也是Kotlin中一种非常有个性且有用的语法,你还会在后续的数据类中再次接触到它的应用。

2.2.2 优先使用val来避免副作用

在很多Kotlin的学习资料中,都会传递一个原则:优先使用val来声明变量。这相当正确,但更好的理解可以是:尽可能采用val、不可变对象及纯函数来设计程序。关于纯函数的概念,其实就是没有副作用的函数,具备引用透明性,我们会在第10章专门探讨这些概念。由于后续的内容我们会经常使用副作用来描述程序的设计,所以我们先大概了解一下什么是副作用。
简单来说,副作用就是修改了某处的某些东西,比方说:

  • 修改了外部变量的值。
  • IO操作,如写数据到磁盘。
  • UI操作,如修改了一个按钮的可操作状态。

来看个实际的例子:我们先用var来声明一个变量a,然后在count函数内部对其进行自增操作。
image.png
image.png
在以上代码中,我们会发现多次调用count(1)得到的结果并不相同,显然这是受到了外部变量 a 的影响,这个就是典型的副作用。如果我们把var换成val,然后再执行类似的操作,编译就会报错。
image.png
这就有效避免了之前的情况。当然,这并不意味着用val声明变量后就不能再对该变量进行赋值,事实上,Kotlin也支持我们在一开始不定义val变量的取值,随后再进行赋值。然而,因为引用不可变,val声明的变量只能被赋值一次,且在声明时不能省略变量类型,如下所示:
image.png
不难发现副作用的产生往往与可变数据及共享状态有关,有时候它会使得结果变得难以预测。比如,我们在采用多线程处理高并发的场景,“并发访问”就是一个明显的例子。然而,在Kotlin编程中,我们推荐优先使用val来声明一个本身不可变的变量,这在大部分情况下更具有优势:

  • 这是一种防御性的编码思维模式,更加安全和可靠,因为变量的值永远不会在其他地方被修改(一些框架采用反射技术的情况除外);
  • 不可变的变量意味着更加容易推理,越是复杂的业务逻辑,它的优势就越大。

回到在Java中进行多线程开发的例子,由于Java的变量默认都是可变的,状态共享使得开发工作很容易出错,不可变性则可以在很大程度上避免这一点。当然,我们说过,val只能确保变量引用的不可变,那如何保证引用对象的不可变性?你会在第6章关于只读集合的介绍中发现一种思路。

2.2.3 var的适用场景

一个可能被提及的问题是:既然val这么好,那么为什么Kotlin还要保留var呢?
事实上,从Kotlin诞生的那一刻就决定了必须拥抱var,因为它兼容Java。除此之外,在某些场景使用var确实会起到不错的效果。举个例子,假设我们现在有一个整数列表,然后遍历元素操作后获得计算结果,如下:
image.png
这是我们非常熟悉的做法,以上代码中的res是个局部的可变变量,它与外界没有任何交互,非常安全可控。我们再来尝试用val实现:
image.png
这就有点尴尬了,必须利用递归才能实现,原本非常简单的逻辑现在变得非常不直观。当然,熟悉Kotlin的朋友可能知道List有一个fold方法,可以实现一个更加精简的版本。
image.png
函数式API果然拥有极强的表达能力。
可见,在诸如以上的场合下,用var声明一个局部变量可以让程序的表达显得直接、易于理解。这种例子很多,即使是Kotlin的源码实现,尤其集合类遍历的实现方法,也大量使用了var。之所以采用这种命令式风格,而不是更简洁的函数式实现,一个很大的原因是因为var的方案有更好的性能,占用内存更少。所以,尤其针对数据结构,可能在业务中需要存储大量的数据,所以显然采用var是其更加适合的实现方案。

2.3 高阶函数和Lambda

通过2.1节的介绍,我们发现Kotlin中的函数定义要更加简洁、灵活。这一节我们会介绍关于函数更加高级的特性—高阶函数和Lambda。由于在后续的内容中你要经常跟它们打交道,因此在开始不久我们就要充分地了解它们。
我们说过,Kotlin天然支持了部分函数式特性。函数式语言一个典型的特征就在于函数是头等公民—我们不仅可以像类一样在顶层直接定义一个函数,也可以在一个函数内部定义一个局部函数,如下所示:
image.png
此外,我们还可以直接将函数像普通变量一样传递给另一个函数,或在其他函数内被返回。如何理解这个特性呢?

2.3.1 抽象和高阶函数

《计算机程序的构造和解释》这本经典的书籍的开篇,有一段关于抽象这个概念的描述:
心智的活动,除了尽力产生各种简单的认识外,主要表现在如下3个方面:
1)将若干简单的认识组合为一个复合认识,由此产生出各种复杂的认识;
2)将两个认识放在一起对照,不管它们如何简单或者复杂,在这样做时并不将它们合二为一;由此得到有关它们的相互关系的认识;
3)将有关认识与那些在实际中和它们同在的所有其他认识隔离开,这就是抽象,所有具有普遍性的认识都是这样得到的。
简单地理解,我们会善于对熟悉或重复的事物进行抽象,比如2岁左右的小孩就会开始认知数字1、2、3……之后,我们总结出了一些公共的行为,如对数字做加减、求立方,这被称为过程,它接收的数字是一种数据,然后也可能产生另一种数据。过程也是一种抽象,几乎我们所熟悉的所有高级语言都包含了定义过程的能力,也就是函数。
然而,在我们以往熟悉的编程中,过程限制为只能接收数据为参数,这个无疑限制了进一步抽象的能力。由于我们经常会遇到一些同样的程序设计模式能够用于不同的过程,比如一个包含了正整数的列表,需要对它的元素进行各种转换操作,例如对所有元素都乘以3,或者都除以2。我们就需要提供一种模式,同时接收这个列表及不同的元素操作过程,最终返回一个新的列表。
为了把这种类似的模式描述为相应的概念,我们就需要构造出一种更加高级的过程,表现为:接收一个或多个过程为参数;或者以一个过程作为返回结果。
这个就是所谓的高阶函数,你可以把它理解成“以其他函数作为参数或返回值的函数”。高阶函数是一种更加高级的抽象机制,它极大地增强了语言的表达能力。

2.3.2 实例:函数作为参数的需求

以上关于高阶函数的阐述可能让你对它建立了初步的印象,然而依旧不够清晰。接下来,我们具体看下函数作为参数到底有什么用。需要注意的是,《Java 8实战》通过一个实现filter方法的例子,很好地展现了函数参数化的作用,我们会采用类似的思路,用实际例子来探讨函数作为参数的需求,以及Kotlin相关的语法特性。
Shaw因为旅游喜欢上了地理,然后他建了一个所有国家的数据库。作为一名程序员,他设计了一个CountryApp类对国家数据进行操作。Shaw偏好欧洲的国家,于是他设计了一个程序来获取欧洲的所有国家。
image.png
以上我们用data class声明了一个Country数据类,当前也许你会感觉陌生,我们会在下一章详细介绍这种语法。
后来,Shaw对非洲也产生了兴趣,于是他又改进了上述方法的实现,支持根据具体的洲来筛选国家。
image.png
以上的程序具备了一定的复用性。然而,Shaw的地理知识越来越丰富了,他想对国家的特点做进一步的研究,比如筛选具有一定人口规模的国家,于是代码又变成下面这个样子:
image.png
image.png
新增了一个population的参数来代表人口(单位:万)。Shaw开始感觉到不对劲,如果按照现有的设计,更多的筛选条件会作为方法参数而不断累加,而且业务逻辑也高度耦合。
解决问题的核心在于对filterCountries方法进行解耦,我们能否把所有的筛选逻辑行为都抽象成一个参数呢?传入一个类对象是一种解决方法,我们可以根据不同的筛选需求创建不同的子类,它们都各自实现了一个校验方法。然而,Shaw了解到Kotlin是支持高阶函数的,理论上我们同样可以把筛选的逻辑变成一个方法来传入,这种思路更加简单。
他想要进一步了解这种高级的特性,所以很快就写了一个新的测试类,如代码清单2-1所示。

image.png


调用isBigEuropeanCountry方法就能够判断一个国家是否是一个人口超过1亿的欧洲国家。然而,怎样才能把这个方法变成filterCountries方法的一个参数呢?要实现这一点似乎要先解决以下两个问题:
  • 方法作为参数传入,必须像其他参数一样具备具体的类型信息。
  • 需要把isBigEuropeanCountry的方法引用当作参数传递给filterCountries。

接下来,我们先来研究第1个问题,即Kotlin中的函数类型是怎样的。

2.3.3 函数的类型

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

  • 通过->符号来组织参数类型和返回值类型,左边是参数类型,右边是返回值类型;
  • 必须用一个括号来包裹参数类型;
  • 返回值类型即使是Unit,也必须显式声明。

如果是一个没有参数的函数类型,参数类型部分就用()来表示。
() -> Unit
如果是多个参数的情况,那么我们就需要用逗号来进行分隔,如:
(Int, String) -> Unit
此外,Kotlin还支持为声明参数指定名字,如下所示:
(errCode: Int, errMsg: String) -> Unit
在本书的第5章中我们还会介绍Kotlin中的可空类型,它将支持用一个“?”来表示类似Java 8中Optional类的效果。如果errMsg在某种情况下可空,那么就可以如此声明类型:
(errCode: Int, errMsg: String?) -> Unit
如果该函数类型的变量也是可选的话,我们还可以把整个函数类型变成可选:
((errCode: Int, errMsg: String?) -> Unit)?
这种组合是不是非常有意思?还没完,我们说过,高阶函数还支持返回另一个函数,所以还可以这么做:
(Int) -> ((Int) -> Unit)
这表示传入一个类型为Int的参数,然后返回另一个类型为(Int) -> Unit的函数。简化它的表达,我们可以把后半部分的括号给省略:
(Int) -> Int -> Unit
需要注意的是,以下的函数类型则不同,它表示的是传入一个函数类型的参数,再返回一个Unit。
((Int) -> Int) -> Unit
好了,在学习了Kotlin函数类型知识之后,Shaw便重新定义了filterCountries方法的参数声明。
image.png
那么,下一个问题来了。我们如何才能把代码清单2-1中的isBigEuropeanCountry方法传递给filterCountries呢?直接把isBigEuropeanCountry当参数肯定不行,因为函数名并不是一个表达式,不具有类型信息,它在带上括号、被执行后才存在值。可以看出,我们需要的是一个单纯的方法引用表达式,用它在filterCountries内部来调用参数。下一节我们会具体介绍如何使用这种语法。

2.3.4 方法和成员引用

Kotlin存在一种特殊的语法,通过两个冒号来实现对于某个类的方法进行引用。以上面的代码为例,假如我们有一个CountryTest类的对象实例countryTest,如果要引用它的isBigEuropeanCountry方法,就可以这么写:
countryTest::isBigEuropeanCountry
为什么使用双冒号的语法?
如果你了解C#,会知道它也有类似的方法引用特性,只是语法上不同,是通过点号来实现的。然而,C#的这种方式存在二义性,容易让人混淆方法引用表达式与成员表达式,所以Kotlin采用::(沿袭了Java 8的习惯),能够让我们更加清晰地认识这种语法。
此外,我们还可以直接通过这种语法,来定义一个类的构造方法引用变量。
image.png
可以发现,getBook的类型为(name: String) -> Book。类似的道理,如果我们要引用某个类中的成员变量,如Book类中的name,就可以这样引用:
Book::name
以上创建的Book::name的类型为(Book) -> String。当我们在对Book类对象的集合应用一些函数式API的时候,这会显得格外有用,比如:
image.png
我们会在6.2节再次提到这种应用。
于是,Shaw便使用了方法引用来传递参数,以下的调用果真奏效了。
val countryApp = CountryApp()
val countryTest = CountryTest()
val countries = ……
countryApp.filterCountries(countries, countryTest::isBigEuropeanCountry)
经过重构后的程序显然比之前要优雅许多,程序可以根据任意的筛选需求,调用同一个filterCountries方法来获取国家数据。

2.3.5 匿名函数

再来思考下代码清单2-1的CountryTest类,这仍算不上一种很好的方案。因为每增加一个需求,我们都需要在类中专门写一个新增的筛选方法。然而Shaw的需求很多都是临时性的,不需要被复用。Shaw觉得这样还是比较麻烦,他打算用匿名函数对程序做进一步的优化。
Kotlin支持在缺省函数名的情况下,直接定义一个函数。所以isBigEuropeanCountry方法我们可以直接定义为:
image.png
于是,Shaw直接调用filterCountries,如代码清单2-2所示。

image.png


这一次我们甚至都不需要CountryTest这个类了,代码的简洁性又上了一层楼。Shaw开始意识到Kotlin这门语言的魅力,很快他发现还有一种语法可以让代码更简单,这就是Lambda表达式。

2.3.6 Lambda是语法糖

提到Lambda表达式,也许你听说过所谓的Lambda演算。其实这是两个不同的概念,Lambda演算和图灵机一样,是一种支持理论上完备的形式系统,也是理解函数式编程的理论基础。古老的Lisp语言就是基于Lambda演算系统而来的,在Lisp中,匿名函数是重要的组成部分,它也被叫作Lambda表达式,这就是Lambda表达式名字的由来。所以,相较Lambda演算而言,Lambda表达式是更加简单的概念。你可以把它理解成简化表达后的匿名函数,实质上它就是一种语法糖。
我们先来分析下代码清单2-2中的filterCountries方法的匿名函数,会发现:

  • fun(country:Country)显得比较啰唆,因为编译器会推导类型,所以只需要一个代表变量的country就行了;
  • return关键字也可以省略,这里返回的是一个有值的表达式;
  • 模仿函数类型的语法,我们可以用->把参数和返回值连接在一起。

因此,简化后的表达就变成了这个样子:
countryApp.filterCountries(countries, {

country ->
country.continient == "EU" && country.population > 10000

})
是不是非常简洁?这个就是Lambda表达式,它与匿名函数一样,是一种函数字面量。我们再来讲解下Lambda具体的语法。现在用Lambda的形式来定义一个加法操作:
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
由于支持类型推导,我们可以采用两种方式进行简化:
val sum = { x: Int, y: Int -> x + y }
或者是:
val sum: (Int, Int) -> Int = { x, y -> x + y }
现在来总结下Lambda的语法:

  • 一个Lambda表达式必须通过{}来包裹;
  • 如果Lambda声明了参数部分的类型,且返回值类型支持类型推导,那么Lambda变量就可以省略函数类型声明;
  • 如果Lambda变量声明了函数类型,那么Lambda的参数部分的类型就可以省略。

此外,如果Lambda表达式返回的不是Unit,那么默认最后一行表达式的值类型就是返回值类型,如:
image.png
Lambda看起来似乎很简单。那么再思考一个场景,如果用fun关键字来声明Lambda表达式又会怎么样?如代码清单2-3所示。
image.png

1.单个参数的隐式名称
首先,也许你在it这个关键字上停留了好几秒,然后依旧不明其意。其实它也是Kotlin简化Lambda表达的一种语法糖,叫作单个参数的隐式名称,代表了这个Lambda所接收的单个参数。这里的调用等价于:
listOf(1, 2, 3).forEach { item -> foo(item) }
默认情况下,我们可以直接用it来代表item,而不需要用item->进行声明。
其次,这行代码的结果可能出乎了你的意料,执行后你会发现什么也没有。为什么会这样?这一次,我们必须要借助IDE的帮助了,以下是把foo函数用IDE转化后的Java代码:
image.png
以上是字节码反编译的Java代码,从中我们可以发现Kotlin实现Lambda表达式的机理。
2. Function类型
Kotlin在JVM层设计了Function类型(Function0、Function1……Function22)来兼容Java的Lambda表达式,其中的后缀数字代表了Lambda参数的数量,如以上的foo函数构建的其实是一个无参Lambda,所以对应的接口是Function0,如果有一个参数那么对应的就是Function1。它在源码中是如下定义的:
image.png
可见每个Function类型都有一个invoke方法,稍后会详细介绍这个方法。
设计Function类型的主要目的之一就是要兼容Java,实现在Kotlin中也能调用Java的Lambda。在Java中,实际上并不支持把函数作为参数,而是通过函数式接口来实现这一特性。所以如果我们要把Java的Lambda传给Kotlin,那么它们就必须实现Kotlin的Function接口,在Kotlin中我们则不需要跟它们打交道。在第6章我们会介绍如何在Kotlin中调用Java的函数式接口。
神奇的数字—22
也许你会问一个问题:为什么这里Function类型最大的是Function22?如果Lambda的参数超过了22个,那该怎么办呢?
虽然22个参数已经够多了,然而现实中也许我们真的需要超过22个参数。其实,在Scala的设计中也遵循了22个数字的设计,这似乎已经成了业界的一种惯例。然而,这个22的设计也给Scala开发者带来了不少麻烦。所以,Kotlin在设计的时候便考虑到了这种情况,除了23个常用的Function类型外,还有一个FunctionN。在参数真的超过22个的时候,我们就可以依靠它来解决问题。更多细节可以参考https://github.com/JetBrains/kotlin/blob/master/spec-docs/function-types.md
3. invoke方法
代码清单2-3中的foo函数的返回类型是Function0。这也意味着,如果我们调用了foo(n),那么实质上仅仅是构造了一个Function0对象。这个对象并不等价于我们要调用的过程本身。通过源码可以发现,需要调用Function0的invoke方法才能执行println方法。所以,我们的疑惑也迎刃而解,上述的例子必须如下修改,才能够最终打印出我们想要的结果:
image.png
也许你觉得invoke这种语法显得丑陋,不符合Kotlin简洁表达的设计理念。确实如此,所以我们还可以用熟悉的括号调用来替代invoke,如下所示:
image.png

2.3.7 函数、Lambda和闭包

在你不熟悉Kotlin语法的情况下,很容易对fun声明函数、Lambda表达式的语法产生混淆,因为它们都可以存在花括号。现在我们已经了解了它们具体的语法,可通过以下的总结来更好地区分:

  • fun在没有等号、只有花括号的情况下,是我们最常见的代码块函数体,如果返回非Unit值,必须带return。
  1. foo(x: Int) { print(x) }

fun foo(x: Int, y: Int): Int { return x image.png y }

  • fun带有等号,是单表达式函数体。该情况下可以省略return。
  1. foo(x: Int, y: Int) = x + y

不管是用val还是fun,如果是等号加花括号的语法,那么构建的就是一个Lambda表达式,Lambda的参数在花括号内部声明。所以,如果左侧是fun,那么就是Lambda表达式函数体,也必须通过()或invoke来调用Lambda,如:
image.png
在Kotlin中,你会发现匿名函数体、Lambda(以及局部函数、object表达式)在语法上都存在“{}”,由这对花括号包裹的代码块如果访问了外部环境变量则被称为一个闭包。一个闭包可以被当作参数传递或者直接使用,它可以简单地看成“访问外部环境变量的函数”。Lambda是Kotlin中最常见的闭包形式。
与Java不一样的地方在于,Kotlin中的闭包不仅可以访问外部变量,还能够对其进行修改,就像这样子:
image.png
此外,Kotlin还支持一种自运行的Lambda语法:
image.png
执行以上代码,结果会打印1。

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

我们已经知道,函数参数化是一种十分强大的特性,结合Lambda表达式,能在很大程度上提高语言的抽象和表达能力。接下来,我们再来了解下高阶函数在Kotlin中另一方面的表现,即一个函数返回另一个函数作为结果。
通过之前的介绍,相信你已经能很容易地理解什么是返回一个函数。还记得我们上面使用过的例子吗?
fun foo(x: Int) = { y: Int -> x + y }
表达上非常简洁,其实它也可以等价于:
fun foo(x: Int): (Int) -> Int {

return { y: Int -> x + y }

}
现在有了函数类型信息之后,可以很清晰地发现,执行foo函数之后,会返回另一个类型为(Int) -> Int的函数。
如果你看过一些介绍函数式编程的文章,可能听说过一种叫作“柯里化(Currying)”的语法,其实它就是函数作为返回值的一种典型的应用。
简单来说,柯里化指的是把接收多个参数的函数变换成一系列仅接收单一参数函数的过程,在返回最终结果值之前,前面的函数依次接收单个参数,然后返回下一个新的函数。
拿我们最熟悉的加法举例子,以下是多参数的版本:
fun sum(x: Int, y: Int, z: Int) = x + y + z
sum(1, 2, 3)
如果我们把它按照柯里化的思路重新设计,那么最终就可以实现链式调用:
fun sum(x: Int) = { y: Int ->

{ z: Int -> x + y + z }

}
sum(1)(2)(3)
你会发现,柯里化非常类似“击鼓传花”的游戏,游戏开始时有个暗号,第1个人将暗号进行演绎,紧接着第2个人演绎,依次类推,经过一系列加工之后,最后一个人揭晓谜底。在这个过程中:

  • 开始的暗号就是第1个参数;
  • 下个环节的演绎就是返回的函数;
  • 谜底就是柯里化后最终执行获得的结果。

可见柯里化是一个比较容易理解的概念,那么为什么会有柯里化呢?
柯里化与Lambda演算
我们说过,Lambda演算是函数式语言的理论基础。在严格遵守这套理论的设计中,所有的函数都只能接收最多一个参数。为了解决多参数的问题,Haskell Curry引入了柯里化这种方法。值得一提的是,这种技术也是根据他的名字来命名的—Currying,后续其他语言也以此来称呼它。
说到底,柯里化是为了简化Lambda演算理论中函数接收多参数而出现的,它简化了理论,将多元函数变成了一元。然而,在实际工程中,Kotlin等语言并不存在这种问题,因为它们的函数都可以接收多个参数进行计算。那么,这是否意味着柯里化对我们而言,仅仅只有理论上的研究价值呢?虽然柯里化在工程中并没有大规模的应用,然而在某些情况下确实起到了某种奇效。
在我们之前介绍过的Lambda表达式中,还存在一种特殊的语法。如果一个函数只有一个参数,且该参数为函数类型,那么在调用该函数时,外面的括号就可以省略,就像这样子:
fun omitParentheses(block: () -> Unit) {

block()

}
omitParentheses {

println("parentheses is omitted")

}
此外,如果参数不止一个,且最后一个参数为函数类型时,就可以采用类似柯里化风格的调用:
fun curryingLike(content: String, block: (String) -> Unit) {

block(content)

}
curryingLike("looks like currying style") {

content ->
println(content)

}
// 运行结果
looks like currying style
它等价于以下的的调用方式:
curryingLike("looks like currying style", {

content ->
println(content)

})
实际上,在Scala中我们就是通过柯里化的技术,实现以上简化语法的表达效果的。Kotlin则直接在语言层面提供了这种语法糖,这个确实非常方便。然而需要注意的是,通过柯里化实现的方案,我们还可以分步调用参数,返回值是一个新的函数。
curryingLike("looks like currying style")
// 运行报错
No value passed for parameter 'block'
然而,以上实现的curryingLike函数并不支持这样做,因为它终究只是Kotlin中的一种语法糖而已。它在函数调用形式上近似柯里化的效果,但却并不是柯里化。Kotlin这样设计的目的是让我们采用最直观熟悉的套路,来替代柯里化实现这种简洁的语法。
Scala中的corresponds方法是另一个典型的柯里化应用,用它可以比较两个序列是否在某个比对条件下相同。现在我们依靠Kotlin上面这种特殊的类柯里化语法特性,再来实现一个Kotlin版本。
首先,我们先穿插下Kotlin的另一项新特性—扩展函数,这是Kotlin中十分强大的功能,我们会在第7章中重点介绍。当前我们先简单了解下它的使用,因为corresponds方法需要借助它来实现。
简单来说,Kotlin中的扩展函数允许我们在不修改已有类的前提下,给它增加新的方法。如代码清单2-4所示。
image.png
在这个例子中,类型View被称为接收者类型,this对应的是这个类型所创建的接收者对象。this可以被省略,就像这样子:
fun View.invisible() {

visibility = View.INVISIBLE

}
我们给Android中的View类定义了一个invisible方法,之后View对象就可以直接调用该方法来隐藏视图。
views.forEach { it.invisible() }
回到我们的corresponds方法,基于扩展函数的语法,我们就可以对Array类型增加这个方法。由于Kotlin的特殊语法支持,我们还是采用了定义普通多参数函数的形式。
image.png
然后再用柯里化的风格进行调用,就显得非常直观:
image.png
虽然本节讲述的是函数作为返回值的应用,然而由于Kotlin的特殊语法,我们可以在大部分场景下用它来替代柯里化的方案,显得更加方便。

2.4 面向表达式编程

在本章之前的几节中,我们已经好几次与一个关键字打过交道,这就是“表达式”。现在罗列下我们已经提及的表达式概念:

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

显然,表达式在Kotlin这门语言中处于一个相当重要的地位,这一节我们会着重介绍在Kotlin中如何利用各种表达式来增强程序表达、流程控制的能力。与Java等语言稍显不同的是,Kotlin中的流程控制不再是清一色的普通语句,它们可以返回值,是一些崭新的表达式语句,如if表达式、when表达式、try表达式等。这样的设计自然与表达式自身的特质相关。在了解具体的语法之前,我们先来探究下表达式和普通语句之间的区别。
表达式(expressions)和语句(statements)虽然是很基本的概念,但也经常被混淆和误解。语句很容易理解,我们在一开始学习命令式编程的时候,程序往往是由一个个语句组成的。比如以下这个例子:
fun main(args: Array) {

var a = 1
while (a < 10) {
    println(a)
    a++
}

}
可以看到,该程序依次进行了赋值、循环控制、打印等操作,这些都可以被称为语句。我们再来看看什么是表达式:
表达式可以是一个值、常量、变量、操作符、函数,或它们之间的组合,编程语言对其进行解释和计算,以求产生另一个值。
通俗地理解,表达式就是可以返回值的语句。我们来写几个表达式的例子:
image.png
这些都是非常明显的表达式。以下是Kotlin中更复杂的表达式例子:
image.png
正如我们所言,一些在其他语言中的普通语句,在Kotlin中也可以是表达式。这样设计到底有什么好处呢?

2.4.1 表达式比语句更安全

我们先来写一段Java代码。刚开始我们还是采用熟悉的if语句用法:
void ifStatement(Boolean flag) {

String a = null;
if (flag) {
    a = "dive into kotlin";
}
System.out.println(a.toUpperCase());

}
非常简单的代码,由于if在这里不是一个表达式,所以我们只能够在外部对变量a进行声明。仔细思考一下,这段代码存在潜在的问题:

  • a必须在if语句外部声明,它被初始化为null。这里的if语句的作用就是对a进行赋值,这是一个副作用。在这个例子中,我们忽略了else分支,如果flag的条件判断永远为true,那么程序运行并不会出错;否则,将会出现“java.lang.NullPointerException”的错误,即使程序依旧会编译通过。因此,这种通过语句创建副作用的方式很容易引发bug。
  • 继续思考,现在的逻辑虽然简单,然而如果变量a来自上下文其他更远的地方,那么这种危险会更加容易被忽视。典型的例子就是一段并发控制的程序,业务开发会变得非常不安全。

接下来,我们再来创建一个Kotlin的版本,现在if会被作为表达式来使用:
fun ifExpression(flag: Boolean) {

val a = if (flag) "dive into Kotlin" else ""
println(a.toUpperCase())

}
下面分析Kotlin的版本:

  • 表达式让代码变得更加紧凑了。我们可以把赋值语句与if表达式混合使用,就不存在变量a没有初始值的情况。
  • 在if作为表达式时,else分支也必须被考虑,这很容易理解,因为表达式具备类型信息,最终它的类型就是if、else多个分支类型的相同类型或公共父类型。

可以看出,基于表达式的方案彻底消除了副作用,让程序变得更加安全。当然,这并不是说表达式不会有副作用,实际上我们当然可以用表达式写出带有副作用的语句,就像这样子:
var a = 1
fun foo() = if (a > 0) {

a = 2 // 副作用,a的值变化了
a

} else 0
然而从设计角度而言,语句的作用就是服务于创建副作用的,相比较表达式的目的则是为了创造新值。在函数式编程中,原则上表达式是不允许包含副作用的。
一切皆表达式
撇开Haskell不谈,在一些极力支持函数式编程的语言中,比如Scala和F#,即使它们不是纯函数式语言,也都实现了一个特性,即一切皆表达式。一切皆表达式的设计让开发者在设计业务时,促进了避免创造副作用的逻辑设计,从而让程序变得更加安全。
由于把百分之百兼容Java作为设计目标,Kotlin并没有采纳一切皆表达式的设计,然而它在Java的基础上也在很大程度上增强了这一点。正如另一个接下来要提及的例子,就是Kotlin中的函数。与Java的函数不同,Kotlin中所有的函数调用也都是表达式。

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

之所以不能说Java中的函数调用皆是表达式,是因为存在特例void。众所周知,在Java中如果声明的函数没有返回值,那么它就需要用void来修饰。如:
void foo () {

System.out.println("return nothing");

}
所以foo()就不具有值和类型信息,它就不能算作一个表达式。同时,这与函数式语言中的函数概念也存在冲突,在Kotlin、Scala这些语言中,函数在所有的情况下都具有返回类型,所以它们引入了Unit来替代Java中的void关键字。
void与Void
当你在描述void的时候,需要注意首字母的大小写,因为Java在语言层设计一个Void类。java.lang.Void类似java.lang.Integer,Integer是为了对基本类型int的实例进行装箱操作,Void的设计则是为了对应void。由于void表示没有返回值,所以Void并不能具有实例,它继承自Object。
如何理解Unit?其实它与int一样,都是一种类型,然而它不代表任何信息,用面向对象的术语来描述就是一个单例,它的实例只有一个,可写为()。
那么,Kotlin为什么要引入Unit呢?一个很大的原因是函数式编程侧重于组合,尤其是很多高阶函数,在源码实现的时候都是采用泛型来实现的。然而void在涉及泛型的情况下会存在问题。
我们先来看个例子,Java这门语言并不天然支持函数是头等公民,我们现在来尝试模拟出一种函数类型:
image.png
看上去似乎没什么问题。我们再来改造下,这一次希望重新实现一个print方法。于是,难题出现了,Return的类型用什么来表示呢?可能你会想到void,但Java中是不能这么干的。无奈之下,我们只能把Return换成Void,即Function,由于Void没有实例,则返回一个null。这种做法严格意义上讲,相当丑陋。
Java 8实际解决办法是通过引入Action这种函数式接口来解决问题,比如:

  • Consumer,接收一个输入参数并且无返回的操作。
  • BiConsumer,接收两个输入参数的操作,并且不返回任何结果。
  • ObjDoubleConsumer,接收一个object类型和一个double类型的输入参数,无返回值。
  • ObjIntConsumer,接收一个object类型和一个int类型的输入参数,无返回值。
  • ObjLongConsumer,接收一个object类型和一个long类型的输入参数,无返回值。
  • ……

虽然解决了问题,但这种方案不可避免地创造了大量的重复劳动,所以,最好的解决办法就是引入一个单例类型Unit,除了不代表任何意义的以外,它与其他常规类型并没有什么差别。

2.4.3 复合表达式:更好的表达力

相比语句而言,表达式更倾向于自成一块,避免与上下文共享状态,互相依赖,因此我们可以说它具备更好的隔离性。隔离性意味着杜绝了副作用,因此我们用表达式描述逻辑可以更加安全。此外,表达式通常也具有更好的表达能力。
典型的一个例子就是表达式更容易进行组合。由于每个表达式都具有值,并且也可以将另一个表达式作为组成其自身的一部分,所以我们可以写出一个复合的表达式。举个例子:
image.png

这个程序描述了获取一个HTTP响应结果,然后进行json解码,最终赋值给res变量的过程。它向我们展示了Kotlin如何利用多个表达式组合表达的能力:

  • try在Kotlin中也是一个表达式,try/catch/finally语法的返回值类型由try或catch部分决定,finally不会产生影响;
  • 在Kotlin中,if-else很大程度上代替了传统三元运算符的做法,虽然增加了语法词数量,但是减少了概念,同时更利于阅读;
  • if-else的返回值即try部分的返回值,最终res的值由try或catch部分决定。

Kotlin中的“?:”
虽然Kotlin没有采用三元运算符,然而它存在一个很像的语法“?:”。注意,这里的问号和冒号必须放在一起使用,它被叫作Elvis运算符,或者null合并运算符。由于Kotlin可以用“?”来表示一种类型的可空性,我们可以用“?:”来给一种可空类型的变量指定为空情况下的值,它有点类似Scala中的getOrElse方法。你可以通过以下的例子理解Elvis运算符:
image.png
是不是觉得相当优雅?接下来,我们再来介绍Kotlin中when表达式,它比我们熟悉的switch语句要强大得多。

2.4.4 枚举类和when表达式

本节主要介绍Kotlin中另一种非常强大的表达式—when表达式。在了解它之前,我们先来看看在Kotlin中如何定义枚举结构,然后再使用when结合枚举更好地来设计业务,并介绍when表达式的具体语法。
1.枚举是类
在Kotlin中,枚举是通过一个枚举类来实现的。先来实现一个很简单的例子:
enum class Day {

MON,
TUE,
WEN,
THU,
FRI,
SAT,
SUN

}
与Java中的enum语法大体相似,无非多了一个class关键词,表示它是一个枚举类。不过Kotlin中的枚举类当然没那么简单,由于它是一种类,我们可以猜测它自然应该可以拥有构造参数,以及定义额外的属性和方法。
enum class DayOfWeek(val day: Int) {

MON(1),
TUE(2),
WEN(3),
THU(4),
FRI(5),
SAT(6),
SUN(7)
;  // 如果以下有额外的方法或属性定义,则必须强制加上分号

fun getDayNumber(): Int {
    return day
}

}
需要注意的是,当在枚举类中存在额外的方法或属性定义,则必须强制加上分号,虽然你很可能不会喜欢这个语法。
枚举类“分号”语法的由来
早期枚举类的语法并没有逗号,然而却有点烦琐:
enum class DayOfWeek(val day: Int) {

  MON: DayOfWeek(1)
  TUE: DayOfWeek(2)
  WEN: DayOfWeek(3)
  THU: DayOfWeek(4)
  FRI: DayOfWeek(5)
  SAT: DayOfWeek(6)
  SUN: DayOfWeek(7)

}
每个枚举值都需要通过DayOfWeek(n)来构造,这确实显得多余。理想的情况是我们只需调用MON(1)来表示即可。然而,简化语法后也带来了一些技术上的问题,比如在枚举类源码实现上很难把具体的枚举值与类方法进行区分。解决这个问题有好几种思路,第一种办法就是把每个方法都加上一个注解前缀,例如:
@inject fun getDayNumber(): Int {

  return day

}
但是这样子就与其他的类在语法上显得不一样,破坏了语法的一致性。好吧,那么能不能反过来,给每个枚举类弄个关键词前缀来区分,比如:
entry MON(1)
显然,这样也不好。因为枚举值的数量无法控制,如果数量较多,会显得啰唆。Kotlin最终采用了引入逗号和分号的语法,即通过逗号对每个枚举值进行分隔,这样就可以最终采用一个分号来对额外定义的属性和方法进行隔离。
这确实是相对更合理的设计方案,尤其是加上逗号之后,Kotlin中的枚举类语法跟Java的枚举更相似了,这符合Kotlin的设计原则。
2.用when来代替if-else
在了解如何声明一个枚举类后,我们再来用它设计一个业务逻辑。比如,Shaw给新一周的几天计划了不同的活动,安排如下:

  • 周六打篮球
  • 周日钓鱼
  • 星期五晚上约会
  • 平日里如果天晴就去图书馆看书,不然就在寝室学习

他设计了一段代码,利用一个函数结合本节最开头的枚举类Day来进行表示:
fun schedule(day: Day, sunny: Boolean) = {

if (day == Day.SAT) {
    basketball()
} else if (day == Day.SUN) {
    fishing()
} else if (day == Day.FRI) {
    appointment()
} else {
    if (sunny) {
        library()
    } else {
        study()
    }
}

}
因为存在不少if-else分支,代码显得不够优雅。对Kotlin日渐熟悉的Shaw开始意识到,更好的改进方法就是用when表达式来优化。现在我们来看看修改后的版本:
fun schedule(sunny: Boolean, day: Day) = when (day) {

Day.SAT -> basketball()
Day.SUN -> fishing()
Day.FRI -> appointment()
else -> when {
    sunny -> library()
    else -> study()
}

}
整个函数一下子“瘦身”了很多,由于少了很多语法关键字干扰,代码的可读性也更上了一层楼。
3. when表达式具体语法
我们根据上述这段代码来分析下when表达式的具体语法:
1)一个完整的when表达式类似switch语句,由when关键字开始,用花括号包含多个逻辑分支,每个分支由->连接,不再需要switch的break(这真是一个恼人的关键字),由上到下匹配,一直匹配完为止,否则执行else分支的逻辑,类似switch的default;
2)每个逻辑分支具有返回值,最终整个when表达式的返回类型就是所有分支相同的返回类型,或公共的父类型。在上面的例子中,假设所有活动函数的返回值为Unit,那么编译器就会自动推导出when表达式的类型,即Unit。以下是一个非Unit的例子:
image.png
3)when关键字的参数可以省略,如上述的子when表达式可改成:
when {

sunny -> library()
else -> study()

}
该情况下,分支->的左侧部分需返回布尔值,否则编译会报错,如:
image.png
4)表达式可以组合,所以这是一个典型的when表达式组合的例子。你在Java中很少见过这么长的表达式,但是这在Kotlin中很常见。如果你足够仔细,还会看出这还是一个我们之前提到过的表达式函数体。
可能你会说,这样嵌套子when表达式,层次依旧比较深。要知道when表达式是很灵活的,我们很容易通过如下修改来解决这个问题:
fun schedule(sunny: Boolean, day: Day) = when {

day == Day.SAT -> basketball()
day == Day.SUN -> fishing()
day == Day.FRI -> appointment()
sunny -> library()
else -> study()

}
是不是很优雅?其实when表达式的威力远不止于此。关于它更多的语法细节,我们会在第4章进一步介绍。同时你也将了解到如何利用when表达式结合代数数据类型,来对业务进行更好的抽象。

2.4.5 for循环和范围表达式

在了解了Kotlin中的流程控制表达式之后,接下来就是我们熟悉的语句while和for。while和do-while的语法与在Java中并没有大多的差异,所以我们重点来看下Kotlin中的for循环的语法和应用。
1. for循环
在Java中,我们经常在for加上一个分号语句块来构建一个循环体,如:
for (int i = 0; i < 10; i++) {

System.out.println(i);

}
在Kotlin中,表达上要更加简洁,可以将上述的代码等价表达为:
for (i in 1..10) println(i)
如果把上述的例子带上花括号和变量i的类型声明,也是支持的:
for (i: Int in 1..10) {

println(i)

}
2.范围表达式
你可能对“1..10”这种语法比较陌生,实际上这是在Kotlin中我们没有提过的范围表达式(range)。我们来看看它在Kotlin官网的文档介绍:
Range表达式是通过rangeTo函数实现的,通过“..”操作符与某种类型的对象组成,除了整型的基本类型之外,该类型需实现java.lang.Comparable接口。
举个例子,由于String类实现了Comparable接口,字符串值之间可以比较大小,所以我们就可以创建一个字符串区间,如:
"abc".."xyz"
字符串的大小根据首字母在字母表中的排序进行比较,如果首字母相同,则从左往右获取下一个字母,以此类推。
另外,当对整数进行for循环时,Kotlin还提供了一个step函数来定义迭代的步长:
image.png
如果是倒序呢?也没有问题,可以用downTo方法来实现:
image.png
此外,还有一个until函数来实现一个半开区间:
image.png
3.用in来检查成员关系
另外一点需要了解的就是in关键字,在Kotlin中我们可以用它来对检查一个元素是否是一个区间或集合中的成员。举几个例子:
image.png
如果我们在in前面加上感叹号,那么就是相反的判断结果:
image.png
除了等和不等,in还可以结合范围表达式来表示更多的含义:
image.png
以上的代码等价于:
"kot" >= "abc" && "abc" <= "xyz"
事实上,任何提供迭代器(iterator)的结构都可以用for语句进行迭代,如:
image.png
此外,我们还可以通过调用一个withIndex方法,提供一个键值元组:
for ((index, value) in array.withIndex()) {

println("the element at $index is $value")

}

2.4.6 中缀表达式

本节中,我们已经见识了不少Kotlin中奇特的方法,如in、step、downTo、until,它们可以不通过点号,而是通过中缀表达式来被调用,从而让语法变得更加简洁直观。那么,这是如何实现的呢?
先来看看Kotlin标准库中另一个类似的方法to的设计,这是一个通过泛型实现的方法,可以返回一个Pair。
infix fun A.to(that: B): Pair
在Kotlin中,to这种形式定义的函数被称为中缀函数。一个中缀函数的表达形式非常简单,我们可以理解成这样:
A 中缀方法 B
不难发现,如果我们要定义一个中缀函数,它必须需满足以下条件:

  • 该中缀函数必须是某个类型的扩展函数或者成员方法;
  • 该中缀函数只能有一个参数;
  • 虽然Kotlin的函数参数支持默认值,但中缀函数的参数不能有默认值,否则以上形式的B会缺失,从而对中缀表达式的语义造成破坏;
  • 同样,该参数也不能是可变参数,因为我们需要保持参数数量始终为1个。

函数可变参数
Kotlin通过varargs关键字来定义函数中的可变参数,类似Java中的“…”的效果。需要注意的是,Java中的可变参数必须是最后一个参数,Kotlin中没有这个限制,但两者都可以在函数体中以数组的方式来使用可变参数变量,正如以下例子:
image.png
此外,我们可以使用*(星号)来传入外部的变量作为可变参数的变量,改写如下:
image.png
由于to会返回Pair这种键值对的结构数据,因此我们经常会把它与map结合在一起使用。如以下例子:
mapOf(

1 to "one",
2 to "two",
3 to "three"

)
可以发现,中缀表达式的方式非常自然。接下来,我们再来自定义一个中缀函数,它是类Person中的一个成员方法:
class Person {

infix fun called(name: String) {
    println("My name is ${name}.")
}

}
因为called方法用infix进行了修饰,所以我们可以这样调用它:
fun main(args: Array) {

val p = Person()
p called "Shaw"

}
// 运行结果
My name is Shaw.
需要注意的是,Kotlin仍然支持使用普通方法的语法习惯来调用一个中缀函数。如这样来执行called方法:
p.called("Shaw")
然而,由于中缀表达式在形式上更像自然语言,所以之前的语法要显得更加的优雅。

2.5 字符串的定义和操作

我们似乎破坏了一个传统。根据惯例,每本编程语言的技术书开头,似乎都会以打印一段“hello world!”的方式来宣告自己的到来。现在,我们决定秉承传统,来完成这一任务。当然,此举实际上不是为了宣扬某种仪式,而是因为本节的内容是关于Kotlin中又一项基础的语法知识,也就是字符串操作。
Kotlin中的字符串并没有什么与众不同,与Java一样,我们通过双引号来定义一个字符串,它是不可变的对象。
val str = "hello world!"
然后,我们可以对其进行各种熟悉的操作:
str.length // 12
str.substring(0,5) // hello
str + " hello Kotlin!" // hello world! hello Kotlin!
str.replace("world", "Kotlin") // hello Kotlin!
由于String是一个字符序列,所以我们可以对其进行遍历:
image.png
还可以访问这个字符序列的成员:
str[0] // h
str.first() // h
str.last() // !
str[str.length - 1] // !
此外,Kotlin的字符串还有各种丰富的API,如:
// 判断是否为空字符串
"".isEmpty() // true
" ".isEmpty() // false
" ".isBlank() // true
"abcdefg".filter { c -> c in 'a'..'d' } // abcd
更多字符串类方法可以查阅Kotlin API文档:https://kotlinlang.org/api/latest/jvm/stdlib/ kotlin/-string/index.html

2.5.1 定义原生字符串

Java在JEP 326改进计划中提议,增加原生字符串的语法支持,因为目前它只能通过转义字符的迂回办法来支持,非常麻烦。而在Kotlin中,已经支持了这种语法,我们来定义一个多行的原生字符串体验一下:
image.png
简而言之,用这种3个引号定义的字符串,最终的打印格式与在代码中所呈现的格式一致,而不会解释转化转义字符(正如上述例子中的n),以及Unicode的转义字符(如uXXXX)。
比如,我们用字符串来描述一段HTML代码,用普通字符串定义时必须是这样子:
image.png
采用原生字符串的格式,会非常方便。如下:
image.png

2.5.2 字符串模板

我们再来举一个很常见的字符串字面量与变量拼接的例子:
image.png
上述代码描述了一个消息模板函数,通过传入消息字段变量,最终返回消息字符串。然而,简简单单的一句话,竟然使用了4个加号,可见相当地不简洁。在Java中,这是我们经常会做的事情。
Kotlin引入了字符串模板来改善这一情况,它支持将变量植入字符串。我们通过它来修改上面的message函数。
image.png
这与声明一个普通的字符串在形式上没什么区别,唯一要做的就是把变量如姓名,通过${name}的格式传入字符串。通过对比我们可以明显看出,字符串模板大大提升了代码的紧凑性和可读性。
此外,除了变量我们当然也可以把表达式通过同样的方式插入字符串中,并且在${expression}中使用双引号。如:
image.png

2.5.3 字符串判等

Kotlin中的判等性主要有两种类型:

  • 结构相等。通过操作符==来判断两个对象的内容是否相等。
  • 引用相等。通过操作符===来判断两个对象的引用是否一样,与之相反的判断操作符是!==。如果比较的是在运行时的原始类型,比如Int,那么===判断的效果也等价于==。

我们通过具体的例子来检测下字符串两种类型的相等性:
image.png

2.6 本章小结

(1)类型推导
Kotlin拥有比Java更加强大的类型推导功能,这避免了静态类型语言在编码时需要书写大量类型的弊端。但它不是万能的,在使用代码块函数体时,必须显式声明返回值类型。此外,一些复杂情况如递归,返回值类型声明也不能省略。
(2)变量声明
我们通过val和var在Kotlin中声明变量,以及一些类的成员属性,代表它们的引用可变性。在函数式开发中,我们优先推荐使用val和不可变对象来减少代码中的副作用,提升程序的可靠性和可组合性。在一些个别情况下,尤其是强调性能的代码中,用var定义局部变量会更加适合。
(3)函数声明
在Kotlin中,一个普通的函数可分为代码块体和表达式体,前者类似于Java中我们定义函数的习惯,后者因为是一个表达式,所以可以省略return关键字。
(4)高阶函数
因为拥抱函数式的设计,在Kotlin中函数是头等公民,这不仅可以让我们在程序中到处定义函数,同时也意味着函数可以作为值进行传递,以及作为另一个函数的返回值。在函数作为参数的时候,我们需要使用函数引用表达式来进行传值。柯里化是函数作为返回值的一种应用,然而在Kotlin中,由于特殊语法糖的存在,我们很少会使用柯里化技术。
(5)Lambda表达式
Lambda是Kotlin中非常重要的语法特性,我们可以把它当作另一种匿名函数。Lambda简洁的语法以及Kotlin语言深度的支持,使得它在我们用Kotlin编程时得到极大程度的应用。
(6)表达式和流程控制
表达式在Kotlin中有着相当重要的地位,这与表达式本身相较于普通语句所带来的优势有关。与后者相比,表达式显得更加安全,更有利于组合,且拥有更强的表达能力。在Kotlin中,流程控制不像Java是清一色的普通语句,利用if、when、try、range、中缀等表达式我们能够写出更加强大的代码。与此同时,Kotlin中的for语句也要更加精简。
(7)字符串操作
Kotlin的字符串跟Java一样,定义的都是不可变对象。除了提供更多丰富的字符串API之外,Kotlin还支持原生字符串、字符串模板这些Java当前并没有支持的特性。

相关文章
|
25天前
|
Java 开发工具 Android开发
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
本系列教程笔记详细讲解了Kotlin语法,适合需要深入了解Kotlin的开发者。若需快速学习Kotlin,建议查看“简洁”系列教程。本期重点介绍了Kotlin与Java的共存方式,包括属性、单例对象、默认参数方法、包方法、扩展方法以及内部类和成员的互操作性。通过这些内容,帮助你在项目中更好地结合使用这两种语言。
41 1
|
26天前
|
Java 开发工具 Android开发
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
32 2
|
25天前
|
IDE 开发工具 数据安全/隐私保护
Kotlin语法 - 类成员
本教程详细讲解Kotlin语法,适合深入学习。若需快速掌握,推荐“简洁”系列教程。内容涵盖类成员、函数与方法、属性访问控制及初始化等。类成员包括方法和属性,函数独立于类,属性访问通过getter/setter实现,支持延迟初始化。
34 3
|
26天前
|
数据安全/隐私保护 Kotlin
Kotlin语法 - 类成员
Kotlin语法 - 类成员
|
25天前
|
Java 编译器 Android开发
Kotlin语法笔记(28) -Kotlin 与 Java 混编
本系列教程详细讲解了Kotlin语法,适合需要深入了解Kotlin的开发者。对于希望快速学习Kotlin的用户,推荐查看“简洁”系列教程。本文档重点介绍了Kotlin与Java混编的技巧,包括代码转换、类调用、ProGuard问题、Android library开发建议以及在Kotlin和Java之间互相调用的方法。
20 1
|
25天前
|
安全 Java 编译器
Kotlin语法笔记(27) -Kotlin 与 Java 共存(二)
本教程详细讲解Kotlin语法,适合希望深入了解Kotlin的开发者。若需快速入门,建议查阅“简洁”系列教程。本文重点探讨Kotlin与Java共存的高级话题,包括属性访问、空安全、泛型处理、同步机制及SAM转换等,助你在项目中逐步引入Kotlin。
20 1
|
26天前
|
Java 编译器 Android开发
Kotlin语法笔记(28) -Kotlin 与 Java 混编
Kotlin语法笔记(28) -Kotlin 与 Java 混编
25 2
|
25天前
|
IDE 开发工具 开发者
Kotlin语法 - 函数与Lambda表达式
本教程详细讲解了Kotlin中的函数与Lambda表达式,包括函数的基本定义、默认返回值类型、匿名函数、Lambda表达式的定义及简化、Lambda与函数引用的结合使用,以及如何在Lambda中实现循环控制。适合希望深入了解Kotlin语法的开发者。
37 1
|
26天前
|
人工智能 Scala Kotlin
Kotlin语法 - 运算符与中缀表达式
Kotlin语法 - 运算符与中缀表达式
|
26天前
|
JavaScript Java Kotlin
Kotlin语法 - 常量与变量
Kotlin语法 - 常量与变量