读书笔记《functional-kotlin》Kotlin-数据类型、对象和类
在本章中,我们将介绍 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 是 Kotlin 中的 基础 类型.在 Kotlin 中,类是为实例提供状态、行为和类型的模板(稍后会详细介绍)。
要定义一个类,只需要一个名称:
class VeryBasic
VeryBasic
不是很有用,但仍然是有效的 Kotlin 语法。
VeryBasic
类没有任何状态或行为;尽管如此,您可以声明 VeryBasic
类型的值,如下面的代码所示:
fun main(args: Array<String>) { val basic: VeryBasic = VeryBasic() }
如您所见,basic
值具有 VeryBasic
类型。换一种说法,basic
是 VeryBasic
的一个实例。
在 Kotlin 中,可以推断类型;因此,前面的 example 等价于以下代码:
fun main(args: Array<String>) { val basic = VeryBasic() }
作为 VeryBasic
实例,basic
拥有 VeryBasic
的副本 ;type 的状态和行为,即无。好难过。
如前所述,类可以具有状态。在 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)
fun main(args: Array<String>) { val myBlueberryCupcake = Cupcake("Blueberry") val myAlmondCupcake = Cupcake("Almond") val myCheeseCupcake = Cupcake("Cheese") val myCaramelCupcake = Cupcake("Caramel") }
在 Kotlin 中,类的行为由方法定义。从技术上讲,method 是 member函数,因此,我们在接下来的章节中学到的任何关于函数的知识也适用于方法:
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()) }
没有什么令人兴奋的,但这是我们的第一种方法。稍后,我们会做更多有趣的事情。
随着我们在 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
类,具有 Cupcake
和 Biscuit 的共享行为和状态
类,我们让这两个类都扩展了 BakeryGood
。通过这样做,Cupcake
(和 Biscuit
)有一个 is-a< /em> 现在与 BakeryGood
建立关系;另一方面,BakeryGood
是 Cupcake
class 的父类或父类。
请注意, BakeryGood
被标记为 open
。这意味着我们专门设计了这个类来扩展。在 Kotlin 中,您无法扩展未open
的类。
将常见的行为和状态转移到父类的过程称为泛化。让我们看看下面的代码:
fun main(args: Array<String>) { val myBlueberryCupcake: BakeryGood = Cupcake("Blueberry") println(myBlueberryCupcake.eat()) }
让我们试用我们的新代码:
糟糕,不是我们所期待的。我们需要更多地折射它:
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" } }
我们声明了一个新方法,name()
;它应该被标记为 open
,因为我们将它设计为可以在其子类中进行更改。
在子类上修改方法的定义称为 override,这就是 why 两个子类中的name()
方法被标记为override
。
在层次结构中扩展类和覆盖行为的过程称为专业化< /strong>。
现在,我们可以拥有更多的烘焙商品!让我们看一下下面的代码:
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()) }
我们还可以创建具有更多属性和方法的类。
到目前为止,一切都很好。我们的面包店看起来不错。但是,我们当前的模型存在问题。我们来看下面的代码:
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" } }
抽象类是一个专门为扩展而设计的类。抽象类不能被实例化,这解决了我们的问题。
abstract
与 open
有何不同?
这两个修饰符都可以让我们扩展一个类,但是 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)
开放类和抽象类非常适合创建 层次结构,但有时它们还不够。一些子集可以跨越明显不相关的层次结构,例如,鸟类和类人猿是双足动物,它们都是动物和脊椎动物,但它们没有直接关系。这就是为什么我们需要不同的构造,而 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 withBakeryGood
and has an is-a relationship withBakeable
. - 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
与抽象类一样,对象 expressions 可以与接口一起使用:
val somethingFried = object : Fried { override fun fry(): String { return "TEST_3" } }
我们已经介绍了对象表达式,但还有更多关于对象的内容。 对象是自然单例(自然,我的意思是作为语言特性而不是作为行为模式实现,就像在其他语言中一样)。 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称为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) }
在类/接口中声明的 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()
是一个实例。
本节只是对泛型的简短介绍;稍后,我们将详细介绍它。
通用编程是一种风格编程专注于创建适用于一般问题的算法(以及附带的数据结构)。
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
作为参数。
类型别名提供了一种定义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
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 是在告诉你, 告诉我你知道自己在做什么。
让我们回顾一下一些选项(我们将在接下来的章节中介绍更多选项)。
fun main(args: Array<String>) { val nullableCupcake: Cupcake? = Cupcake.almond() if (nullableCupcake != null) { nullableCupcake.eat() } }
Kotlin 将做一个聪明的演员。在 if
块中,nullableCupcake
是 Cupcake
,而不是 纸杯蛋糕?
;因此,可以访问任何方法或属性。
if (nullableCupcake is Cupcake) { nullableCupcake.eat() }
它也适用于 when
:
when (nullableCupcake) { is Cupcake -> nullableCupcake.eat() }
检查 null 和非 null 类型的这两个选项都有点冗长。让我们检查一下其他选项。
安全调用让您可以访问方法和属性 如果值不为空,则为可空值(在底层,在字节码级别,安全调用被转换为 if(x != null)
):
nullableCupcake?.eat()
但是,如果你在表达式中使用它呢?
val result: String? = nullableCupcake?.eat()
如果我们的值为 null,它将返回 null,因此 result
必须具有 String?
类型。
这为在链上使用安全调用提供了机会,如下所示:
val length: Int? = nullableCupcake?.eat()?.length
如果为 null,则 Elvis 运算符 (?:
) 返回 alternative 值值用于表达式:
val result2: String = nullableCupcake?.eat() ?: ""
如果 nullabluCupcake?.eat()
为 null
,则 ?:
;运算符将返回替代值 ""
。
显然,Elvis 运算符可以与一系列安全调用一起使用:
val length2: Int = nullableCupcake?.eat()?.length ?: 0
类型系统是一组确定类型的规则 的语言结构。
(好的)类型系统将帮助您:
- 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 的类型系统。
Kotlin 中的所有类型都扩展自 Any
类型(稍等,实际上这不是真的,但为了便于解释,请耐心等待)。
我们创建的每个类和接口隐式 扩展Any
。所以,如果我们写一个 method 以 Any
作为参数,它将收到任何值:
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 类型系统层次结构的顶级类。
由于其类型推断和表达式评估,有时在 Kotlin 中存在 expressions 并不清楚返回的是哪种类型。大多数语言通过返回可能的类型选项之间的最小公共类型来解决这个问题。 Kotlin 采取了不同的路线。
让我们看一个模棱两可的表达式的例子:
fun main(args: Array<String>) { val nullableCupcake: Cupcake? = Cupcake.almond() val length = nullableCupcake?.eat()?.length ?: "" }
length
有什么类型? Int
还是 String
?不,length
value 的类型是 Any
。很合乎逻辑。 Int
和 String
之间的最小通用类型是 Any
。到目前为止,一切都很好。现在让我们看下面的代码:
val length = nullableCupcake?.eat()?.length ?: 0.0
按照该逻辑,在这种情况下,length
应该具有 Number
type ( Int
和 Double
之间的通用类型,不是吗?
错了, length
仍然是 Any
。在这些情况下,Kotlin 不会搜索最小通用类型。如果你想要一个特定的类型,它必须显式声明:
val length: Number = nullableCupcake?.eat()?.length ?: 0.0
Kotlin 没有返回 void
的方法(就像 Java 或 C 那样)。相反,一个方法(或者,准确地说,一个表达式)可以有一个 Unit
类型。
Unit
类型意味着调用 expression 是因为它的副作用,而不是它的回报。 classic Unit
表达式的示例是 println()
,一种仅为其副作用而调用的方法。
Unit
与任何其他 Kotlin 类型一样,从 Any
扩展而来,并且可以为空。 Unit?
看起来很奇怪而且没有必要,但需要与类型系统保持一致。拥有一致的类型系统有几个优点,包括更好的编译时间和工具:
anyMachine.process(Unit)
Nothing
是位于整个 Kotlin 层次结构的 bottom 的类型. Nothing
扩展了所有 Kotlin 类型,包括 Nothing?
。
但是,为什么我们需要 Nothing
和 Nothing?
类型?
val result: String = nullableCupcake?.eat() ?: throw RuntimeException() // equivalent to nullableCupcake!!.eat()
在 Elvis 运算符的一方面,我们有一个 String
。另一方面,我们有 Nothing
。因为 String
和 Nothing
的共同类型是 String
(而不是 < code class="literal">Any),值 result
是一个 String
。
Nothing
对编译器也有特殊意义。一旦在表达式上返回 Nothing
type,之后的行将被标记为不可访问。
Nothing?
是空值的类型:
val x: Nothing? = null val nullsList: List<Nothing?> = listOf(null)
类、接口和对象是 OOP 类型系统的一个很好的起点,但 Kotlin 提供了更多的构造,例如数据类、注释和枚举(还有一个名为密封类的附加类型,我们将在后面介绍) .
创建主要目的是保存数据的类是 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
orvar
- Data classes can't be abstract, open, sealed, or inner
有了这些限制,数据类带来了很多好处。
Canonical methods 是 methods 在任何
。因此,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. WhenhashCode()
is invoked several times in the same instance, it should always return the same value. Two instances that return true when they are compared withequals
must have the samehashCode()
.toString(): String
: AString
representation of an instance. This method will be invoked when an instance is concatenated to aString
.
有时,我们希望重用 现有 实例的值。 copy()
方法允许我们创建数据类的新实例,覆盖我们想要的参数:
val myItem = Item(myAlmondCupcake, 0.40, 5) val mySecondItem = myItem.copy(product = myCaramelCupcake) //named parameter
在这种情况下,mySecondItem
从 unitPrice
和 quantity
"literal">myItem,并替换 product
属性。
按照惯例,具有一系列 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
让我们看下面的示例代码:
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) } } }
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" } }