【语言学习】Kotlin学习 - hippowc/hippowc.github.io GitHub Wiki

基本语法

定义包

包的声明应处于源文件顶部

package my.demo
import java.util.*
// ……

定义函数

带有两个 Int 参数、返回 Int 的函数:

fun sum(a: Int, b: Int): Int {
    return a + b
}

将表达式作为函数体、返回值类型自动推断的函数:

fun sum(a: Int, b: Int) = a + b

函数返回无意义的值, Unit 返回类型可以省略:

fun printSum(a: Int, b: Int): Unit {
    println("sum of $a and $b is ${a + b}")
}

定义变量

定义只读局部变量使用关键字 val 定义。只能为其赋值一次

val a: Int = 1  // 立即赋值
val b = 2   // 自动推断出 `Int` 类型
val c: Int  // 如果没有初始值类型不能省略
c = 3       // 明确赋值

可重新赋值的变量使用 var 关键字:

var x = 5 // 自动推断出 `Int` 类型
x += 1

顶层变量:

val PI = 3.14
var x = 0

fun incrementX() { 
    x += 1 
}

注释

// 这是一个行注释

/* 这是一个多行的
   块注释。 */

字符串模板

var a = 1
// 模板中的简单名称:
val s1 = "a is $a" 

a = 2
// 模板中的任意表达式:
val s2 = "${s1.replace("is", "was")}, but now is $a"

条件表达式

fun maxOf(a: Int, b: Int): Int {
    if (a > b) {
        return a
    } else {
        return b
    }
}

使用可空值及 null 检测

当某个变量的值可以为 null 的时候,必须在声明处的类型后添加 ? 来标识该引用可为空。

fun parseInt(str: String): Int? {
    // ……
}

使用类型检测及自动类型转换

is 运算符检测一个表达式是否某类型的一个实例。 如果一个不可变的局部变量或属性已经判断出为某类型,那么检测后的分支中可以直接当作该类型使用,无需显式转换

fun getStringLength(obj: Any): Int? {
    if (obj is String) {
        // `obj` 在该条件分支内自动转换成 `String`
        return obj.length
    }

    // 在离开类型检测分支后,`obj` 仍然是 `Any` 类型
    return null
}

for 循环

// 不需要index
val items = listOf("apple", "banana", "kiwifruit")
for (item in items) {
    println(item)
}
// 需要index
val items = listOf("apple", "banana", "kiwifruit")
for (index in items.indices) {
    println("item at $index is ${items[index]}")
}

使用while循环

val items = listOf("apple", "banana", "kiwifruit")
var index = 0
while (index < items.size) {
    println("item at $index is ${items[index]}")
    index++
}

使用 when 表达式

fun describe(obj: Any): String =
    when (obj) {
        // 这里不需要obj
        1          -> "One"
        "Hello"    -> "Greeting"
        is Long    -> "Long"
        !is String -> "Not a string"
        else       -> "Unknown"
    }

使用区间(range)

// 包含 1-5
for (x in 1..5) {
    print(x)
}
for (x in 1..10 step 2) {
    print(x)
}
println()
for (x in 9 downTo 0 step 3) {
    print(x)
}

使用集合

使用 in 运算符来判断集合内是否包含某实例

when {
    "orange" in items -> println("juicy")
    "apple" in items -> println("apple is fine too")
}

使用 lambda 表达式来过滤(filter)与映射(map)集合:

val fruits = listOf("banana", "avocado", "apple", "kiwifruit")
fruits
  .filter { it.startsWith("a") }
  .sortedBy { it }
  .map { it.toUpperCase() }
  .forEach { println(it) }

创建基本类及其实例

val rectangle = Rectangle(5.0, 2.0) // 不需要“new”关键字
val triangle = Triangle(3.0, 4.0, 5.0)

基本类型

在 Kotlin 中,所有东西都是对象,在这个意义上讲我们可以在任何变量上调用成员函数与属性。一些类型可以有特殊的内部表示——例如,数字、字符以及布尔值可以在运行时表示为原生类型值,但是对于用户来说,它们看起来就像普通的类。

数字

与java很类似,Byte(8) Short(16) Int(32) Long(64) Float(32) Double(64)

数字的常量:

  • 十进制: 123 Long 类型用大写 L 标记: 123L
  • 十六进制: 0x0F
  • 二进制: 0b00001011
  • 注意: 不支持八进制

数字字面值中的下划线(自 1.1 起):

val oneMillion = 1_000_000
val hexBytes = 0xFF_EC_DE_5E

数字默认使用jvm的原生类型表示,除非声明为可空,会进行装箱。装箱后不保留同一性,但保留相等性:

val a: Int = 1000
println(a === a) // true
val boxA: Int? = a
val anotherBoxA: Int? = a
println(boxA === anotherBoxA) // false
println(boxA == anotherBoxA) // true

数字类型进行转换要显式转换,Int 隐式转换为 Long是会丢失相等性,更会丢失同一性。

kotlin支持数字运算的标准集,位运算没有符号进行表示,但是可以使用中缀方式调用命名函数。

数字除了支持大于、小于、等于外,新增支持区间 x in a..b

字符

字符用 Char 类型表示。它们不能直接当作数字,不能与数字进行比较。但是可以显式把字符转换为 Int 数字

字符字面值用单引号括起来: '1'。 特殊字符可以用反斜杠转义。 支持这几个转义序列:\t、 \b、\n、\r、'、"、\ 与 $。 编码其他字符要用 Unicode 转义序列语法:'\uFF00'。

布尔值

布尔用 Boolean 类型表示,它有两个值:true 与 false。

内置的布尔运算有:

  • || – 短路逻辑或
  • && – 短路逻辑与
  • ! - 逻辑非

数组

数组在 Kotlin 中使用 Array 类来表示,它定义了 get 与 set 函数(按照运算符重载约定这会转变为 [])以及 size 属性,以及一些其他有用的成员函数:

class Array<T> private constructor() {
    val size: Int
    operator fun get(index: Int): T
    operator fun set(index: Int, value: T): Unit

    operator fun iterator(): Iterator<T>
    // ……
}

我们可以使用库函数 arrayOf() 来创建一个数组并传递元素值给它,这样 arrayOf(1, 2, 3) 创建了 array [1, 2, 3]。 或者,库函数 arrayOfNulls() 可以用于创建一个指定大小的、所有元素都为空的数组。

与 Java 不同的是,Kotlin 中数组是不型变的(invariant)。这意味着 Kotlin 不让我们把 Array 赋值给 Array,以防止可能的运行时失败(但是你可以使用 Array, 参见类型投影)。

字符串

字符串用 String 类型表示。字符串是不可变的。 字符串的元素——字符可以使用索引运算符访问: s[i]。 可以用 for 循环迭代字符串:

for (c in str) {
    println(c)
}

可以用 + 操作符连接字符串。这也适用于连接字符串与其他类型的值, 只要表达式中的第一个元素是字符串

Kotlin 有两种类型的字符串字面值: 转义字符串可以有转义字符,以及原始字符串可以包含换行以及任意文本。原始字符串 使用三个引号(""")分界符括起来,内部没有转义并且可以包含换行以及任何其他字符

字符串模板:字符串可以包含模板表达式 ,即一些小段代码,会求值并把结果合并到字符串中。 模板表达式以美元符($)开头,由一个简单的名字构成,或者用花括号括起来的任意表达式.原始字符串与转义字符串内部都支持模板。 如果你需要在原始字符串中表示字面值 $ 字符(它不支持反斜杠转义),你可以用下列语法:

val price = """
${'$'}9.99
"""

包与导入

源文件通常以包声明开头

package foo.bar

fun baz() { ... }
class Goo { ... }

源文件所有内容(无论是类还是函数)都包含在声明的包内。 所以上例中 baz() 的全名是 foo.bar.baz、Goo 的全名是 foo.bar.Goo。 有多个包会默认导入到每个 Kotlin 文件中:

kotlin.* kotlin.annotation.* kotlin.collections.* kotlin.comparisons.* (自 1.1 起) kotlin.io.* kotlin.ranges.* kotlin.sequences.* kotlin.text.*

根据目标平台还会导入额外的包:

JVM: java.lang.* kotlin.jvm.* JS: kotlin.js.*

除了默认导入之外,每个文件可以包含它自己的导入指令,可以导入一个单独的名字, 也可以导入一个作用域下的所有内容(包、类、对象等),如果出现名字冲突,可以使用 as 关键字在本地重命名冲突项来消歧义

import foo.Bar // Bar 可访问
import bar.Bar as bBar // bBar 代表“bar.Bar”

关键字 import 并不仅限于导入类;也可用它来导入其他声明:顶层函数及属性;在对象声明中声明的函数和属性;枚举常量

控制流:if、when、for、while

在 Kotlin 中,if是一个表达式,即它会返回一个值。

// 传统用法
var max = a 
if (a < b) max = b

// With else 
var max: Int
if (a > b) {
    max = a
} else {
    max = b
}
 
// 作为表达式
val max = if (a > b) a else b

if的分支可以是代码块,最后的表达式作为该块的值:

val max = if (a > b) {
    print("Choose a")
    a
} else {
    print("Choose b")
    b
}

如果你使用 if 作为表达式而不是语句(例如:返回它的值或者把它赋给变量),该表达式需要有 else 分支。

When 表达式

when 将它的参数与所有的分支条件顺序比较,直到某个分支满足条件。 when 既可以被当做表达式使用也可以被当做语句使用。如果它被当做表达式, 符合条件的分支的值就是整个表达式的值,如果当做语句使用, 则忽略个别分支的值。(像 if 一样,每一个分支可以是一个代码块,它的值是块中最后的表达式的值。)

如果其他分支都不满足条件将会求值 else 分支。 如果 when 作为一个表达式使用,则必须有 else 分支, 除非编译器能够检测出所有的可能情况都已经覆盖了

如果很多分支需要用相同的方式处理,则可以把多个分支条件放在一起,用逗号分隔:

when (x) {
    0, 1 -> print("x == 0 or x == 1")
    else -> print("otherwise")
}

我们可以用任意表达式(而不只是常量)作为分支条件, 我们也可以检测一个值在(in)或者不在(!in)一个区间或者集合中, 另一种可能性是检测一个值是(is)或者不是(!is)一个特定类型的值。注意: 由于智能转换,你可以访问该类型的方法与属性而无需任何额外的检测。

when 也可以用来取代 if-else if链。 如果不提供参数,所有的分支条件都是简单的布尔表达式,而当一个分支的条件为真时则执行该分支:

when {
    x.isOdd() -> print("x is odd")
    x.isEven() -> print("x is even")
    else -> print("x is funny")
}

For 循环

for 循环可以对任何提供迭代器(iterator)的对象进行遍历

While 循环

while (x > 0) {
    x--
}
do {
  val y = retrieveData()
} while (y != null) // y 在此处可见

返回与跳转

  • return。默认从最直接包围它的函数或者匿名函数返回。
  • break。终止最直接包围它的循环。
  • continue。继续下一次最直接包围它的循环。

在 Kotlin 中任何表达式都可以用标签(label)来标记。 标签的格式为标识符后跟 @ 符号,例如:abc@、fooBar@都是有效的标签(参见语法)。 要为一个表达式加标签,我们只要在其前加标签即可。我们可以用标签限制 break 或者continue:

标签限制的 return 允许我们从外层函数返回。 最重要的一个用途就是从 lambda 表达式中返回

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return // 非局部直接返回到 foo() 的调用者
        print(it)
    }
    println("this point is unreachable")
}

这个 return 表达式从最直接包围它的函数即 foo 中返回

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach lit@{
        if (it == 3) return@lit // 局部返回到该 lambda 表达式的调用者,即 forEach 循环
        print(it)
    }
    print(" done with explicit label")
}

现在,它只会从 lambda 表达式中返回。通常情况下使用隐式标签更方便。 该标签与接受该 lambda 的函数同名。

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return@forEach // 局部返回到该 lambda 表达式的调用者,即 forEach 循环
        print(it)
    }
    print(" done with implicit label")
}

有一些难以理解

类与继承

Kotlin 中使用关键字 class 声明类

class Invoice { ... }

类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成。类头与类体都是可选的; 如果一个类没有类体,可以省略花括号。注意,类是可以有参数的,还可以把构造函数写在花括号外面。

在 Kotlin 中的一个类可以有一个主构造函数以及一个或多个次构造函数。主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。

class Person constructor(firstName: String) { ... }

如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字。

class Person(firstName: String) { ... }

主构造函数不能包含任何的代码。初始化的代码可以放到以 init 关键字作为前缀的初始化块(initializer blocks)中。在实例初始化期间,初始化块按照它们出现在类体中的顺序执行,与属性初始化器交织在一起;请注意,主构造的参数可以在初始化块中使用。它们也可以在类体内声明的属性初始化器中使用:

class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println)
    
    init {
        println("First initializer block that prints ${name}")
    }
    
    val secondProperty = "Second property: ${name.length}".also(::println)
    
    init {
        println("Second initializer block that prints ${name.length}")
    }
}

与普通属性一样,主构造函数中声明的属性可以是可变的(var)或只读的(val)。如果构造函数有注解或可见性修饰符,这个 constructor 关键字是必需的,并且这些修饰符在它前面:

class Customer public @Inject constructor(name: String) { …… }

次构造函数

类也可以声明前缀有 constructor的次构造函数:

class Person {
    constructor(parent: Person) {
        parent.children.add(this)
    }
}

如果类有一个主构造函数,每个次构造函数需要委托给主构造函数, 可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用 this 关键字即可:

class Person(val name: String) {
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

请注意,初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块中的代码都会在次构造函数体之前执行。即使该类没有主构造函数,这种委托仍会隐式发生,并且仍会执行初始化块

如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是 public。如果你不希望你的类有一个公有构造函数,你需要声明一个带有非默认可见性的空的主构造函数

class DontCreateMe private constructor () { ... }

创建类的实例

Kotlin 并没有 new 关键字。

val invoice = Invoice()

val customer = Customer("Joe Smith")

关于次构造函数委托主构造函数以及为什么这样设计有些不理解??什么是委托??

继承

在 Kotlin 中所有类都有一个共同的超类 Any,这对于没有超类型声明的类是默认超类,要声明一个显式的超类型,我们把类型放到类头的冒号之后:

open class Base(p: Int)

class Derived(p: Int) : Base(p)

otlin 力求清晰显式。与 Java 不同,Kotlin 对于可覆盖的成员(我们称之为开放)以及覆盖后的成员需要显式修饰符

open class AnotherDerived() : Base() {
    final override fun v() { ... }
}

属性覆盖与方法覆盖类似;在超类中声明然后在派生类中重新声明的属性必须以 override 开头,并且它们必须具有兼容的类型。每个声明的属性可以由具有初始化器的属性或者具有 getter 方法的属性覆盖。你也可以用一个 var 属性覆盖一个 val 属性,但反之则不行。这是允许的,因为一个 val 属性本质上声明了一个 getter 方法,而将其覆盖为 var 只是在子类中额外声明一个 setter 方法。

派生类初始化顺序

在构造派生类的新实例的过程中,第一步完成其基类的初始化, 基类构造函数执行时,派生类中声明或覆盖的属性都还没有初始化。如果在基类初始化逻辑中(直接或通过另一个覆盖的 open 成员的实现间接)使用了任何一个这种属性,那么都可能导致不正确的行为或运行时故障。设计一个基类时,应该避免在构造函数、属性初始化器以及 init 块中使用 open 成员。

调用超类实现

派生类中的代码可以使用 super 关键字调用其超类的函数与属性访问器的实现: 在一个内部类中访问外部类的超类,可以通过由外部类名限定的 super 关键字来实现:super@Outer

在 Kotlin 中,实现继承由下述规则规定:如果一个类从它的直接超类继承相同成员的多个实现, 它必须覆盖这个成员并提供其自己的实现(也许用继承来的其中之一)。 为了表示采用从哪个超类型继承的实现,我们使用由尖括号中超类型名限定的 super,如 super:

open class A {
    open fun f() { print("A") }
    fun a() { print("a") }
}

interface B {
    fun f() { print("B") } // 接口成员默认就是“open”的
    fun b() { print("b") }
}

class C() : A(), B {
    // 编译器要求覆盖 f():
    override fun f() {
        super<A>.f() // 调用 A.f()
        super<B>.f() // 调用 B.f()
  }
}

抽象类

类以及其中的某些成员可以声明为 abstract。 抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用 open 标注一个抽象类或者函数——因为这不言而喻。

伴生对象

与 Java 或 C# 不同,在 Kotlin 中类没有静态方法。在大多数情况下,它建议简单地使用包级函数。

如果你需要写一个可以无需用一个类的实例来调用、但需要访问类内部的函数(例如,工厂方法),你可以把它写成该类内对象声明中的一员。

属性与字段

Kotlin的类可以有属性。 属性可以用关键字var 声明为可变的,否则使用只读关键字val。一个只读属性的语法和一个可变的属性的语法有两方面的不同:1、只读属性的用 val开始代替var 2、只读属性不允许 setter

声明一个属性的完整语法是

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

val isEmpty: Boolean
    get() = this.size == 0

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // 解析字符串并赋值给其他属性
    }

幕后字段

在 Kotlin 类中不能直接声明字段。然而,当一个属性需要一个幕后字段时,Kotlin 会自动提供。这个幕后字段可以使用field标识符在访问器中引用.这块内容也不是很懂

编译期常量

已知值的属性可以使用 const 修饰符标记为 编译期常量。 这些属性需要满足以下要求:

  • 位于顶层或者是 object 声明 或 companion object 的一个成员
  • 以 String 或原生类型值初始化
  • 没有自定义 gette

延迟初始化属性与变量

属性声明为非空类型必须在构造函数中初始化。 然而,这经常不方便。例如:属性可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 这种情况下,你不能在构造函数内提供一个非空初始器。 但你仍然想在类体中引用该属性时避免空检查。

为处理这种情况,你可以用 lateinit 修饰符标记该属性,在初始化前访问一个 lateinit 属性会抛出一个特定异常,该异常明确标识该属性被访问及它没有初始化的事实。要检测一个 lateinit var 是否已经初始化过,请在该属性的引用上使用 .isInitialized

if (foo::bar.isInitialized) {
    println(foo.bar)
}

此检测仅对可词法级访问的属性可用,即声明位于同一个类型内、位于其中一个外围类型中或者位于相同文件的顶层的属性。

接口

Kotlin 的接口与 Java 8 类似,既包含抽象方法的声明,也包含实现。与抽象类不同的是,接口无法保存状态。它可以有属性但必须声明为抽象或提供访问器实现。

一个接口可以从其他接口派生,从而既提供基类型成员的实现也声明新的函数与属性。很自然地,实现这样接口的类只需定义所缺少的实现:

可见性修饰符

类、对象、接口、构造函数、方法、属性和它们的 setter 都可以有 可见性修饰符。在 Kotlin 中有这四个可见性修饰符:private、 protected、 internal 和 public。 如果没有显式指定修饰符的话,默认可见性是 public。

函数、属性和类、对象和接口可以在顶层声明,即直接在包内:

// 文件名:example.kt
package foo

private fun foo() { …… } // 在 example.kt 内可见

public var bar: Int = 5 // 该属性随处可见
    private set         // setter 只在 example.kt 内可见
    
internal val baz = 6    // 相同模块内可见

如果你不指定任何可见性修饰符,默认为 public,这意味着你的声明将随处可见; 如果你声明为 private,它只会在声明它的文件内可见; 如果你声明为 internal,它会在相同模块内随处可见; protected 不适用于顶层声明。

对于类内部声明的成员:

private 意味着只在这个类内部(包含其所有成员)可见; protected—— 和 private一样 + 在子类中可见。 internal —— 能见到类声明的 本模块内 的任何客户端都可见其 internal 成员; public —— 能见到类声明的任何客户端都可见其 public 成员。

局部变量、函数和类不能有可见性修饰符。

可见性修饰符 internal 意味着该成员只在相同模块内可见。更具体地说, 一个模块是编译在一起的一套 Kotlin 文件:

一个 IntelliJ IDEA 模块; 一个 Maven 项目; 一个 Gradle 源集(例外是 test 源集可以访问 main 的 internal 声明); 一次 Ant 任务执行所编译的一套文件。

扩展

Kotlin 同 C# 与 Gosu 类似,能够扩展一个类的新功能而无需继承该类或使用像装饰者这样的任何类型的设计模式。 这通过叫做 扩展 的特殊声明完成。Kotlin 支持 扩展函数 与 扩展属性。

扩展函数

声明一个扩展函数,我们需要用一个 接收者类型 也就是被扩展的类型来作为他的前缀。 下面代码为 MutableList 添加一个swap 函数:

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // “this”对应该列表
    this[index1] = this[index2]
    this[index2] = tmp
}

这个 this 关键字在扩展函数内部对应到接收者对象(传过来的在点符号前的对象) 现在,我们对任意 MutableList 调用该函数了

扩展函数是静态分发的,调用的扩展函数是由函数调用所在的表达式的类型来决定的, 而不是由表达式运行时求值结果决定的

可以为可空的接收者类型定义扩展。这样的扩展可以在对象变量上调用, 即使其值为 null,并且可以在函数体内检测 this == null

扩展属性

与函数类似,Kotlin 支持扩展属性:

val <T> List<T>.lastIndex: Int
    get() = size - 1

由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。这就是为什么扩展属性不能有初始化器。

扩展的作用域

大多数时候我们在顶层定义扩展,即直接在包里,要使用所定义包之外的一个扩展,我们需要在调用方导入它

package foo.bar

fun Baz.goo() { …… }

package com.example.usage
import foo.bar.goo // 导入所有名为“goo”的扩展
                   // 或者
import foo.bar.*   // 从“foo.bar”导入一切

fun usage(baz: Baz) {
    baz.goo()
}

数据类

我们经常创建一些只保存数据的类。 在这些类中,一些标准函数往往是从数据机械推导而来的。在 Kotlin 中,这叫做 数据类 并标记为 data:

data class User(val name: String, val age: Int)

编译器自动从主构造函数中声明的所有属性导出以下成员:

  • equals()/hashCode() 对;
  • toString() 格式是 "User(name=John, age=42)";
  • componentN() 函数 按声明顺序对应于所有属性;
  • copy() 函数

在类体中声明的属性

于那些自动生成的函数,编译器只使用在主构造函数内部定义的属性。如需在生成的实现中排出一个属性,请将其声明在类体中

复制

在很多情况下,我们需要复制一个对象改变它的一些属性,但其余部分保持不变。 copy() 函数就是为此而生成。对于上文的 User 类

val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)

数据类与解构声明

为数据类生成的 Component 函数 使它们可在解构声明中使用:

val jane = User("Jane", 35)
val (name, age) = jane

标准数据类

标准库提供了 Pair 与 Triple。尽管在很多情况下命名数据类是更好的设计选择, 因为它们通过为属性提供有意义的名称使代码更具可读性。

密封类

密封类用来表示受限的类继承结构:当一个值为有限集中的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例

要声明一个密封类,需要在类名前面添加 sealed 修饰符。虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

一个密封类是自身抽象的,它不能直接实例化并可以有抽象(abstract)成员, 密封类不允许有非-private 构造函数(其构造函数默认为 private), 使用密封类的关键好处在于使用 when 表达式 的时候,如果能够验证语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了

泛型

与 Java 类似,Kotlin 中的类也可以有类型参数

class Box<T>(t: T) {
    var value = t
}

一般来说,要创建这样类的实例,我们需要提供类型参数, 但是如果类型参数可以推断出来,例如从构造函数的参数或者从其他途径,允许省略类型参数

型变

Java 类型系统中最棘手的部分之一是通配符类型(参见 Java Generics FAQ)。 而 Kotlin 中没有。 相反,它有两个其他的东西:声明处型变(declaration-site variance)与类型投影(type projections)。

首先,Java 中的泛型是不型变的,这意味着 List 并不是 List 的子类型。 为什么这样? 如果 List 不是不型变的,它就没比 Java 的数组好到哪去,因为如下代码会通过编译然后导致运行时异常

// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!!即将来临的问题的原因就在这里。Java 禁止这样!
objs.add(1); // 这里我们把一个整数放入一个字符串列表
String s = strs.get(0); // !!! ClassCastException:无法将整数转换为字符串

关于协变需要详细了解下这一章???

嵌套类与内部类

类可以嵌套在其他类中:

class Outer {
    private val bar: Int = 1
    class Nested {
        fun foo() = 2
    }
}

val demo = Outer.Nested().foo() // == 2

类可以标记为 inner 以便能够访问外部类的成员。内部类会带有一个对外部类的对象的引用

class Outer {
    private val bar: Int = 1
    inner class Inner {
        fun foo() = bar
    }
}

val demo = Outer().Inner().foo() // == 1

枚举类

枚举类的最基本的用法是实现类型安全的枚举:

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

每个枚举常量都是一个对象。枚举常量用逗号分隔。因为每一个枚举都是枚举类的实例,所以他们可以是这样初始化过的:

enum class Color(val rgb: Int) {
        RED(0xFF0000),
        GREEN(0x00FF00),
        BLUE(0x0000FF)
}

对象表达式与对象声明

我们需要创建一个对某个类做了轻微改动的类的对象,而不用为之显式声明新的子类。 Java 用匿名内部类 处理这种情况

对象表达式

要创建一个继承自某个(或某些)类型的匿名类的对象,我们会这么写:

object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) { …… }

    override fun mouseEntered(e: MouseEvent) { …… }
}

如果超类型有一个构造函数,则必须传递适当的构造函数参数给它。 多个超类型可以由跟在冒号后面的逗号分隔的列表指定:

open class A(x: Int) {
    public open val y: Int = x
}

interface B { …… }

val ab: A = object : A(1), B {
    override val y = 15
}

任何时候,如果我们只需要“一个对象而已”,并不需要特殊超类型,那么我们可以简单地写:

fun foo() {
    val adHoc = object {
        var x: Int = 0
        var y: Int = 0
    }
    print(adHoc.x + adHoc.y)
}

匿名对象可以用作只在本地和私有作用域中声明的类型。如果你使用匿名对象作为公有函数的返回类型或者用作公有属性的类型,那么该函数或属性的实际类型会是匿名对象声明的超类型,如果你没有声明任何超类型,就会是 Any。在匿名对象中添加的成员将无法访问

对象声明

单例模式在一些场景中很有用, 而 Kotlin(继 Scala 之后)使单例声明变得很容易:

object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ……
    }

    val allDataProviders: Collection<DataProvider>
        get() = // ……
}

这称为对象声明。并且它总是在 object 关键字后跟一个名称。 就像变量声明一样,对象声明不是一个表达式,不能用在赋值语句的右边。对象声明的初始化过程是线程安全的。如需引用该对象,我们直接使用其名称即可:

DataProviderManager.registerDataProvider(……)

伴生对象

类内部的对象声明可以用 companion 关键字标记:

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

该伴生对象的成员可通过只使用类名作为限定符来调用:

val instance = MyClass.create()

伴生对象有什么意义???

对象表达式和对象声明之间的语义差异

对象表达式和对象声明之间有一个重要的语义差别:

  • 对象表达式是在使用他们的地方立即执行(及初始化)的;
  • 对象声明是在第一次被访问到时延迟初始化的;
  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配。

内联类

有时候,业务逻辑需要围绕某种类型创建包装器。然而,由于额外的堆内存分配问题,它会引入运行时的性能开销。此外,如果被包装的类型是原生类型,性能的损失是很糟糕的,因为原生类型通常在运行时就进行了大量优化,然而他们的包装器却没有得到任何特殊的处理。

为了解决这类问题,Kotlin 引入了一种被称为 内联类 的特殊类,它通过在类的前面定义一个 inline 修饰符来声明:

inline class Password(val value: String)

内联类必须含有唯一的一个属性在主构造函数中初始化。在运行时,将使用这个唯一属性来表示内联类的实例

委托

委托模式已经证明是实现继承的一个很好的替代方式, 而 Kotlin 可以零样板代码地原生支持它。 Derived 类可以通过将其所有公有成员都委托给指定对象来实现一个接口 Base:

interface Base {
    fun print()
}

class BaseImpl(val x: Int) : Base {
    override fun print() { print(x) }
}

class Derived(b: Base) : Base by b

fun main() {
    val b = BaseImpl(10)
    Derived(b).print()
}

Derived 的超类型列表中的 by-子句表示 b 将会在 Derived 中内部存储, 并且编译器将生成转发给 b 的所有 Base 的方法。

委托属性

有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现它们, 但是如果能够为大家把他们只实现一次并放入一个库会更好。例如包括:

  • 延迟属性(lazy properties): 其值只在首次访问时计算;
  • 可观察属性(observable properties): 监听器会收到有关此属性变更的通知;
  • 把多个属性储存在一个映射(map)中,而不是每个存在单独的字段中。

所以到底什么是委托????

函数

Kotlin 中的函数使用 fun 关键字声明:

fun double(x: Int): Int {
    return 2 * x
}

val result = double(2)

Sample().foo() // 创建类 Sample 实例并调用 foo

函数参数使用 Pascal 表示法定义,即 name: type。参数用逗号隔开。每个参数必须有显式类型:

函数参数可以有默认值,当省略相应的参数时使用默认值。与其他语言相比,这可以减少重载数量:

fun read(b: Array<Byte>, off: Int = 0, len: Int = b.size) { …… }

覆盖方法总是使用与基类型方法相同的默认参数值。 当覆盖一个带有默认参数值的方法时,必须从签名中省略默认参数值:

open class A {
    open fun foo(i: Int = 10) { …… }
}

class B : A() {
    override fun foo(i: Int) { …… }  // 不能有默认值
}

如果一个默认参数在一个无默认值的参数之前,那么该默认值只能通过使用命名参数调用该函数来使用:

fun foo(bar: Int = 0, baz: Int) { …… }

foo(baz = 1) // 使用默认值 bar = 0

当一个函数调用混用位置参数与命名参数时,所有位置参数都要放在第一个命名参数之前。例如,允许调用 f(1, y = 2) 但不允许 f(x = 1, 2)。

可以通过使用星号操作符将可变数量参数(vararg) 以命名形式传入:

fun foo(vararg strings: String) { …… }

foo(strings = *arrayOf("a", "b", "c"))

单表达式函数

当函数返回单个表达式时,可以省略花括号并且在 = 符号之后指定代码体即可:

fun double(x: Int): Int = x * 2

当返回值类型可由编译器推断时,显式声明返回类型是可选的:

fun double(x: Int) = x * 2

中缀表示法

标有 infix 关键字的函数也可以使用中缀表示法(忽略该调用的点与圆括号)调用。中缀函数必须满足以下要求

  • 它们必须是成员函数或扩展函数;
  • 它们必须只有一个参数;
  • 其参数不得接受可变数量的参数且不能有默认值。
infix fun Int.shl(x: Int): Int { …… }

// 用中缀表示法调用该函数
1 shl 2

// 等同于这样
1.shl(2)

中缀函数调用的优先级低于算术操作符、类型转换以及 rangeTo 操作符。中缀函数调用的优先级高于布尔操作符 && 与 ||、is- 与 in- 检测以及其他一些操作符。中缀函数总是要求指定接收者与参数。当使用中缀表示法在当前接收者上调用方法时,需要显式使用 this;不能像常规方法调用那样省略。这是确保非模糊解析所必需的。

函数作用域

在 Kotlin 中函数可以在文件顶层声明,这意味着你不需要像一些语言如 Java、C# 或 Scala 那样需要创建一个类来保存一个函数。此外除了顶层函数,Kotlin 中函数也可以声明在局部作用域、作为成员函数以及扩展函数。

  • Kotlin 支持局部函数,即一个函数在另一个函数内部,局部函数可以访问外部函数(即闭包)的局部变量
  • 成员函数是在类或对象内部定义的函数
  • 函数可以有泛型参数,通过在函数名前使用尖括号指定
fun <T> singletonList(item: T): List<T> { …… }

Kotlin 支持一种称为尾递归的函数式编程风格。 这允许一些通常用循环写的算法改用递归函数来写,而无堆栈溢出的风险。 当一个函数用 tailrec 修饰符标记并满足所需的形式时,编译器会优化该递归,留下一个快速而高效的基于循环的版本

高阶函数与 lambda 表达式

Kotlin 函数都是头等的,这意味着它们可以存储在变量与数据结构中、作为参数传递给其他高阶函数以及从其他高阶函数返回。可以像操作任何其他非函数值一样操作函数。作为一门静态类型编程语言的 Kotlin 使用一系列函数类型来表示函数并提供一组特定的语言结构,例如 lambda 表达式。

高阶函数是将函数用作参数或返回值的函数。

Kotlin 使用类似 (Int) -> String 的一系列函数类型来处理函数的声明

所有函数类型都有一个圆括号括起来的参数类型列表以及一个返回类型:(A, B) -> C 表示接受类型分别为 A 与 B 两个参数并返回一个 C 类型值的函数类型。 参数类型列表可以为空,如 () -> A。Unit 返回类型不可省略。

函数类型可以有一个额外的接收者类型,它在表示法中的点之前指定: 类型 A.(B) -> C 表示可以在 A 的接收者对象上以一个 B 类型参数来调用并返回一个 C 类型值的函数。 带有接收者的函数字面值通常与这些类型一起使用。

挂起函数属于特殊种类的函数类型,它的表示法中有一个 suspend 修饰符 ,例如 suspend () -> Unit 或者 suspend A.(B) -> C。

函数类型表示法可以选择性地包含函数的参数名:(x: Int, y: Int) -> Point。 这些名称可用于表明参数的含义

  • 如需将函数类型指定为可空,请使用圆括号:((Int, Int) -> Int)?。
  • 函数类型可以使用圆括号进行接合:(Int) -> ((Int) -> Unit)
  • 箭头表示法是右结合的,(Int) -> (Int) -> Unit 与前述示例等价,但不等于 ((Int) -> (Int)) -> Unit。

还可以通过使用类型别名给函数类型起一个别称:

typealias ClickHandler = (Button, ClickEvent) -> Unit

函数类型实例化

使用函数字面值的代码块,采用以下形式之一: lambda 表达式: { a, b -> a + b }, 匿名函数: fun(s: String): Int { return s.toIntOrNull() ?: 0 } 带有接收者的函数字面值可用作带有接收者的函数类型的值。

使用已有声明的可调用引用: 顶层、局部、成员、扩展函数:::isOdd、 String::toInt, 顶层、成员、扩展属性:List::size, 构造函数:::Regex 这包括指向特定实例成员的绑定的可调用引用:foo::toString。

使用实现函数类型接口的自定义类的实例

class IntTransformer: (Int) -> Int {
    override operator fun invoke(x: Int): Int = TODO()
}

val intFunction: (Int) -> Int = IntTransformer()

函数类型实例调用

函数类型的值可以通过其 invoke(……) 操作符调用:f.invoke(x) 或者直接 f(x), 如果该值具有接收者类型,那么应该将接收者对象作为第一个参数传递。 调用带有接收者的函数类型值的另一个方式是在其前面加上接收者对象, 就好比该值是一个扩展函数:1.foo(2)

val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plus

println(stringPlus.invoke("<-", "->"))
println(stringPlus("Hello, ", "world!")) 

println(intPlus.invoke(1, 1))
println(intPlus(1, 2))
println(2.intPlus(3)) // 类扩展调用

Lambda 表达式与匿名函数

lambda 表达式与匿名函数是“函数字面值”,即未声明的函数, 但立即做为表达式传递。Lambda 表达式的完整语法形式如下:

val sum = { x: Int, y: Int -> x + y }

lambda 表达式总是括在花括号中, 完整语法形式的参数声明放在花括号内,并有可选的类型标注, 函数体跟在一个 -> 符号之后。如果推断出的该 lambda 的返回类型不是 Unit,那么该 lambda 主体中的最后一个(或可能是单个)表达式会视为返回值。

如果我们把所有可选标注都留下,看起来如下:

val sum: (Int, Int) -> Int = { x, y -> x + y }

将 lambda 表达式传给最后一个参数

在 Kotlin 中有一个约定:如果函数的最后一个参数接受函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外:

val product = items.fold(1) { acc, e -> acc * e }

如果该 lambda 表达式是调用时唯一的参数,那么圆括号可以完全省略:

run { println("...") }

一个 lambda 表达式只有一个参数是很常见的。如果编译器自己可以识别出签名,也可以不用声明唯一的参数并忽略 ->。 该参数会隐式声明为 it

上面提供的 lambda 表达式语法缺少的一个东西是指定函数的返回类型的能力。在大多数情况下,这是不必要的。因为返回类型可以自动推断出来。然而,如果确实需要显式指定,可以使用另一种语法: 匿名函数 。

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

闭包

Lambda 表达式或者匿名函数(以及局部函数和对象表达式) 可以访问其 闭包 ,即在外部作用域中声明的变量。 与 Java 不同的是可以修改闭包中捕获的变量:

var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)

内联函数

使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包。 即那些在函数体内会访问到的变量。 内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销

参考

https://www.kotlincn.net/

⚠️ **GitHub.com Fallback** ⚠️