vlambda博客
学习文章列表

读书笔记《functional-kotlin》Kotlin-数据类型、对象和类

Chapter 1. Kotlin – Data Types, Objects, and Classes

在本章中,我们将介绍 Kotlin 的类型系统,面向对象编程 (OOP ) 与 Kotlin、修饰符、解构声明等。

Kotlin 主要是一种 OOP 语言,具有一些功能特性。当我们使用 OOP 语言解决问题时,我们尝试使用与问题相关的信息以抽象的方式对作为问题一部分的对象进行建模。

如果我们正在为我们的公司设计一个 HR 模块,我们将使用状态或数据(姓名、出生日期、社会安全号码等)和行为(支付薪水、转移到另一个部门等)对员工进行建模。因为一个人可能非常复杂,所以有些信息与我们的问题或领域无关。例如,员工最喜欢的自行车样式与我们的人力资源系统无关,但与在线自行车商店非常相关。

一旦我们确定了对象(带有数据和行为)以及与我们领域中其他对象的关系,我们就可以开始开发和编写我们将成为软件解决方案一部分的代码。我们将使用语言构造(构造是一种表示允许语法的奇特方式)来编写对象、类别、关系等。

Kotlin 有许多可以用来编写程序的构造,在本章中,我们将介绍其中的许多构造,例如:

  • Classes
  • Inheritance
  • Abstract classes
  • Interfaces
  • Objects
  • Generics
  • Type alias
  • Null types
  • Kotlin's type system
  • Other types

Classes


Classes 是 Kotlin 中的 基础 类型.在 Kotlin 中,类是为实例提供状态、行为和类型的模板(稍后会详细介绍)。

要定义一个类,只需要一个名称:

class VeryBasic

VeryBasic 不是很有用,但仍然是有效的 Kotlin 语法。

 VeryBasic 类没有任何状态或行为;尽管如此,您可以声明 VeryBasic 类型的值,如下面的代码所示:

fun main(args: Array<String>) {
    val basic: VeryBasic = VeryBasic()
}

如您所见,basic 值具有 VeryBasic 类型。换一种说法,basicVeryBasic 的一个实例。

在 Kotlin 中,可以推断类型;因此,前面的 example 等价于以下代码:

fun main(args: Array<String>) {
    val basic = VeryBasic()
}

作为 VeryBasic 实例,basic 拥有 VeryBasic 的副本  ;type 的状态和行为,即无。好难过。

Properties

如前所述,类可以具有状态。在 Kotlin 中,类的状态由 属性<表示 /跨度>。让我们看一下蓝莓纸杯蛋糕的例子:

class BlueberryCupcake {
  var flavour = "Blueberry"
}

  BlueberryCupcake 类有一个 has-a 属性String 类型的“literal">风味。

当然,我们可以有 BlueberryCupcake 类的 instances

fun main(args: Array<String>) {
    val myCupcake = BlueberryCupcake()
    println("My cupcake has ${myCupcake.flavour}")
}

现在,因为我们将 flavour 属性声明为变量,所以它的内部值可以在运行时更改:

fun main(args: Array<String>) {
    val myCupcake = BlueberryCupcake()
    myCupcake.flavour = "Almond"
    println("My cupcake has ${myCupcake.flavour}")
}

这在现实生活中是不可能的。纸杯蛋糕不会改变它们的味道(除非它们变得陈旧)。如果我们将 flavour 属性更改为一个值,它就不能被修改:

class BlueberryCupcake {
    val flavour = "Blueberry"
}

fun main(args: Array<String>) {
    val myCupcake = BlueberryCupcake()
    myCupcake.flavour = "Almond" //Compilation error: Val cannot be reassigned
    println("My cupcake has ${myCupcake.flavour}")
}

让我们为杏仁纸杯蛋糕声明一个新类:

class AlmondCupcake {
    val flavour = "Almond"
}

fun main(args: Array<String>) {
    val mySecondCupcake = AlmondCupcake()
    println("My second cupcake has ${mySecondCupcake.flavour} flavour")
}

这里有一些可疑的东西。 BlueberryCupcake 和AlmondCupcake结构相同;只有一个内部值被改变。

在现实生活中,您没有不同的烤模来搭配不同的纸杯蛋糕口味。同一个质量好的烤盘可以用来做各种口味。同样,一个设计良好的 Cupcake 类可以用于不同的实例:

class Cupcake(flavour: String) { 
  val flavour = flavour
}

Cupcake 类有一个带有参数 flavour 的构造函数,该参数分配给 flavour 值。

因为这是一个很常见的习语,所以 Kotlin 有一点语法糖来更简洁地定义它:

class Cupcake(val flavour: String)

现在,我们可以定义几个instances 有不同的口味:

fun main(args: Array<String>) {
    val myBlueberryCupcake = Cupcake("Blueberry")
    val myAlmondCupcake = Cupcake("Almond")
    val myCheeseCupcake = Cupcake("Cheese")
    val myCaramelCupcake = Cupcake("Caramel")
}

Methods

在 Kotlin 中,类的行为由方法定义。从技术上讲,methodmember函数,因此,我们在接下来的章节中学到的任何关于函数的知识也适用于方法:

class Cupcake(val flavour: String) {
  fun eat(): String {
    return "nom, nom, nom... delicious $flavour cupcake"
  }
}

eat() 方法返回一个 String 值。现在,让我们调用 eat() 方法,如下代码所示:

fun main(args: Array<String>) {
    val myBlueberryCupcake = Cupcake("Blueberry")
    println(myBlueberryCupcake.eat())
}

following 表达式是 preceding 代码:

读书笔记《functional-kotlin》Kotlin-数据类型、对象和类

没有什么令人兴奋的,但这是我们的第一种方法。稍后,我们会做更多有趣的事情。

Inheritance


随着我们在 Kotlin 中继续建模我们的领域,我们意识到特定对象非常相似。如果我们回到我们的 HR 示例,员工和承包商非常相似。两者都有姓名、出生日期等;他们也有一些差异。例如,承包商有日薪,而雇员有月薪。很明显,他们很相似——他们都是人; people 是承包商和员工都属于的超集。因此,两者都有自己的特定特征,这些特征使它们足够不同,可以分类为不同的子集。

这就是继承的全部意义,有组和子组,它们之间存在关系。在继承层次结构中,如果你在层次结构中向上,你会看到更一般的特征和行为,如果你向下,你会看到更具体的特征和行为。墨西哥卷饼和微处理器都是对象,但它们的用途和用途截然不同。 

让我们引入一个新的 Biscuit 类:

class Biscuit(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour biscuit" 
  } 
}

同样,这个类看起来与 Cupcake 几乎完全相同。我们可以重构这些类以减少代码重复:

open class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour bakery good" 
  } 
} 

class Cupcake(flavour: String): BakeryGood(flavour) 
class Biscuit(flavour: String): BakeryGood(flavour)

我们引入了一个新的 BakeryGood 类,具有 CupcakeBiscuit 的共享行为和状态 类,我们让这两个类都扩展了 BakeryGood。通过这样做,Cupcake(和 Biscuit)有一个 is-a< /em> 现在与 BakeryGood 建立关系;另一方面,BakeryGoodCupcake class 的父类或父类。

请注意, BakeryGood 被标记为 open。这意味着我们专门设计了这个类来扩展。在 Kotlin 中,您无法扩展未open 的类。

将常见的行为和状态转移到父类的过程称为泛化。让我们看看下面的代码:

fun main(args: Array<String>) {
    val myBlueberryCupcake: BakeryGood = Cupcake("Blueberry")
    println(myBlueberryCupcake.eat())
}

让我们试用我们的新代码:

读书笔记《functional-kotlin》Kotlin-数据类型、对象和类

糟糕,不是我们所期待的。我们需要更多地折射它:

open class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  } 

  open fun name(): String { 
    return "bakery good" 
  } 
} 

class Cupcake(flavour: String): BakeryGood(flavour) { 
  override fun name(): String { 
    return "cupcake" 
  } 
} 

class Biscuit(flavour: String): BakeryGood(flavour) { 
  override fun name(): String { 
    return "biscuit" 
  } 
}

有用!让我们看看 输出:

读书笔记《functional-kotlin》Kotlin-数据类型、对象和类

我们声明了一个新方法,name();它应该被标记为 open,因为我们将它设计为可以在其子类中进行更改。

在子类上修改方法的定义称为 override,这就是 why 两个子类中的name() 方法被标记为override

在层次结构中扩展类和覆盖行为的过程称为专业化< /strong>

Note

经验法则 将一般状态和行为放在层次结构的顶部(泛化),将特定状态和行为放在子类中(特化)。

现在,我们可以拥有更多的烘焙商品!让我们看一下下面的代码:

open class Roll(flavour: String): BakeryGood(flavour) { 
  override fun name(): String { 
    return "roll" 
  } 
} 

class CinnamonRoll: Roll("Cinnamon")

子类也可以扩展。只需将它们标记为 asopen

open class Donut(flavour: String, val topping: String) : BakeryGood(flavour)
{
    override fun name(): String {
        return "donut with $topping topping"
    }
}

fun main(args: Array<String>) {
    val myDonut = Donut("Custard", "Powdered sugar")
    println(myDonut.eat())
}

我们还可以创建具有更多属性和方法的类。

Abstract classes


到目前为止,一切都很好。我们的面包店看起来不错。但是,我们当前的模型存在问题。我们来看下面的代码:

fun main(args: Array<String>) {
    val anyGood = BakeryGood("Generic flavour")
}

我们可以直接实例化BakeryGood类,太通用了。为了纠正这种情况,我们可以将 BakeryGood 标记为 abstract

abstract class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  }

  open fun name(): String { 
    return "bakery good" 
  } 
}

抽象类是一个专门为扩展而设计的类。抽象类不能被实例化,这解决了我们的问题。

abstractopen 有何不同?

这两个修饰符都可以让我们扩展一个类,但是 open 可以让我们实例化,而 abstract 不能。

现在我们无法实例化,我们在 BakeryGood 类中的 name() 方法不再那么有用了,而且我们所有的子类,除了 CinnamonRoll,无论如何都要覆盖它(CinnamonRoll 在 Roll 实现):

abstract class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  } 

  abstract fun name(): String 
}

标记为 abstract 的方法没有主体,只有签名声明(方法签名是标识方法的一种方式)。在 Kotlin 中,签名由方法名称、方法编号、参数类型和返回类型组成。

任何直接扩展 BakeryGood 的类都必须覆盖 name() 方法。覆盖抽象方法的技术术语是 implement,从现在开始,我们 使用它。因此,Cupcake 类实现了 name() 方法(Kotlin 没有方法实现的关键字;这两种情况,方法实现和方法覆盖,都使用关键字 override)。

让我们引入一个新类,Customer;面包店无论如何都需要顾客:

class Customer(val name: String) {
  fun eats(food: BakeryGood) {
    println("$name is eating... ${food.eat()}")
  }
}

fun main(args: Array<String>) {
    val myDonut = Donut("Custard", "Powdered sugar")
    val mario = Customer("Mario")
mario.eats(myDonut)
}

eats(food: BakeryGood) 方法采用 BakeryGood 参数,因此任何扩展了 BakeryGood 参数,多少层级无关紧要。请记住,我们可以直接实例化 BakeryGood

如果我们想要一个简单的 BakeryGood 会发生什么?例如,测试。

还有一个替代方案,一个匿名子类:

fun main(args: Array<String>) {
    val mario = Customer("Mario")

    mario.eats(object : BakeryGood("TEST_1") {
        override fun name(): String {
            return "TEST_2"
        }
    })
}

这里引入了一个新关键字,object。稍后,我们将更详细地介绍 object,但现在,知道这是一个 object 表达式就足够了object 表达式定义了扩展类型的匿名类的实例。

在我们的示例中,object 表达式(从技术上讲,匿名类 ) 必须覆盖 name() 方法并传递一个值作为 BakeryGood 的参数构造函数,与 标准类 完全一样。

请记住,object 表达式是一个实例,因此它可以用于声明值:

val food: BakeryGood = object : BakeryGood("TEST_1") { 
  override fun name(): String { 
    return "TEST_2" 
  } 
} 

mario.eats(food)

Interfaces


开放类和抽象类非常适合创建 层次结构,但有时它们还不够。一些子集可以跨越明显不相关的层次结构,例如,鸟类和类人猿是双足动物,它们都是动物和脊椎动物,但它们没有直接关系。这就是为什么我们需要不同的构造,而 Kotlin 为我们提供了接口(其他语言处理这个问题的方式不同)。

我们的烘焙食品很棒,但我们需要先将它们煮熟:

abstract class BakeryGood(val flavour: String) { 
  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  } 

  fun bake(): String { 
    return "is hot here, isn't??" 
  } 

  abstract fun name(): String 
}

使用我们新的 bake() 方法 ,它可以烹饪我们所有令人惊叹的产品,但是等等,甜甜圈不是烤的,而是油炸的。

如果我们可以将  bake() 方法移动到第二个抽象类, Bakeable会怎么样?让我们在下面的代码中尝试一下:

abstract class Bakeable { 
  fun bake(): String { 
    return "is hot here, isn't??" 
  } 
} 

class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable() { //Compilation error: Only one class may appear in a supertype list 
  override fun name(): String { 
    return "cupcake" 
 }
}

错误的!在 Kotlin 中,一个类不能同时扩展两个类。让我们看一下下面的代码:

interface Bakeable { 
  fun bake(): String { 
    return "is hot here, isn't??" 
  } 
} 

class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable { 
  override fun name(): String { 
    return "cupcake" 
  } 
}

但是,它可以扩展许多接口。 interface 是一种定义行为的类型;在 Bakeable 接口的情况下,即 bake()方法。

那么, 开放/抽象类和接口之间有什么区别?

让我们从以下相似之处开始:

  • Both are types. In our example, Cupcake has an is-a relationship with BakeryGood and has an is-a relationship with Bakeable.
  • Both define behaviors as methods.
  • Although open classes can be instantiated directly, neither abstract classes nor interfaces can.

现在,让我们看看以下差异:

  • A class can extend just one class (open or abstract), but can extend many interfaces.
  • An open/abstract class can have constructors.
  • An open/abstract class can initialize its own values. An interface's values must be initialized in the classes that extend the interface.
  • An open class must declare the methods that can be overridden as open. An abstract class could have both open and abstract methods.

在接口中,所有方法都是开放的,没有实现的方法不需要抽象修饰符:

interface Fried { 
  fun fry(): String 
} 

open class Donut(flavour: String, val topping: String) : BakeryGood(flavour), Fried { 
  override fun fry(): String { 
    return "*swimming on oil*" 
  } 

  override fun name(): String { 
    return "donut with $topping topping" 
  } 
}

什么时候应该使用其中一个?:

  • Use open class when:
    • The class should be extended and instantiated
  • Use abstract class when:
    • The class can't be instantiated
    • A constructor is needed it
    • There is initialization logic (using init blocks)

让我们看一下下面的代码:

abstract class BakeryGood(val flavour: String) {
init {
    println("Preparing a new bakery good") 
  } 

  fun eat(): String { 
    return "nom, nom, nom... delicious $flavour ${name()}" 
  } 

  abstract fun name(): String 
}
  • Use interface when:
    • Multiple inheritances must be applied
    • No initialized logic is needed

Note

我的建议是您应该始终从界面开始。界面更直接、更干净;它们还允许更模块化的设计。如果需要数据初始化/构造函数,请移至抽象/开放。

与抽象类一样,对象 expressions 可以与接口一起使用:

val somethingFried = object : Fried { 
  override fun fry(): String { 
    return "TEST_3" 
  } 
}

Objects


我们已经介绍了对象表达式,但还有更多关于对象的内容。 对象自然单例(自然,我的意思是作为语言特性而不是作为行为模式实现,就像在其他语言中一样)。 singleton 是一种类型,它只有一个且只有一个实例,并且 Kotlin 是一个单例。这开启了许多有趣的模式(以及一些不好的做法)。作为单例的对象对于协调整个系统的操作很有用,但如果它们用于保持全局状态也可能很危险。

对象表达式不需要扩展任何类型:

fun main(args: Array<String>) {
    val expression = object {
        val property = ""

        fun method(): Int {
            println("from an object expressions")
            return 42
        }
    }

    val i = "${expression.method()} ${expression.property}"
    println(i)
}

在这种情况下, 表达式 值是一个没有任何特定类型的对象。我们可以访问它的属性和功能。

有一个限制——没有类型的对象表达式只能在本地、方法内部或类内部私有地使用:

class Outer {
    val internal = object {
        val property = ""
    }
}

fun main(args: Array<String>) {
    val outer = Outer()

    println(outer.internal.property) // Compilation error: Unresolved reference: property
}

在这种情况下, 属性 值无法访问。

Object declarations

对象也可以有名称。这种object称为object declaration

object Oven {
  fun process(product: Bakeable) {
    println(product.bake())
  }
}

fun main(args: Array<String>) {
    val myAlmondCupcake = Cupcake("Almond")
    Oven.process(myAlmondCupcake)
}

对象是单例;你不需要实例化 Oven 来使用它。对象还可以扩展其他类型:

interface Oven {
  fun process(product: Bakeable)
}

object ElectricOven: Oven {
  override fun process(product: Bakeable) {
    println(product.bake())
  }
}

fun main(args: Array<String>) {
    val myAlmondCupcake = Cupcake("Almond")
    ElectricOven.process(myAlmondCupcake)
}

Companion objects

在类/接口中声明的 Objects 可以标记为 companion 对象。观察以下代码中 companion 对象的使用:

class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable {
  override fun name(): String { 
    return "cupcake" 
  } 

  companion object { 
    fun almond(): Cupcake { 
      return Cupcake("almond") 
    } 

    fun cheese(): Cupcake { 
      return Cupcake("cheese") 
    } 
  } 
}

现在,可以直接使用伴生对象中的方法,使用类名而不实例化它:

fun main(args: Array<String>) {
    val myBlueberryCupcake: BakeryGood = Cupcake("Blueberry")
    val myAlmondCupcake = Cupcake.almond()
    val myCheeseCupcake = Cupcake.cheese()
    val myCaramelCupcake = Cupcake("Caramel")
}

伴随对象的方法不能从实例中使用:

fun main(args: Array<String>) {
    val myAlmondCupcake = Cupcake.almond()
    val myCheeseCupcake = myAlmondCupcake.cheese() //Compilation error: Unresolved reference: cheese
}

Companion 对象可以在类外部用作名称为 Companion 的值:

fun main(args: Array<String>) {
    val factory: Cupcake.Companion = Cupcake.Companion
}

或者,Companion 对象可以有一个名称:

class Cupcake(flavour: String) : BakeryGood(flavour), Bakeable {
    override fun name(): String {
        return "cupcake"
    }

    companion object Factory {
        fun almond(): Cupcake {
            return Cupcake("almond")
        }

        fun cheese(): Cupcake {
            return Cupcake("cheese")
        }
    }
}

fun main(args: Array<String>) {
    val factory: Cupcake.Factory = Cupcake.Factory
}

它们也可以不带名称使用,如以下代码所示:

fun main(args: Array<String>) {
    val factory: Cupcake.Factory = Cupcake
}

不要被这种语法所迷惑。  Cupcake 不带括号的值是伴生对象; Cupcake() 是一个实例。

Generics


本节只是对泛型的简短介绍;稍后,我们将详细介绍它。

通用编程是一种风格编程专注于创建适用于一般问题的算法(以及附带的数据结构)。

Kotlin 支持泛型编程的方式是使用类型参数。简而言之,我们使用类型参数编写代码,然后在使用它们时将这些类型作为参数传递。 

以我们的 Oven 接口为例:

interface Oven {
  fun process(product: Bakeable)
}

烤箱是一台机器,所以我们可以更概括一下:

interface Machine<T> {
  fun process(product: T)
}

Machine<T> 接口定义了一个类型参数 T 和一个方法 process(T)

现在,我们可以用 Oven 来扩展它:

interface Oven: Machine<Bakeable>

现在,Oven 正在使用 Bakeable 类型参数扩展 Machine,所以process 方法现在将 Bakeable 作为参数。

Type alias


类型别名提供了一种定义names 已经存在的类型。类型别名有助于使复杂类型更易于阅读,还可以提供其他提示。

Oven 接口在某种意义上只是Machine<Bakeable>的名称:

typealias Oven = Machine<Bakeable>

我们的新类型别名 Oven 与我们良好的旧 Oven 接口完全相同。它可以扩展并具有 Oven 类型的值。

类型别名也可用于增强类型信息,提供与您的域相关的有意义的名称:

typealias Flavour = String

abstract class BakeryGood(val flavour: Flavour) {

它也可以用于集合:

typealias OvenTray = List<Bakeable>

它也可以与对象一起使用:

typealias CupcakeFactory = Cupcake.Companion

Nullable types


Kotlin 的主要特性之一是可空类型。 Nullable types 允许我们明确定义一个值是否可以包含或为 null:

fun main(args: Array<String>) {
    val myBlueberryCupcake: Cupcake = null //Compilation error: Null can not be a value of a non-null type Cupcake
}

这在 Kotlin 中无效;  Cupcake 类型不允许 null 值。要允许空值,myBlueberryCupcake 必须具有不同的类型:

fun main(args: Array<String>) {
    val myBlueberryCupcake: Cupcake? = null
}

本质上,Cupcake 是非空类型,Cupcake? 是可空类型。

在层次结构中,Cupcake是 Cupcake?的子类型。因此,在定义了 Cupcake? 的任何情况下,都可以使用 Cupcake,但反之则不行:

fun eat(cupcake: Cupcake?){
//  something happens here    
}

fun main(args: Array<String>) {
    val myAlmondCupcake = Cupcake.almond()

    eat(myAlmondCupcake)

    eat(null)
}

Kotlin 的编译器区分可空类型和非空类型的实例。

让我们以这些值为例:

fun main(args: Array<String>) {
    val cupcake: Cupcake = Cupcake.almond()
    val nullabeCupcake: Cupcake? = Cupcake.almond()
}

接下来,我们将对可空类型和非空类型调用 eat() 方法:

fun main(args: Array<String>) {
    val cupcake: Cupcake = Cupcake.almond()
    val nullableCupcake: Cupcake? = Cupcake.almond()

    cupcake.eat() // Happy days
    nullableCupcake.eat() //Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type Cupcake?
}

cupcake 上调用 eat() 方法非常简单;在 nullableCupcake 上调用 eat() 是编译错误。

为什么?对于 Kotlin,从可为空的值调用方法是危险的,潜在的 NullPointerException (NPE 来自现在开始)可以被抛出。因此,为了安全起见,Kotlin 将其标记为编译错误。

如果我们真的想调用一个方法或从一个可为空的值访问一个属性会发生什么?

好吧,Kotlin 为您提供了处理可空值的选项,并带有一个 catch ——所有这些都是显式的。从某种意义上说,Kotlin 是在告诉你, 告诉我你知道自己在做什么

让我们回顾一下一些选项(我们将在接下来的章节中介绍更多选项)。

Checking for null

检查 null 作为 if 块中的条件:

fun main(args: Array<String>) {
    val nullableCupcake: Cupcake? = Cupcake.almond()

    if (nullableCupcake != null) {
      nullableCupcake.eat()
    }
}

Kotlin 将做一个聪明的演员。在 if 块中,nullableCupcakeCupcake,而不是 纸杯蛋糕?;因此,可以访问任何方法或属性。

Checking for non-null types

这类似于 previous 之一,但它直接检查类型:

if (nullableCupcake is Cupcake) {
  nullableCupcake.eat()
}

它也适用于 when

when (nullableCupcake) {
  is Cupcake -> nullableCupcake.eat()
}

检查 null 和非 null 类型的这两个选项都有点冗长。让我们检查一下其他选项。

Safe calls

安全调用让您可以访问方法和属性 如果值不为空,则为可空值(在底层,在字节码级别,安全调用被转换为 if(x != null)):

nullableCupcake?.eat()

但是,如果你在表达式中使用它呢?

val result: String? = nullableCupcake?.eat()

如果我们的值为 null,它将返回 null,因此 result 必须具有 String? 类型。

这为在链上使用安全调用提供了机会,如下所示:

val length: Int? = nullableCupcake?.eat()?.length

The Elvis (?:) operator

如果为 null,则 Elvis 运算符 (?:) 返回 alternative 值值用于表达式:

val result2: String = nullableCupcake?.eat() ?: ""

如果 nullabluCupcake?.eat()null,则 ?:  ;运算符将返回替代值 ""

显然,Elvis 运算符可以与一系列安全调用一起使用:

val length2: Int = nullableCupcake?.eat()?.length ?: 0

The (!!) operator

代替 null 值, !! 运算符将 throw 一个 NPE:

val result: String = nullableCupcake!!.eat()

如果您可以处理 NPE, !! 运算符为您提供了一个非常方便的功能,即免费的智能转换:

val result: String = nullableCupcake!!.eat()

val length: Int = nullableCupcake.eat().length

如果 nullableCupcake!!.eat() 没有抛出 NPE,Kotlin 会将其类型从 Cupcake? 更改为 Cupcake 从下一行开始。

Kotlin's type system


类型系统是一组确定类型的规则 的语言结构。

(好的)类型系统将帮助您:

  • Making sure that the constituent parts of your program are connected in a consistent way
  • Understanding your program (by reducing your cognitive load)
  • Expressing business rules
  • Automatic low-level optimizations

我们已经覆盖了足够的基础来理解 Kotlin 的类型系统。

The Any type

Kotlin 中的所有类型都扩展自 Any 类型(稍等,实际上这不是真的,但为了便于解释,请耐心等待)。

我们创建的每个类和接口隐式 扩展Any。所以,如果我们写一个 methodAny 作为参数,它将收到任何值:

fun main(args: Array<String>) {

    val myAlmondCupcake = Cupcake.almond()

    val anyMachine = object : Machine<Any> {
      override fun process(product: Any) {
        println(product.toString())
      }
    }

    anyMachine.process(3)

    anyMachine.process("")

    anyMachine.process(myAlmondCupcake)    
}

可空值呢?让我们看一下:

fun main(args: Array<String>) {

    val anyMachine = object : Machine<Any> {
      override fun process(product: Any) {
        println(product.toString())
      }
    }

    val nullableCupcake: Cupcake? = Cupcake.almond()

    anyMachine.process(nullableCupcake) //Error:(32, 24) Kotlin: Type mismatch: inferred type is Cupcake? but Any was expected
}

Any 与任何其他类型相同,也有一个可以为空的对应物,Any?。  Any 扩展自 Any?。所以,最后,Any? 是 Kotlin 类型系统层次结构的顶级类。

Minimum common types

由于其类型推断和表达式评估,有时在 Kotlin 中存在 expressions 并不清楚返回的是哪种类型。大多数语言通过返回可能的类型选项之间的最小公共类型来解决这个问题。 Kotlin 采取了不同的路线。

让我们看一个模棱两可的表达式的例子:

fun main(args: Array<String>) {
    val nullableCupcake: Cupcake? = Cupcake.almond()

    val length = nullableCupcake?.eat()?.length ?: ""
}

length 有什么类型? Int 还是 String?不,length value 的类型是 Any。很合乎逻辑。 IntString 之间的最小通用类型是 Any。到目前为止,一切都很好。现在让我们看下面的代码:

val length = nullableCupcake?.eat()?.length ?: 0.0

按照该逻辑,在这种情况下,length 应该具有 Number type ( IntDouble 之间的通用类型,不是吗?

错了, length 仍然是 Any。在这些情况下,Kotlin 不会搜索最小通用类型。如果你想要一个特定的类型,它必须显式声明:

val length: Number = nullableCupcake?.eat()?.length ?: 0.0

The Unit type

Kotlin 没有返回 void 的方法(就像 Java 或 C 那样)。相反,一个方法(或者,准确地说,一个表达式)可以有一个 Unit 类型。

Unit 类型意味着调用 expression 是因为它的副作用,而不是它的回报。 classic Unit 表达式的示例是 println(),一种仅为其副作用而调用的方法。

Unit 与任何其他 Kotlin 类型一样,从 Any 扩展而来,并且可以为空。 Unit? 看起来很奇怪而且没有必要,但需要与类型系统保持一致。拥有一致的类型系统有几个优点,包括更好的编译时间和工具:

anyMachine.process(Unit)

The Nothing type

Nothing 是位于整个 Kotlin 层次结构的 bottom 的类型. Nothing 扩展了所有 Kotlin 类型,包括 Nothing?

但是,为什么我们需要 NothingNothing? 类型?

Nothing 表示无法执行的表达式(基本上是抛出异常):

val result: String = nullableCupcake?.eat() ?: throw RuntimeException() // equivalent to nullableCupcake!!.eat()

在 Elvis 运算符的一方面,我们有一个 String。另一方面,我们有 Nothing。因为 StringNothing 的共同类型是 String (而不是 < code class="literal">Any),值 result 是一个 String

Nothing 对编译器也有特殊意义。一旦在表达式上返回 Nothing type,之后的行将被标记为不可访问。

Nothing? 是空值的类型:

val x: Nothing? = null

val nullsList: List<Nothing?> = listOf(null)

Other types


类、接口和对象是 OOP 类型系统的一个很好的起点,但 Kotlin 提供了更多的构造,例如数据类、注释和枚举(还有一个名为密封类的附加类型,我们将在后面介绍) .

Data classes

创建主要目的是保存数据的类是 Kotlin 中的 common 模式(在其他语言中也是一种常见模式,想想JSON 或 Protobuff)。

为此,Kotlin 有一个特殊的类:

data class Item(val product: BakeryGood,
  val unitPrice: Double,
  val quantity: Int)

声明数据类,有一些限制:

  • The primary constructor should have at least one parameter
  • The primary constructor's parameters must be val or var
  • Data classes can't be abstract, open, sealed, or inner

有了这些限制,数据类带来了很多好处。

Canonical methods

Canonical methodsmethods任何。因此,Kotlin 中的所有实例都有它们。

对于数据类,Kotlin 创建了所有规范方法的正确实现。

方法如下:

  • equals(other: Any?): Boolean: This method compares value equivalence, rather than reference.
  • hashCode(): Int: A hash code is a numerical representation of an instance. When hashCode() is invoked several times in the same instance, it should always return the same value. Two instances that return true when they are compared with equals must have the same hashCode().
  • toString(): String: A String representation of an instance. This method will be invoked when an instance is concatenated to a String.

The copy() method

有时,我们希望重用 现有 实例的值。 copy() 方法允许我们创建数据类的新实例,覆盖我们想要的参数:

val myItem = Item(myAlmondCupcake, 0.40, 5)

val mySecondItem = myItem.copy(product = myCaramelCupcake) //named parameter

在这种情况下,mySecondItemunitPricequantity "literal">myItem,并替换 product 属性。

Destructuring methods

按照惯例,具有一系列 methods 的类的任何实例名为 component1() component2() 等可用于解构声明。

Kotlin 将为任何数据类生成这些方法:

val (prod: BakeryGood, price: Double, qty: Int) = mySecondItem

prod 值被初始化为 component1(), price 并返回 component2() ,依此类推。尽管前面的示例使用显式类型,但这些不是必需的:

val (prod, price, qty) = mySecondItem

在某些情况下,并非所有值都是必需的。所有未使用的值都可以替换为 (_):

val (prod, _, qty) = mySecondItem

Annotations

注释是一种将元信息附加 元信息(例如文档、配置等)的方法。

让我们看下面的示例代码:

annotation class Tasty

annotation 本身可以被注释以修改其行为:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Tasty

在这种情况下,可以在类、接口和对象上设置Tasty注解,并且可以在运行时进行查询。

有关选项的完整列表,请查看 Kotlin 文档。

注释可以有一个限制参数,它们不能为空:

@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class Tasty(val tasty:Boolean = true)

@Tasty(false)
object ElectricOven : Oven {
  override fun process(product: Bakeable) {
    println(product.bake())
  }
}

@Tasty
class CinnamonRoll : Roll("Cinnamon")

@Tasty
interface Fried {
  fun fry(): String
}

要在运行时查询注解值,我们必须使用反射 API(kotlin-reflect.jar 必须在您的类路径中):

fun main(args: Array<String>) {
    val annotations: List<Annotation> = ElectricOven::class.annotations

    for (annotation in annotations) {
        when (annotation) {
            is Tasty -> println("Is it tasty? ${annotation.tasty}")
            else -> println(annotation)
        }
    }
}

Enum

Kotlin 中的枚举是一种定义 一组常量值的方法。 枚举作为配置值非常有用,但不限于此

enum class Flour {
  WHEAT, CORN, CASSAVA
}

每个元素都是一个扩展 Flour 类的对象。

像任何对象一样,它们可以扩展接口:

interface Exotic {
  fun isExotic(): Boolean
}

enum class Flour : Exotic {
  WHEAT {
    override fun isExotic(): Boolean {
      return false 
    }
  },

  CORN {
    override fun isExotic(): Boolean {
      return false
    }
  },

  CASSAVA {
    override fun isExotic(): Boolean {
      return true
    }
  }
}

枚举也可以有抽象方法:

enum class Flour: Exotic {
  WHEAT {
    override fun isGlutenFree(): Boolean {
      return false
    }

    override fun isExotic(): Boolean {
      return false
    }
  },

  CORN {
    override fun isGlutenFree(): Boolean {
      return true
    }

    override fun isExotic(): Boolean {
      return false
    }
  },

  CASSAVA {
    override fun isGlutenFree(): Boolean {
      return true
    }

    override fun isExotic(): Boolean {
      return true
    }
  };

  abstract fun isGlutenFree(): Boolean
}

任何方法定义都必须在 (;) 分隔最后一个元素之后声明。

当枚举与 when 表达式一起使用时,Kotlin 的编译器会检查是否涵盖了所有情况(单独或使用 else):

fun flourDescription(flour: Flour): String {
  return when(flour) { // error
    Flour.CASSAVA -> "A very exotic flavour"
  }
}

在这种情况下,我们只检查 CASSAVA 而不是其他元素;因此,它失败了:

fun flourDescription(flour: Flour): String {
  return when(flour) {
    Flour.CASSAVA -> "A very exotic flavour"
    else -> "Boring"
  }
}

Summary


在本章中,我们介绍了 OOP 的基础知识以及 Kotlin 如何支持它。我们学习了如何使用类、接口、对象、数据类、注释和枚举。我们还探索了 Kotlin 类型系统,并了解它如何帮助我们编写更好、更安全的代码。

在下一章中,我们将从函数式编程的介绍开始。