三、工作中的类:属性和功能

在阅读了关于类和对象的第 2 章后,现在是时候更多地关注属性和它们的类型,以及我们必须声明函数的选项和从函数内部可以做什么。这一章讨论了属性和函数声明,也讨论了面向对象语言的一个重要特性,继承,通过这个特性,一些类的属性和函数可以被其他类修改和重定义。我们还学习了可见性和封装,这有助于我们改进程序结构。

属性及其类型

属性是定义对象状态的数据容器或变量。类中的属性声明使用可选的可见性类型、可选的修饰符、不可变(不可改变)变量的关键字val或可变(可改变)变量的关键字var、名称、类型和初始值:

[visibility] [modifiers] val propertyName:PropertyType = initial_value
[visibility] [modifiers] var propertyName:PropertyType = initial_value

除此之外,一个类的构造函数中的任何属性由valvar直接自动添加到一个使用相同名称的隐藏属性中。在下面的段落中,我们将讨论类体中给定属性的所有可能选项。

简单属性

简单属性既不提供可见性也不提供任何修饰符,因此它们的声明如下

val propertyName:PropertyType = initial_value
var propertyName:PropertyType = initial_value

分别用于不可变和可变变量。以下是一些附加规则:

  • 如果在类或单例对象或伴随对象中,一个值在init{ }块中被赋值,那么= initial_value可以被省略。

  • 如果 Kotlin 可以通过给定的初始值推断出类型,那么:PropertyType可以省略。

这种简单的属性可以从外部通过instanceName.propertyName访问类,通过ObjectName.propertyName访问单例对象。在类或单例对象内部,只需使用propertyName来访问它。

让我们给第 2 章的NumberGuess项目中的GameUser类添加两个简单的属性。我们从构造函数中知道了名字和姓氏,因此派生一个首字母属性和一个全名属性可能会很有趣,如下所示:

class GameUser(val firstName:String,
           val lastName:String,
           val userName:String,
           val registrationNumber:Int,
           val birthday:String = "",
           val userRank:Double = 0.0) {
    val fullName:String
    val initials:String
    init {
        fullName = firstName + " " + lastName
        initials = firstName.toUpperCase() +
                   lastName.toUpperCase()
    }
}

这里你可以看到对于fullNameinitials我们只有val s,所以不可能给它们重新赋值。因为我们首先在init{ }中分配它们,所以在属性声明中省略= initial value是可能的。同样,因为所有的构造函数参数都有一个val前缀,所以它们都被传递给相应的属性,所以它们都是属性:firstNamelastNameuserNameregistrationNumberbirthday,userRank。为了访问它们,我们使用,例如:

val user = GameUser("Peter", "Smith", "psmith", 123, "1988-10-03", 0.79)
val firstName = user.firstName
val fullName = user.fullName

user.firstName = "Linda"赋值是不可能的,因为我们有不可变的val s。如果我们有var s,这将是允许的:

class GameUser(var firstName:String,
           var lastName:String,
           var userName:String,
           var registrationNumber:Int,
           var birthday:String = "",
           var userRank:Double = 0.0) {
    var fullName:String
    var initials:String
    init {
        fullName = firstName + " " + lastName
        initials = firstName.toUpperCase() +
                   lastName.toUpperCase()
    }
}

// somewhere inside a function in class MainActivity
val user = GameUser("Peter", "Smith", "psmith",
        123, "1988-10-03", 0.79)
user.firstName = "Linda"
console.log(user.fullName)

你能猜出产量吗?这个短程序打印了Peter Smith,虽然我们把名字改成了Linda。这个问题的答案是,全名是在init{ }中计算出来的,而且在我们改变名字后init{ }不会被再次调用,所以我们必须注意这一点。

注意

例如,您可以引入一个像setFirstName()这样的新函数,并相应地更新名字、全名和首字母。一个可能更简洁的变体是一个动态计算全名的函数,不使用单独的属性:fun fullName() = firstName + " " + lastName

这也是你应该尽可能选择val s 而不是var s 的原因之一;避免损坏的状态更容易。

练习 1

以下代码有什么问题?

class Triangle(color: String) {
    fun changeColor(newColor:String) {
        color = newColor
    }
}

属性类型

在示例代码片段中,我们已经看到了一些可以用于属性的类型。这是一份详尽的清单。

  • String:这是一个字符串。来自基本多语言平面(最初的 Unicode 规范)的每个字符都是类型Char(见后面)。补充字符使用两个Char元素。对于大多数实际用途和大多数语言来说,假设每个字符串元素都是一个单独的Char是一种可以接受的方法。

  • Int:这是一个整数。值的范围从 2,147,483,648 到 2,147,483,647。

  • Double:这是一个介于 4.94065645841246544 10-324 和 1.79769313486231570 10+308 之间的浮点数,正负符号均可。形式上,它是 IEEE 754 规范中的 64 位浮点值。

  • Boolean:这是一个布尔值,可以是真,也可以是假。

  • 任何类:属性可以保存任何类或单例对象的实例。这包括内置类、库中的类(由您使用的其他人构建的软件)以及您自己的类。

  • Char:这是一个单字符。Kotlin 中的字符使用 UTF-16 编码格式(来自原始 Unicode 规范的字符)来存储它们。

  • Long:这是一个扩展整数,取值范围在 9,223,372,036,854,775,808 和 9,223,372,036,854,775,807 之间。

  • Short:这是一个缩小了取值范围的整数。值从–32,768 到 32,767。您不会经常看到这种情况,因为对于大多数实际用例来说,Int是更好的选择。

  • Byte:这是一个从–128 到 127 的很小范围内的整数。这种类型经常用于低级操作系统函数调用。您可能不会经常使用这种类型,除非您对文件执行输入/输出(I/O)操作时会经常用到它。

  • Float:这是一个精度较低的浮点数。正负符号的范围从 1.40129846432481707 10-45到 3.4028234638528860 10+38。形式上,它是 IEEE 754 规范中的 32 位浮点值。除非存储空间或性能是个大问题,否则你通常会更喜欢Double而不是Float

  • 你可以使用任何类或接口作为类型,包括那些由 Kotlin 提供的内置的,来自你使用的其他程序,以及来自你自己的程序。

  • 枚举是一组无序文本值中可能值的数据对象。详见第 4 章

属性值分配

属性可以在四个地方赋值。第一个位置是在属性声明处,如

class TheClassName {
    val propertyName1:PropertyType1 = initial_value
    var propertyName2:PropertyType2 = initial_value
    ...
}

object SingletonObjectName {
    val propertyName1:PropertyType1 = initial_value
    var propertyName2:PropertyType2 = initial_value
    ...
}

class TheClassName {
    companion object {
        val propertyName1:PropertyType1 = initial_value
        var propertyName2:PropertyType2 = initial_value
        ...
    }
}

其中initial_value是可以转换为预期属性类型的任何表达式或文字。我们将在本章后面讨论文字和类型转换。

第二个可以赋值的地方是在init{ }块内:

// we are inside a class, a singleton object, or
// a companion object
init {
    propertyName1 = initial_value
    propertyName2 = initial_value
    ...
}

这只有在属性之前声明过的情况下才有可能,要么在类、单例对象或伴随对象中声明,要么在主构造函数声明中声明为var

只有当属性在一个init{ }块中被赋值时,你才能省略属性声明中的初始值赋值。因此,可以这样写

// we are inside a class, a singleton object, or
// a companion object
val propertyName1:PropertyType1
var propertyName2:PropertyType2
init {
    propertyName1 = initial_value
    propertyName2 = initial_value
    ...
}

可以给属性赋值的第三个地方是函数内部。很明显,这只对可变的var变量是可能的。那些变量必须已经用var propertyName:PropertyType =声明过了,对于赋值你必须省略var

// we are inside a class, a singleton object, or
// a companion object
var propertyName1:PropertyType1 = initial_value
...
fun someFunction() {
    propertyName1 = new_value
    ...
}

第四个可以赋值的地方是在类、单例对象或伴随对象之外。使用instanceName.ObjectName.并添加属性名,如下所示:

instanceName.propertyName = new_value
ObjectName.propertyName = new_value

这显然只可能发生在可变的。

练习 2

创建一个具有一个属性var a:Int的类A。执行赋值:(a)在声明中将其设置为1,( b)在init{ }块中将其设置为2,( c)在函数fun b(){}中将其设置为3,( d)在main函数中将其设置为4

文字

文字表示可用于属性赋值和内部表达式的固定值。数字是文字,但字符串和字符也是。以下是一些例子:

val anInteger = 42
val anotherInteger = anInteger + 7
val aThirdInteger = 0xFF473
val aLongInteger = 700_000_000_000L
val aFloatingPoint = 37.103
val anotherFloatingPoint = -37e-12
val aSinglePrecisionFloat = 1.3f
val aChar = 'A'
val aString = "Hello World"
val aMultiLineString = """First Line
    Second Line"""

3-1 列出了你可以用于 Kotlin 程序的所有可能的文字。

表 3-1。

文字

|

文字类型

|

描述

|

进入

| | --- | --- | --- | | 小数整数 | 整数 0,1,2,… | 0, 1, 2, …, 2147483647,–1, –2, …, –2147483648 如果您愿意,可以使用下划线作为千位分隔符,如 2_012 所示 | | 两倍精确浮动 | 之间的双精度浮点数 4.94065645841247.10 -324和 1.79769313486232.10 +308带有正号或负号 | 点符号:[s]三。场流分级法(field flow fractionation)其中[s]不为任何值,或为正值的+号,为负值的–号;III 是整数部分(任意位数),FFF 是小数部分(任意位数)科学符号:《气候公约》。FFFe[t]DDD 其中,[s]为空,正值为+,负值为 CCC。FFF 是尾数(一位或多位数字;那个。如果不需要的话,可以省略 FFF),[t]是零或+表示正指数,–表示负指数,DDD 是(十进制)指数(一位或多位) | | 茶 | 单个字符 | 使用单引号,如val someChar=‘A’。有许多特殊字符:写\t表示制表符、\b表示退格、\n表示换行符、\r表示回车、\表示单引号、\\表示反斜杠、\$表示美元符号。此外,您可以为任何 unicode 字符 XXXX(十六进制值)编写\ uXXXX 例如,\u03B1是一个 α | | 线 | 一串字符 | 使用双引号,如val someString = "Hello World".中的字符,适用与Char s 相同的规则,除了对于单引号,不使用前面的反斜杠,但是对于双引号,使用一个反斜杠:"Don't say \"Hello\""。在 Kotlin 中还有多行的原始的字符串文字:使用三重双引号,如在""" Here goes multiline contents"""中。这里里面的字符的转义规则不再适用(这就是名字 raw 的由来)。 | | 十六进制的整数 | 使用十六进制的整数 0,1,2,… | 0x 0.0x 1.0x 2、…、0x 9.0x a、0x x、0x x、0x x 10、…、0x 7 fff、–0x 1、–0x 2、…、–0x 800000000 | | 长的小数整数 | 具有扩展限制的长整数 0,1,2,… | 0, 1, 2, …, 9223372036854775807, –1, –2, …, –9223372036854775808 如果你愿意,你可以使用下划线作为千位分隔符,如 2_012L | | 长的十六进制的整数 | 使用十六进制的整数 0,1,2,…具有扩展的限制 | 0x0,0x1,0x2,…,0x9,0xA,0xB,…,0xF,0x10,…,0x 7 fffffffffffffffff,–0x 1,–0x 2,…,–0x 80000000000000 | | 浮动 | 单精度浮点数 | 与双精度浮点数相同,但在末尾加一个 f;例如val f = 3.1415f |

注意

记住,在十进制中 214 的意思是2 · 102+ 1 · 101+ 4 · 100。在十六进制系统中我们相应地有 0x13D 的意思2 · 162+ 3 · 161+ 13 · 160。字母 A,B,…,F 对应于 10,11,…,15。

至于类型兼容性,可以将普通整数赋给长整数属性,但不能反过来。您还可以将精度降低的浮点数赋给 double 属性,但不能反过来。不允许的赋值要求你使用一个转换(见第 5 章)。

要将文字分配给ShortByte属性,请使用整数,但要确保不超过限制。

单引号和三双引号String文字表示都展示了一个称为字符串模板的特性。这意味着一个以美元符号开始的表达式,后面是一个用花括号括起来的表达式,这个表达式被执行,其结果被传递给字符串。因此"4 + 1 = ${4+1}"的计算结果是字符串"4 + 1 = 5"。对于仅由单个属性名构建的简单表达式,可以省略花括号,如在"The value of a is $a"中。

练习 3

找到一种更短的书写方式

val a = 42
val s = "If we add 4 to a we get " + (a+4).toString()

避免字符串串联"" + ""

属性可见性

可见性是指程序的哪些部分可以从其他类、接口、对象或伴随对象中访问哪些函数和属性。我们将在本章后面的“类和类成员的可见性”一节中深入讨论可见性。

空值

特殊关键字null指定了一个可以用于任何可空属性的值。null as 值意味着未初始化、尚未决定或未定义。任何属性都可以为空,但是在声明中,您必须给类型说明符添加一个问号:

var propertyName:PropertyType? = null

这对于任何类型都是可能的,包括类,因此您可以编写,例如:

var anInteger:Int? = null
var anInstance:SomeClass? = null

对于可变可空的var属性,你也可以在任何时候分配null值:

var anInteger:Int? = 42
anInteger = null

像 Java 这样的其他语言允许任何对象类型为空,这经常会导致问题,因为null既没有属性也没有函数。例如,如果someInstance指向一个真实的对象,那么someInstance.someFunction(),表现良好。但是,如果您设置了someInstance = null,,则随后的someInstance.someFunction()是不可能的,因此会导致异常状态。因为 Kotlin 区分了普通属性和可空属性,所以 Kotlin 编译器可以更容易地避免这种状态不一致。

我们已经使用了所谓的解引用操作符(。)来访问函数和属性。为了提高稳定性,Kotlin 不允许。可空变量(或表达式)的运算符。相反,有一个安全调用变体?."在这种情况下,您必须使用——只有当运算符左侧的值不是null时,才会发生解引用。如果是null,操作员计算到null本身。看看这个例子:

var s:String? = "Hello"
val l1 = s?.length() // -> 5
s = null
val l2 = s?.length() // -> null

练习

以下哪一项是正确的?

  1. 您可以执行任务val a:Int = null.

  2. 可以写val a:Int? = null; val b:Long = a.toLong().

  3. 可以写val a:Int? = null; val b:Long? = a.toLong().

  4. 可以写val a:Int? = null; val b:Long? = a?.toLong().

属性声明修饰符

您可以在属性声明中添加以下修饰符:

  • const:增加const
const val name = ...

来声明将该属性转换成一个编译时间常数。属性的类型必须是IntLongShortDoubleFloatByteBooleanChar,String才能工作。您可以使用它来避免将常量放入伴随对象中。除此之外,关于使用,使用和不使用const没有区别。

  • lateinit:如果加上lateinit
lateinit var name:Type

其中Type是一个类、接口,或者String(IntLongShortDoubleFloatByteBooleanChar都不是)你告诉 Kotlin 编译器接受var存在或者不存在null。你可以这样写

class TheClass {
    lateinit var name:String
    fun someFunction() {
        val stringSize = name.length
    }
}

这会导致运行时错误,但不会导致编译时错误,从而阻碍了 Kotlin 可空性检查系统。如果变量以 Kotlin 编译器无法检测的方式初始化(例如,通过反射),那么使用lateinit是有意义的。除非你真的知道你想做什么,否则不要使用lateinit。顺便说一下,可以通过使用::name.isInitialized.来检查lateinit var是否已经初始化

成员函数

成员函数是负责访问它们的类、单例对象和伴随对象的元素。在函数内部,结构单元的状态被查询和/或更改。基于状态的计算可以通过获取输入并产生依赖于该输入和状态的输出来进行。函数也可以是不使用状态的纯函数,这意味着给定一些特定的输入参数,它们总是产生相同的输出。图 3-1 说明了各种可能性。

img/476388_1_En_3_Fig1_HTML.jpg

图 3-1。

功能

根据所使用的术语,函数有时也被称为操作方法

不返回值的函数

要声明一个不返回任何东西的函数,在 Kotlin 中,你要在一个类、一个单例对象或一个伴随对象的主体内部进行编写。

[modifiers]
fun functionName([parameters]) {
    [Function Body]
}

在函数体内,可以有任意数量的return语句退出函数。一个return在主体的末尾也是允许的,但不是必需的。

函数可能有也可能没有输入参数。如果他们没有,就写fun functionName() {}。如果输入参数存在,它们将如下声明:

parameterName1:ParameterType1,
parameterName2:ParameterType2, ...

注意

在 Kotlin 中,函数参数不能在函数体内重新分配。这不是一个缺点,因为在函数内部重新分配函数参数无论如何都被认为是不好的做法。

函数也可以有可变参数列表。这个特性被称为 varargs ,我们将在后面讨论它。我们稍后将讨论的另一个特性是默认参数。如果在函数调用中没有指定参数,这样的参数允许指定将使用的默认值。

例如,有参数和没有参数的两个简单函数声明如下所示:

fun printAdded(param1:Int, param2:Int]) {
    console.log(param1 + param2)
}
fun printHello() {
    console.log("Hello")
}

在接口内部——请记住,我们使用接口来描述需要做什么,而不是如何做——函数没有实现,因此不允许声明主体。对于不返回任何内容的函数,接口中的函数声明如下所示:

fun functionName([parameters])

您可以在函数声明前添加可选的[modifiers]来微调函数的行为,如下所示:

  • privateprotectedinternalpublic:这些是可见性修饰符。可见性将在本章后面的“类和类成员的可见性”一节中解释。

  • open:用它来标记一个类中的函数,使其可以被子类覆盖。有关详细信息,请参阅本章后面的“继承”一节。

  • override:使用这个来标记一个类中的一个函数,作为从一个接口或者从一个超类中重写一个函数。有关详细信息,请参阅本章后面的“继承”一节。

  • final override:同override,但表示禁止子类进一步覆盖。

  • abstract:抽象函数不能有体,有抽象函数的类不能实例化。您必须在子类中覆盖这样的函数,使它们具体化(这意味着“不抽象”它们)。有关详细信息,请参阅本章后面的“继承”一节。

您不能随意混合修改器。特别是对于可见性修饰符,只允许一个。但是,您可以将任何可见性修改器与此处列出的其他修改器的任何组合进行组合。如果需要多个修饰符,要使用的分隔符是空格字符。

注意,接口中的声明通常没有也不需要修饰符。例如,此处不允许除public,以外的可见性值。接口中的函数默认为public,由于它们本身在接口中没有实现,你可以默认认为它们是abstract,所以没有必要显式添加abstract

练习 5

以下函数有什么问题?

fun multiply10(d:Double):Double {
    d = d * 10
    return d
}

练习 6

以下函数有什么问题?

fun printOut(d:Double) {
    println(d)
    return
}

返回值的函数

要在 Kotlin 中声明一个类、单例对象或伴随对象中的返回值函数,在函数体中添加: ReturnType到函数头并编写

[modifiers]
fun functionName([parameters]): ReturnType {
    [Function Body]
    return [expression]
}

函数参数与不返回值的函数相同,前面讨论的修饰符也是如此。对于返回的值或表达式,Kotlin 必须能够将表达式的类型转换为函数返回类型。这种函数的一个例子如下:

fun add37(param:Int): Int {
    val retVal = param + 37
    return retVal
}

主体中可能有多个return语句,但是它们都必须返回预期类型的值。

注意

经验告诉我们,为了提高代码质量,最好总是在末尾使用一个return语句。

如果可能,也可以用一个表达式替换正文:

 [modifiers]
 fun functionName([parameters]): ReturnType = [expression]

如果表达式生成的类型是预期的函数返回类型,这里可以省略: ReturnType。Kotlin 因此可以推断

fun add37(param:Int) = param + 37

函数的返回类型是Int

同样,对于接口,函数没有实现,这种情况下的函数声明如下

fun functionName([parameters]): ReturnType

注意

实际上,Kotlin 内部让所有函数返回值。如果不需要返回值,Kotlin 会假设一个特殊的 void 类型,并将其称为Unit。如果您省略了: ReturnType并且函数不返回值,或者如果函数体根本没有return语句,则假定为Unit。如果,不管出于什么原因,它有助于提高你的程序的可读性,你甚至可以写fun name() : Unit {}来表达一个函数不返回任何有趣的值。

练习 7

以下是真的吗?

fun printOut(d:Double) {
    println(d)
}

与...相同

fun printOut(d:Double):Unit {
    println(d)
}

练习 8

创建以下类的较短版本:

class A(val a:Int) {
    fun add(b:Int):Int {
        return a + b
    }
    fun mult(b:Int):Int {
        return a * b
    }
}

练习 9

创建一个接口AInterface来描述练习 8 中的所有类A

访问屏蔽属性

在名称冲突的情况下,函数参数可能会屏蔽类属性。比方说,一个类有一个属性 xyz ,一个函数参数有一个完全相同的名字 xyz ,如

class A {
    val xyz:Int = 7
    fun meth1(xyz:Int) {
        [Function-Body]
    }
}

据说参数xyz屏蔽了函数体内的属性xyz。这意味着如果你在函数中写xyz,参数被寻址,而不是属性。不过,仍然可以通过在名称前添加this.来寻址属性:

class A {
    val xyz:Int = 7
    fun meth1(xyz:Int) {
        val q1 = xyz // parameter
        val q2 = this.xyz // property
        ...
    }
}

this指的是这个当前对象,所以this.xyz指的是这个当前对象的属性xyz,而不是函数规范中可见的xyz

注意

有些人用术语遮蔽的而不是遮蔽的来描述这样的性质。两者的意思是一样的。

练习 10

的产量是多少

class A {
    val xyz:Int = 7
    fun meth1(xyz:Int):String {
        return "meth1: " + xyz +
              " " + this.xyz
    }
}
fun main(args:Array<String>) {
    val a = A()
    println(a.meth1(42))
}

函数调用

给定一个实例、一个单例对象或一个伴随对象,调用函数如下:

instance.functionName([parameters]) // outside the class
functionName([parameters]) // inside the classObject.functionName([parameters]) // outside the objectfunctionName([parameters]) // inside the object

要从类内部调用伙伴对象的函数,你也只需使用functionName([parameters])。在类外,你可以在这里使用ClassName.functionName([parameters])

练习 11

给定这个类

class A {
    companion object {
        fun x(a:Int):Int { return a + 7 }
    }
}

描述如何在一个println()函数中从类外访问带有参数42的函数x()

函数命名参数

对于函数调用,可以使用参数名来提高可读性:

 instance.function(par1 = [value1], par2 = [value2], ...)

或者

 function(par1 = [value1], par2 = [value2], ...)

从类或对象内部。这里的parN是函数声明中确切的函数参数名。使用命名参数的另一个好处是,您可以使用任何您喜欢的参数排序顺序,因为 Kotlin 知道如何正确分配所提供的参数。您也可以混合使用未命名参数和命名参数,但是有必要将所有命名参数放在参数列表的末尾。

练习 12

给定这个类

class Person {
    var firstName:String? = null
    var lastName:String? = null
    fun setName(fName:String, lName:String) {
        firstName = fName
        lastName = lName
    }
}

创建一个实例,并使用命名参数将名称设置为John Doe

警告

在函数调用中使用命名参数极大地提高了代码的可读性。但是,如果您使用其他程序的代码,请小心,因为在新的程序版本中,参数名称可能会改变。

函数默认参数

如果在函数调用中省略,函数参数可能会有默认值。要指定默认值,您只需使用

parameterName:ParameterType = [default value]

在函数声明中。函数参数列表可以有任意数量的默认值,但它们都必须位于参数列表的末尾:

fun functionName(
    param1:ParamType1,
    param2:ParamType2,
    ...
    paramM:ParamTypeM = [default1],
    paramM+1:ParamTypeM+1 = [default2],
    ...) { ... }

要应用缺省值,只需在调用中省略它们。如果您省略列表末尾的 x 参数,最右边的 x 参数将采用默认值。这种排序顺序依赖性使得使用默认参数有点麻烦。但是,如果混合使用命名参数和缺省参数,使用缺省参数会增加函数的通用性。

练习 13

到函数声明

fun set(lastName:String,
    firstName:String,
    birthDay?:String,
    ssn:String?) { ... }

添加为默认值lastName = "", firstName = ""birthDay = nullssn = null。然后使用命名参数调用函数,只需指定lastName = "Smith"ssn = "1234567890"

函数 Vararg 参数

我们知道函数的存在是为了获取输入数据,并根据输入数据改变对象的状态,可能会产生一些输出数据。到目前为止,我们已经学习了固定参数列表,涵盖了所有可能用例的一个大的子集。但是,未知的、潜在的无限大小的列表呢?这样的列表被称为数组集合、,除了保存单个数据元素的类型之外,任何现代计算机语言都需要提供一种方法来处理这样的数据。我们将在第 9 章中更详细地介绍数组和集合,但是现在你应该知道数组和集合是完全成熟的类型,你可以将它们用于单个构造函数和函数参数,如……, someArray:Array<String>,

然而,在使用许多不同的单值参数和一个数组或集合参数之间有一个构造: varargs 。想法如下:作为函数声明的参数列表中的最后一个元素,添加一个vararg限定符,如

fun functionName(
    param1:ParamType1,
    param2:ParamType2,
    ...
    paramN:ParamTypeN,
    vararg paramV:ParamTypeV) { ... }

结果是一个能够接受 N + x 个参数的函数,其中 x 是从0到无穷大的任意数。然而,前提是所有的vararg参数都是由ParamTypeV指定的类型。当然,N 可能是0,所以一个函数可以有一个vararg参数:

fun functionName(varargs paramV:ParamTypeV) {
    ...
}

注意

Kotlin 实际上允许vararg参数出现在参数表的前面。然而,只有当vararg之后的下一个参数具有不同的类型时,Kotlin 才能在函数调用期间分发传入的参数。因为这会使调用结构变得复杂,所以最好避免这种vararg结构。

要调用这样一个函数,在调用中提供所有非vararg参数,然后是任意数量的vararg参数(包括零):

functionName(param1, param2, ..., paramN,
    vararg1, vararg2, ...)

作为一个简单的例子,我们创建一个函数,它将日期作为String,然后是任意数量的名字,再次作为String

fun meth(date:String, vararg names:String) {
    ...
}

现在可以进行以下调用:

meth("2018-01-23")
meth("2018-01-23", "Gina Eleniak")
meth("2018-01-23", "Gina Eleniak",
      "John Smith")
meth("2018-01-23", "Gina Eleniak",
      "John Smith", "Brad Cold")

你可以随意扩充名单。

现在的问题是:我们如何在函数内部处理vararg参数?答案是该参数是一个指定类型的数组,它具有我们在第 9 章中描述的所有特性,包括一个size属性和访问操作符[],以获取元素,如[0]、[1]等等。因此,如果我们使用带参数的示例函数(date:String, vararg names:String)并通过

meth("2018-01-23", "Gina Eleniak",
      "John Smith", "Brad Cold")

在函数内部,你将有date = "2018-01-23"vararg参数:

names.size = 3
names[0] = "Gina Eleniak"
names[1] = "John Smith"
names[2] = "Brad Cold")

练习 14

构建一个Club类并添加一个带有单个vararg参数names的函数addMembers。在函数内部,使用

println("Number: " + names.size)
println(names.joinToString(" : "))

打印参数。在类外创建一个main(args:Array<String>)函数,实例化一个Club,用“Hughes,John”,“Smith,Alina”,“Curtis,Solange”三个名字调用其addMembers()函数。

抽象函数

类内部的函数可以不用体来声明,并标记为abstract。这也将该类转换成一个抽象类,Kotlin 要求该类被标记为abstract可编译。

abstract class TheAbstractClass {
    abstract fun function([parameters])
    ... more functions ...
}

抽象类是介于接口和普通类之间的东西:它们为一些函数提供实现,而将其他函数抽象(未实现)以允许一些变化。因此,抽象类经常服务于某种“基础”实现,将细节留给一个或多个实现抽象功能的类。

抽象函数也使得函数的行为像接口函数,包括具有这种函数的类不能被实例化。你必须从这样一个实现所有功能的抽象类中创建一个子类,才能拥有可以实例化的东西。

abstract class TheAbstractClass {
    abstract fun function([parameters])
    ... more functions ...
}

// A subclass of TheAbstractClass ->
class TheClass : TheAbstractClass() {
    override fun function([parameters]) {
        // do something...
    }
}

这里TheClass可以被实例化,因为它实现了抽象函数。有关子类化的更多细节,请参阅本章后面的“继承”一节。

多态性

在一个类、一个单例对象、一个伴随对象或一个接口中,可以有几个函数使用相同的名称和不同的参数。这并没有什么神奇之处,但是这个特性在面向对象理论中有自己的名字:多态

如果有几个同名的函数,Kotlin 会通过查看参数来决定。调用代码指定实际使用哪个函数。这个调度过程通常是有效的,您不会看到任何问题,但是对于复杂的类和某个类的许多可能性,可能包括带有默认参数、接口和 varargs 的复杂参数列表,决定调用哪个函数是不明确的。在这种情况下,编译器会发出一条错误消息,您必须重新设计函数调用或您的类,这样一切才能正常工作。

多态性的用例是多种多样的;作为一个简单的例子,考虑一个具有几个add()函数的类,这些函数允许一个Int参数、Double参数或String参数。它的代码可以是:

class Calculator {
    fun add(a:Int) {
        ...
    }
    fun add(a:Double) {
        ...
    }
    fun add(a:String) {
        ...
    }
}

如果您现在用某个参数调用calc.add(),Kotlin 会获取参数的类型来找出要调用哪个函数。

警告

小心函数命名:多态性(即几个函数同名)不应该是偶然发生的,或者仅仅是出于技术原因。相反,从功能的角度来看,使用一个特定名称的所有函数应该服务于相同的目的。

本地功能

在 Kotlin 中,函数可以在函数内部声明。这样的函数被称为局部函数,它们可以从声明开始使用,直到封闭函数结束。

fun a() {
    fun b() {
        ...
    }
    ...
    b()
    ...
}

遗产

在现实生活中,继承意味着把自己的财产留给别人。在像 Kotlin 这样的面向对象的计算机语言中,想法是相似的。给定一个类A,,写class B : A表示我们将所有素材从类A给类B。除了拥有一个重新命名的A副本之外,这有什么好处呢?神奇的是,类B可以否决或者否决它从类A继承的部分素材。这可以用来改变它所继承的类的某些方面,以引入新的行为。

尽管这种对函数和属性的重写与现实生活中的继承有些不同,但继承类和重写特定的函数和属性是任何面向对象语言的核心方面之一。

从其他类继承的类

继承的精确语法是

open class A { ... }
class B : A() {
    [overriding assets]
    [own assets]
}

如果A有一个空的默认构造函数,并且

open class A([constructor parameters]) { ... }
class B : A([constructor parameters]) {
    [overriding assets]
    [own assets]
}

否则。当然,类B可能有自己的构造函数:

open class A([constructor parameters]) { ... }
class B([own constructor parameters]) :
      A([constructor parameters])
{
    [overriding assets]
    [own assets]
}

类声明中的open是 Kotlin 的专长。只有标有open的类才能用于继承。

注意

这是 Kotlin 制造商的一个有点奇怪的设计决定。它基本上禁用了继承,除非您将open添加到所有可能用于继承的类中。在现实生活中,开发人员很可能会忘记在他们的所有类中添加open,甚至拒绝在任何地方添加open,因为这感觉很讨厌,所以如果你的程序使用其他程序或库中的类,继承很可能会被破坏。不幸的是,没有出路,所以我们不得不接受这一点。当然,您可以在自己的类中任何需要的地方添加open

相对于彼此,用作继承基础的类也被称为超类,从其继承的类是子类。因此,在前面的代码中,AB的超类,BA的子类。

在我们的NumberGuess例子中,你可以看到,例如,我们的MainActivity类继承自AppCompatActivity。这种内置活动类的子类化对于任何与 Android 一起工作的应用都很重要。

构造函数继承

在子类构造的最开始,将调用超类的构造函数,包括init{ }块。如果超类提供了二级构造函数,那么子类也可以调用二级构造函数。这可以通过简单地使用二级构造函数的参数签名来实现:

open class A([constructor parameters]) {
    constructor([parameters2]) { ... }
}
class B : A([parameters2]) {
    ...
}

因为我们知道次级构造函数总是调用主构造函数,所以任何情况下的设计继承总是调用超类的主构造函数和init{ }块。如果子类提供了自己的init{ }块,这也是正确的,然后这个块被第二个调用。初学者往往会忘记这个事实,但如果你记住这一点,你可以避免一些困难。

在 Kotlin 中,子类可以从超类的构造函数中窃取属性。为此,valvar需要加上open,,如下例所示:

open class A(open val a:Int) {
}

然后,子类可以覆盖相关的参数:

open class A(open val a:Int) {
}
class B(override val a:Int) : A(42) {
    ...
}

这种被重写的属性将由以前使用其自己的属性原始版本的超类中的任何代码来处理。

练习 15

的输出会是什么

open class A(open val a:Int) {
    fun x() {
        Log.d("LOG",
              "A.x() -> a = ${a}")
    }
    fun q() {
        Log.d("LOG",
              "A.q() -> a = ${a}")
    }
}

class B(override val a:Int) : A(37) {
    fun y() {
        Log.d("LOG",
              "B.y() -> a = ${a}")
        q()
    }
}

// inside some activity function:
val b = B(7)
b.y()

请注意,Log.d("TAG",)将第二个参数打印到控制台。

覆盖功能

要覆盖超类的函数,在子类中你必须使用override修饰符并编写

open class A {
    open fun function1() { ... }
}
class B : A() {
    override
    fun function1() { ... }
}

同样,我们必须将open添加到超类中的函数,以使它有资格继承。当然,该函数可以有一个参数列表,并且超类和子类中的参数类型必须相同,以使重写正确工作。被覆盖的函数在子类中获得了一个新版本,但是原始版本并没有完全丢失。可以通过写来寻址原始函数

super.functionName(param1, param2, ...)

在子类中。

覆盖属性

Kotlin 有一个其他面向对象语言没有的特性。不仅可以覆盖函数,还可以覆盖属性。为此,这些属性需要在超类中标记为open,如

open class A {
    open var a:Int = 0
}

从此超类继承的类可以通过声明来重写该属性

class B : A() {
    override var a:Int = 0
}

使用这种符号,来自类BA内部的属性的任何使用都被类B中声明的属性所覆盖。该属性的行为就好像类A中的声明不再存在一样,并且A中以前使用该属性的“他们的”版本的函数将使用来自类B的属性。

练习 16

的输出会是什么

open class A() {
    private var g:Int = 99
    fun x() {
        Log.d("LOG", "A.x() : g = ${g}")
    }
    fun q() {
        Log.d("LOG", "A.q() : g = ${g}")
    }
}

class B : A() {
    var g:Int = 8
    fun y() {
        Log.d("LOG", "B.y() : g = ${g}")
        q()
    }
}

// inside some activity function:
val b = B()
b.x()
b.y()

注意Log是由自动包含在你的项目中的 Android 库提供的。如果第一次出现错误,请将光标放在它上面,然后按 Alt+Enter 以获得解决方法。你能猜到为什么类A中的属性g必须被声明为private,这意味着其他类不能看到或使用它吗?

练习 17

在练习 16 中,从属性声明中移除private,并使类B覆盖来自类A的属性g。输出会是什么?

访问超类素材

即使函数或属性在某个子类中被覆盖,如果在前面加上一个super.,您也可以从超类中访问原始版本,例如,在

open class A() {
    open var a:Int = 99
    open fun x() {
        Log.d("LOG", "Hey from A.x()")
    }
}

class B : A() {
    override var a:Int = 77
    override fun x() {
        Log.d("LOG", "Hey from A.x()")
    }
    fun show() {
        Log.d("LOG", "Property: " + a)
        Log.d("LOG", "Formerly: " + super.a)
        Log.d("LOG", "Function: ")
        x()
        Log.d("LOG", "Formerly: ")
        super.x()
    }
}

// inside some activity function:
val b = B()
b.show()

输出显示,从子类B中,我们可以使用被覆盖的和原始的属性和函数:

Property: 77
Formerly: 99
Function:
Hey from B.x()
Formerly:
Hey from A.x()

局部变量

局部变量是在某个函数中声明和使用的valvar变量;例如:

class TheClass {
    fun function() {
        ...
        var localVar1:Int = 7
        val localVar1:Int = 8
        ...
    }
}

这种局部变量从声明到函数结束都是有效的;这就是为什么他们被称为本地的。当然,它们被允许计算从函数返回某些东西所必需的任何表达式,因为它们在返回发生之前不会被销毁。

出于代码质量的原因,局部变量不应该屏蔽函数参数。如果你有一个任何类型的函数参数xyz,你不应该在函数内部使用名字xyz来声明一个局部变量。编译器允许这样做,但是它会发出一个关于隐藏的警告。

练习 18

下面哪个类是有效的?对于任何无效的类,描述问题是什么。

1\.    class TheClass {
          var a:Int = 7

          fun function() {
              val a = 7
          }
      }

2\.    class TheClass {
          fun function(a:String) {
              val a = 7
          }
      }

3\.    class TheClass {
          fun function() {
              println(a)
              val a = 7
          }
      }

4\.    class TheClass {
          fun function():Int {
              val a = 7
              return a - 1
          }
      }

5\.    class TheClass {
          fun function1():Int {
              val a = 7
              return a - 1
          }
          fun function2():Int {
              a = 8
              return a - 1
          }
      }

类和类成员的可见性

到目前为止,我们主要以一种字面上自由的方式谈论了类、单例对象和伴随对象(结构单元)以及它们的属性和功能:

class TheName { // or object or companion object
    val prop1:Type1
    var prop2:Type2
    fun function() {
        ...
    }
}

这里字面上的自由意味着以这种方式声明的结构单元、函数和属性可以从任何地方自由访问。在 Kotlin,这种可达性被称为公众可见性。你甚至可以用这种方式给它们添加关键字public来明确描述这种公共可见性。

public [class or (companion) object] TheName {
    public val prop1:Type1
    public var prop2:Type2
    public fun function() {
        ...
    }
}

然而,为了简洁起见,您通常会省略它,因为 public 是 Kotlin 中的默认可见性。

在 Kotlin,可以对能见度进行限制。乍一看,如果我们在任何地方都保持默认的公共可见性,这可能会更容易,因为任何东西都可以从任何地方访问,并且您不必考虑限制。然而,对于任何重要的项目,都有很好的理由考虑区分可见性。与之相关的关键术语是封装。我们这样说是什么意思?以模拟时钟为例。它显示时间,并提供了一种通过一些时钟控制来调整时间的方法。我们可以用两个函数对此建模,time()setTime():

class Clock {
    fun time(): String {
        ...
    }
    fun setTime(time:String) {
        ...
    }
}

从用户的角度来看,这就是与时钟“交谈”所需要的一切。时钟内部发生的事情是一个不同的故事:首先,为了调整时间,时钟需要从当前显示的时间中增加或减少一些时间。这是通过转动时钟的控制盘来实现的。第二,时针、分针和秒针的角度更全面地描述了时钟的当前状态。还有一个技术装置,对每一秒的滴答声做出反应。这对应于时钟的齿轮。我们还需要一个每秒触发事件的计时器,就像模拟时钟中的弹簧一样。最后,我们还需要在init{ }块中添加一些定时器初始化代码。考虑到所有这些因素,我们必须重写我们的类,使其如下所示:

class Clock {
    var hourAngle:Double = 0
    var minuteAngle:Double = 0
    var secondsAngle:Double = 0
    var timer:Timer = Timer()

    init {
        ...
    }

    fun time(): String {
        ...
    }

    fun setTime(time:String) {
        ...
    }

    fun adjustTime(minutes:Int) {
        ...
    }

    fun tick() {
        ...
    }
}

我们现在有两种访问素材的类:用户关心的外部类和用户不需要知道的内部类。封装通过引入一个新的可见性类 private,精确地处理了对客户隐藏内部的问题。顾名思义,私有属性和函数是结构单元的私有属性,外部的任何人都不需要关心它们,甚至不允许访问它们。要表明一个属性或函数是私有的,只需在它前面加上private关键字。对于我们的Clock类,我们这样写

class Clock {
    private var hourAngle:Double = 0
    private var minuteAngle:Double = 0
    private var secondsAngle:Double = 0
    private var timer:Timer = Timer()

    init {
        ...
    }

    fun time(): String {
        ...
    }

    fun setTime(time:String) {
        ...
    }

    private fun adjustTime(minutes:Int) {
            ...
    }

    private fun tick() {
            ...
    }
}

以这种方式分离功能和属性有以下好处:

  • 客户端不需要知道一个类或一个对象的内部功能的细节。它可以忽略任何标有private,的东西,减少干扰,更容易理解和使用这个类或对象。

  • 因为客户端只需要知道公共属性和函数,所以只要公共属性和函数以预期的方式运行,私有函数以及所有私有属性的实现就可以在任何时候自由地改变。因此,更容易改进类或修复缺陷。

回到NumberGuess游戏,我们已经使用了private作为可见性说明符。如果您只查看 activity 类的函数签名,您会看到:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?)
    override fun onSaveInstanceState(outState: Bundle?)
    fun start(v: View)
    fun guess(v:View)

    ///////////////////////////////////////////////////
    ///////////////////////////////////////////////////

    private fun putInstanceData(outState: Bundle?)
    private fun fetchSavedInstanceData(
          savedInstanceState: Bundle?)
    private fun log(msg:String)
}

这里你也清楚地看到我们需要onCreate()onSaveInstanceState()成为公共的,因为 Android 运行时需要从外部访问它们以进行生命周期处理。此外,start()guess()也需要是公共的,因为它们是通过按键从外部访问的。剩下的三个函数只能从类内部访问,因此这些函数具有private可见性。

除了publicprivate之外,还有两个可见性修改器:internalprotected。表 3-2 连同我们已经知道的两个一起描述了它们。

表 3-2。

能见度

|

能见度

|

素材

|

描述

| | --- | --- | --- | | public | 功能或属性 | (默认)该函数或属性在结构单元内外的任何地方都是可见的。 | | private | 功能或属性 | 该功能或属性仅在同一结构单元内部可见。 | | protected | 功能或属性 | 从同一个结构单元内部,以及从任何直接子类内部,函数或属性都是可见的。子类是通过class TheSubclass-Name : TheClassName {}声明的,它们继承了它们所继承的类的所有公共的和受保护的属性和函数。 | | internal | 功能或属性 | 函数和属性仅对来自同一程序的结构单元是公共的。对于来自其他编译的程序,尤其是来自您的软件中包含的其他程序,internal会得到和private一样的待遇。 | | public | 类、单例对象或伴随对象 | (默认)结构单元在程序内外的任何地方都是可见的。 | | private | 类、单例对象或伴随对象 | 结构单元仅在同一文件中可见。对于内部类,结构单元仅在封闭结构单元中可见。例如class A {``private class B {``... }``fun function() {``val b = B()``}``} | | protected | 类、单例对象或伴随对象 | 结构单元仅在封闭结构单元或其子类中可见。例如class A {``protected class B {``... }``fun function() {``val b = B()``}``}``class AA : A {``// subclass of A``fun function() {``val b = B()``}``} |

注意

对于小项目,除了默认的public之外,你不会关心任何可见性修饰符。对于较大的项目,添加可见性限制有助于提高软件质量。

自我参考:这个

在任何类的函数中,关键字this指的是当前的实例。我们知道,在类内部,我们可以通过使用它们的名字来引用同一个类中的函数和属性。如果可见的话,从类的外部,我们将不得不预先考虑实例名。您可以将this视为可以在类内部使用的实例名,因此,如果我们在一个函数中,引用来自同一个类的属性或函数,我们可以等效地使用

functionName()      -the same as-      this.functionName()
propertyName        -the same as-      this.propertyName

如果一个函数的参数与同一个类的属性同名,我们已经知道参数屏蔽了属性。我们还知道,如果我们加上this,我们仍然可以访问该属性。事实上,这是使用this的主要用例。在某些情况下,如果在函数或属性名前面加上this.,也会有助于提高可读性。例如,在设置实例属性的函数中,使用this有助于表达设置属性是函数的主要目的。

考虑一下这个:

var firstName:String = ""
var lastName:String = ""
var socialSecurityNumber:String = ""
...
fun set(fName:String, lName:String, ssn:String) {
    this.lastName = lName
    this.firstName = fName
    this.socialSecurityNumber = ssn
}

从技术上来说,没有这三个this.实例它也能工作,但是在这种情况下,它的表达能力就弱了。

将类转换为字符串

在 Kotlin 中,任何类都自动且隐式地从内置类Any继承。不用明确陈述,也没有办法阻止。这个超超类已经提供了几个函数,其中一个具有名称和返回类型toString():String。这个函数是一种多用途的诊断函数,经常被用来让一个实例在文本表示中告诉它的状态。这个函数是open,所以任何类都可以覆盖这个函数,让你的类以一种非正式的方式指示实例状态。

您可以在被覆盖的toString()中自由地做任何您想做的事情,但是大多数情况下会返回一个或另一个属性,例如在本例中:

class Line(val x1:Double, val y1:Double,
           val x2:Double, val y2:Double) {
{
    override fun toString() =
        "(${x1},${y1}) -> (${x2},${y2})"
}

通常你不想错过超类在它们自己的toString()实现中所做的事情,所以你可能更喜欢这样写:

class Line(val x1:Double, val y1:Double,
           val x2:Double, val y2:Double) {
{
    override fun toString() = super.toString()
        " (${x1},${y1}) -> (${x2},${y2})"
}

记住super.地址没有覆盖属性和函数。

练习 19

你能猜到如果你写这个会发生什么吗?

class Line(val x1:Double, val y1:Double,
           val x2:Double, val y2:Double) {
{
    override fun toString() = toString() +
        " (${x1},${y1}) -> (${x2},${y2})"
}

许多内置类在它们的toString()实现中已经提供了一些有用的输出,所以在大多数情况下,你不必仅仅为了提供合理的toString()输出而覆盖内置类。对于其他一些内置类和任何没有自己的toString()实现的类来说,toString()表示实例的内存位置。例如:

class A
val a = A()
println(a.toString())

将打印类似于@232204a1 的内容,根据具体情况,这些内容并不丰富。因此,对于诊断输出,提供一个toString()实现是一个好主意。