十、面向对象编程

在这一章中,我们会发现,在 Kotlin,类是一切事物的基础,事实上,一切事物都是类。

我们已经讨论过重用别人的代码,特别是安卓应用编程接口,但是在这一章,我们将真正了解这是如何工作的,并了解面向对象编程 ( OOP )以及如何使用它。

在本章中,我们将涵盖以下主题:

  • 面向对象程序设计简介以及封装、多态性和继承这三个关键主题
  • 基本的类,包括如何编写我们的第一个类,包括添加属性用于数据/变量封装和函数来完成事情
  • 探索可见性修改器,进一步帮助和细化封装
  • 了解构造器,它使能够让我们快速准备好我们的类,将其转化为可用的对象/实例
  • 编写一个基础类迷你应用,把我们在这一章学到的所有东西付诸实践

如果你试图记住这一章(或下一章),你将不得不在你的大脑中腾出很多空间,你可能会忘记一些真正重要的东西。

一个好的目标将是努力去实现它。这样,你的理解会变得更加全面。需要时,您可以参考本章(以及下一章)进行复习。

类型

如果你没有马上完全理解本章或下一章的所有内容也没关系!继续阅读,确保完成所有应用。

引入面向对象程序设计

第 1 章安卓和 Kotlin 入门中,我们提到 Kotlin 是一种面向对象的语言。一门面向对象的语言要求我们使用 OOP 它不是可选的额外内容,而是 Kotlin 的一部分。

让我们多了解一点。

OOP 到底是什么?

面向对象编程是一种编程方式,包括将我们的需求分解成比整体更容易管理的块。

每个块都是独立的,并且可能被其他程序重用,同时与其他块作为一个整体一起工作。

这些块就是我们一直称之为对象的东西。当我们计划/编码一个对象时,我们用一个类来做。一个类可以被认为是一个对象的蓝图。

我们实现了一个类的对象。这被称为一个类的实例。想一想房子的蓝图——你不能住在里面,但你可以用它建造房子;所以,你构建一个它的实例。通常,当我们为我们的应用设计类时,我们编写它们来表示现实世界的东西。

然而,OOP 不仅仅是这个。这也是一种做事的方式——一种定义最佳实践的方法。

OOP 的三个核心原则是封装多态继承。这些听起来可能很复杂,但是,一步一步来,相当简单。

封装

封装意味着通过只允许您选择的变量和函数被访问,来保持代码的内部工作不受使用它的代码的干扰。

这意味着您的代码总是可以被更新、扩展或改进,而不会影响使用它的程序,只要暴露的部分仍然以相同的方式被访问。

你可能还记得第一章安卓和 Kotlin的这段代码:

locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)

有了适当的封装,卫星公司或安卓应用编程接口团队是否需要更新他们的代码工作方式并不重要。如果getLastKnownLocation函数签名保持不变,我们就不用担心里面发生了什么。我们在更新前编写的代码在更新后仍然有效。

如果一辆车的制造商去掉车轮,让它成为一辆电动悬停车,如果它仍然有方向盘、油门和刹车踏板,驾驶它应该不是一个挑战。

当我们使用安卓应用编程接口的类时,我们是按照安卓开发人员设计它们的类来允许我们这样做的。

在本章中,我们将深入研究封装。

多态性

多态性允许我们编写更少依赖于我们试图操作的类型的代码,使我们的代码更加清晰和高效。多态性意味着多种形式。如果我们编码的对象可以是多种类型的东西,那么我们可以利用这一点。未来的一些例子将会说明这一点。一个类比会给你一个更真实的视角。如果我们有汽车工厂,只要通过改变给机器人和进入生产线的零件的指令,就可以制造面包车和小卡车,那么工厂就是多态的。

如果我们可以编写无需重新启动就能处理不同类型数据的代码,那不是很有用吗?我们会在第 11 章Kotlin中看到一些这样的例子。

我们还将在第 12 章中找到更多关于连接我们的 Kotlin 到用户界面和可空性的多态信息。

遗传

就像听起来的那样,继承意味着我们可以利用其他人的类的所有特性和好处(包括封装和多态性),同时针对我们的情况进一步细化他们的代码。实际上,我们已经这样做了,每次我们使用:操作符如下:

class MainActivity : AppCompatActivity() {

AppCompatActivity类本身继承自Activity。所以,每次我们创建一个新的安卓项目,我们都继承了Activity。我们可以走得更远,我们会看到它有多有用。

想象一下如果世界上最强壮的男人和世界上最聪明的女人在一起。他们的孩子很有可能从基因遗传中获得重大利益。Kotlin 中的继承让我们可以对另一个人的代码和我们自己的代码做同样的事情。

我们将在下一章研究继承的作用。

为什么会这样?

当仔细使用时,所有这些面向对象程序允许你添加新的特性,而不用担心它们如何与现有的特性交互。当你不得不改变一个类时,它的自包含(封装)特性对程序的其他部分意味着更少,甚至可能是零。这是封装部分。

你可以使用其他人的代码(比如安卓应用编程接口),而不知道或者甚至不关心它是如何工作的。想想安卓的生命周期,ToastLog,所有的 UI 小部件,听卫星等等。我们不知道,也不需要知道,他们在内部是如何运作的。作为一个更详细的例子,Button类有将近 50 个函数——我们真的想自己写所有这些,仅仅为了一个按钮吗?用别人的Button类会好很多。

面向对象程序允许你不费力地为高度复杂的情况编写应用。

您可以创建一个类的多个相似但不同的版本,而无需通过使用继承从头开始该类,并且由于多态性,您仍然可以在新对象中使用原对象类型的函数。

真的有道理。Kotlin 从一开始就考虑到了所有这些,所以我们被迫使用所有这些面向对象程序——然而,这是一件好事。让我们快速回顾一下课堂。

课程回顾

类是一堆代码的容器,这些代码可以包含函数、变量、循环,以及我们已经学过的所有其他 Kotlin 语法。一个类是 Kotlin 包的一部分,大多数包通常会有多个类。大多数情况下,尽管不总是这样,每个新类都将在它自己的.kt代码文件中定义,与类同名,就像到目前为止我们所有基于活动的类一样。

一旦我们写好了一个类,我们就可以用它来制作我们想要的任意多的对象。记住,类是蓝图,我们基于蓝图制作对象。房子不是平面图,就像物体不是类一样——它是由类构成的物体。一个对象是一个引用变量,就像一个字符串,稍后,我们将发现引用变量的确切含义。现在,让我们看一些实际的代码。

基础班

类有两个主要步骤。首先,我们必须声明我们的类,然后我们可以通过将它实例化为一个实际可用的对象来使它变得生动起来。请记住,类只是一个蓝图,在使用它做任何事情之前,您必须使用这个蓝图来构建一个对象。

宣告一个类

类可以有不同的大小和复杂性,这取决于它的目的。这是类声明的最简单的例子。

请记住,我们通常会在自己的文件中声明一个与类同名的新类。

在这本书的其余部分,我们将讨论该规则的一些例外情况。

让我们看一下声明类的三个例子:

// This code goes in a file named Soldier.kt
class Soldier

// This code would go in a file called Message.kt
class Message

// This code would go in a file called ParticleSystem.kt
class ParticleSystem

类型

请注意,我们将在本章末尾做一个完整的工作项目来练习。在下载包的Chapter10/Chapter Example Classes文件夹中也有本章所有理论示例的完整课程。

在前面的代码中首先要注意的是,我将三个类声明放在了一起。在真实代码中,每个声明都包含在自己的文件中,该文件的名称与类的名称相同,文件扩展名为.kt

要声明一个类,我们使用class关键字,后跟该类的名称。因此,我们可以算出,在前面的代码中,我们声明了一个名为Soldier的类、一个名为Message的类和一个名为ParticleSystem的类。

我们已经知道,类可以,并且经常,模拟现实世界的事情。因此,假设这三个假设类将模拟一名士兵(可能来自一个游戏)、一条信息(可能来自一个电子邮件或短信应用)和一个粒子系统(可能来自一个科学模拟应用)是安全的。

粒子系统是包含作为该系统一部分的单个粒子的系统。在计算中,它们被用来建模/模拟/可视化事物,例如化学反应/爆炸和粒子行为,也许是烟雾。在第 21 章线程和启动实时绘图应用中,我们将构建一个很酷的绘图应用,使用粒子系统让用户的绘图看起来栩栩如生。

然而,显而易见的是,像我们刚才看到的三个简单声明并不包含足够的代码来实现任何有用的功能。我们稍后将扩展类声明。首先,让我们看看如何使用我们已经声明的类。

实例化一个类

为了从我们的类中构建一个可用对象,我们将转向另一个代码文件。到目前为止,在整本书中,我们使用了AppCompatActivity类中的onCreate函数来演示不同的概念。虽然你可以在安卓的任何地方实例化一个类,但是由于生命周期函数,使用onCreate来实例化我们的类的对象/实例是很常见的。

看看下面的代码。我强调了新代码要关注的方面:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

         // Instantiating one of each of our classes
             val soldier = Soldier()
 val message = Message()
 val particleSystem = ParticleSystem()    

   } // End of onCreate function

}// End of MainActivity class

在前面的代码中,我们从之前声明的三个类中的每一个实例化了一个实例(创建了一个可用的对象)。让我们更仔细地检查语法。下面是实例化Soldier类实例的代码行:

val soldier = Soldier()

首先,我们决定是否需要更改实例。和常规变量一样,我们选择valvar。接下来,我们命名我们的实例。在前面的代码中,对象/实例被称为soldier,但是我们可以将其称为soldierXmarinejohn117,甚至squashedBanana。这个名字是任意的,但是,和变量一样,称它们为有意义的东西是有意义的。此外,与变量一样,在名称的开头使用小写字母,在名称的任何后续单词中使用大写字母是惯例,但不是必需的。

当使用valvar来声明类的实例时,它们之间的区别更加微妙和显著。我们将首先学习关于类的细节,在第 12 章中,我们将重新访问valvar,看看在我们的实例的引擎盖下发生了什么。

代码的最后一部分包含赋值运算符=,后跟类名Soldier,以及一组左右括号()

赋值运算符告诉 Kotlin 编译器将代码右侧的结果赋给左侧的变量。类型推断确定soldier属于Soldier类型。

类名后的好奇,但可能是熟悉的外观()暗示我们正在调用一个函数。我们是——它是一个特殊的函数,叫做构造函数,由 Kotlin 编译器提供。关于构造函数有很多要讨论的,所以我们将把对话推迟到本章的稍后部分。

现在,我们需要知道的是,下一行代码创建了一个Soldier类型的可用对象,称为soldier:

val soldier = Soldier()

请记住,面向对象的目标之一是我们可以重用我们的代码。我们不仅限于Soldier类型的一个对象。我们可以选择多少就有多少。看看下面这段代码:

val soldier1 = Soldier()
val soldier2 = Soldier()
val soldier3 = Soldier()

soldier1soldier2soldier3实例都是独立的、不同的实例。诚然,他们都是同一类型的人,但这是他们唯一的联系。你和你的邻居可能都是人类,但你们不是同一个人类。如果我们对soldier1做了什么,或者改变了什么,那就只有对soldier1做了什么。soldier2soldier3实例不受影响。事实上,可以实例化一整队Soldier对象。

OOP 的力量正在慢慢显露出来,但是在我们讨论的这个阶段,房间里的大象是我们的班级实际上根本不做任何事情。此外,我们的实例没有任何值(数据),因此我们也无法改变它们。

类有函数和变量(种)

当我们到达本章后面的类变量是属性一节时,我将解释这个有点神秘的(有点)标题。

我们在讨论 Kotlin 的过程中了解到的任何代码都可以用作一个类的一部分。这就是我们如何让我们的类有意义,让我们的实例真正有用。让我们扩展一下类声明,并添加一些变量和函数。

使用一个类的变量

首先,我们将向我们的空Soldier类添加一些变量,如下面的代码所示:

class Soldier{

    // Variables
    val name = "Ryan"
    val rank = "Private"
    val missing = true
}

请记住,前面的所有代码都将放在一个名为Soldier.kt的文件中。现在我们有了一个包含一些成员变量的类声明,我们可以使用它们,如下面的代码所示:

// First declare an instance of Soldier called soldier1
val soldier1 = Soldier()

// Now access and print each of the variables  
Log.i("Name =","${soldier1.name}")
Log.i("Rank =","${soldier1.rank}")
Log.i("Missing =","${soldier1.missing}")

如果将代码置于onCreate函数中,将在 logcat 窗口中产生以下输出:

Name =: Ryan
Rank =: Private
Missing =: true

在前面的代码中,我们以通常的方式实例化了Soldier类的一个实例。但是现在,因为Soldier类有一些带值的变量,我们可以使用点语法访问这些值:

instanceName.variableName

或者,我们可以通过使用这个特定的示例来访问这些值:

soldier1.name
soldier1.rank
// Etc..

为了清楚起见,我们使用实例名,而不是类名:

Soldier.name // ERROR!

类型

像往常一样,在我们继续进行的过程中,我们会涉及一些例外和变化。

如果我们想改变变量值,我们可以使用完全相同的点语法。当然,如果你还记得第七章Kotlin 变量、运算符和表达式的话,可以更改的变量需要声明为var,而不是val。这是重新制作的Soldier类,这样我们可以稍微改变一下使用方式:

class Soldier{

    // Member variables
    var name = "Ryan"
    var rank = "Private"
    var missing = true
}

现在,我们可以使用点语法操纵变量的值,就像它们是常规的var变量一样:

// First declare an instance of Soldier called soldier1
val soldier1 = Soldier()

// Now access and print each of the variables  
Log.i("Name =","${soldier1.name}")
Log.i("Rank =","${soldier1.rank}")
Log.i("Missing =","${soldier1.missing}")

// Mission to rescue Private Ryan succeeds
soldier1.missing = false;

// Ryan behaved impeccably
soldier1.rank = "Private First Class"

// Now access and print each of the variables  
Log.i("Name =","${soldier1.name}")
Log.i("Rank =","${soldier1.rank}")
Log.i("Missing =","${soldier1.missing}")

前面的代码将在 logcat 窗口中产生以下输出:

Name =: Ryan
Rank =: Private
Missing =: true
Name =: Ryan
Rank =: Private First Class
Missing =: false

在前面的输出中,首先我们看到和之前一样的三行,然后我们又看到三行表示 Ryan 不再缺失,已经提升到Private First Class

使用一个类的函数和变量

现在我们可以给我们的班级提供数据,是时候给他们提供他们能做的事情,让他们变得更有用了。为了实现这一点,我们可以给我们的类函数。请看一下Soldier类的扩展代码。我已经将变量恢复到val并突出显示了新代码:

class Soldier{

    // members
    val name = "Ryan"
    val rank = "Private"
    val missing = true

    // Class function
 fun getStatus() {
 var status = "$rank $name"
 if(missing){
 status = "$status is missing!"
 }else{
 status = "$status ready for duty."
 }

 // Print out the status
 Log.i("Status",status)
 }
}

getStatus函数中的代码声明了一个名为status的新String变量,并使用包含在rankname中的值对其进行初始化。然后,它用if表达式检查missing中的值,并根据缺失是true还是false追加is missingready for duty

然后,我们可以使用这个新函数,如下面的代码所示:

val soldier1 = Soldier()
soldier1.getStatus()

和以前一样,我们创建了一个Soldier类的实例,然后在那个实例上使用点语法来调用getStatus函数。前面的代码将在 logcat 窗口中产生以下输出:

Status: Private Ryan is missing!

如果我们将missing的值更改为false,将产生以下输出:

Status: Private Ryan ready for duty.

请注意,类中的函数可以采用我们在第 9 章Kotlin 函数中讨论的任何形式。

如果你认为所有这些类的东西都很棒,但同时又显得有点死板和不灵活,那么你是对的。如果都叫 Ryan,都不见了,有几个、几百个、几千个Soldier实例有什么意义?当然,我们已经看到我们可以使用var变量,然后改变它们,但这仍然可能是尴尬和冗长的。

我们需要在每个实例中更好地操作和初始化数据的方法。而且,如果我们回想一下这一章的开头,当时我们简要讨论了封装的主题,那么我们也会意识到,我们不仅需要允许代码操作我们的数据,还需要控制这种操作何时以及如何发生。

为了获得这些知识,我们需要更多地了解类中的变量,然后了解更多关于封装和可见性的细节,最后揭示当我们实例化类的实例时,我们在代码末尾看到的那些类似函数的括号()是怎么回事。

类变量是属性

原来在 Kotlin 类中,变量不仅仅是我们已经了解过的普通旧变量。它们是属性。到目前为止,我们所学的关于如何使用变量的一切仍然适用,但是一个属性不仅仅是一个值。它有获取器设置器T8】和一个隐藏在幕后的名为字段的特殊类变量。

getter 和 setters 可以认为是编译器自动生成的特殊函数。事实上,我们已经在不知情的情况下使用了它们。

当我们在类中声明的属性/变量上使用点语法时,Kotlin 使用 getter 来“获取”值。当我们使用点语法设置值时,Kotlin 使用 setter。

当我们使用刚才看到的点语法时,不能直接访问字段/变量本身。这种抽象的原因是为了帮助封装。

如果你以前用另一种面向对象语言(可能是 Java 或 C++)做过一些编程,这可能会令人困惑,但是如果你使用了更现代的面向对象语言(可能是 C#),那么这对于你来说并不完全是新的。如果 Kotlin 是你的第一语言,那么你可能比之前有经验的人更有优势,因为你没有背负之前学习的包袱。

而且,正如你可能猜到的,如果变量是var,那么提供一个 getter 和 setter,但是如果是val,那么只提供一个 getter。因此,当Soldier类中的变量(从现在开始,我们大部分时间称之为属性)是var时,我们可以获取并设置它们,但是当它们是val时,我们只能获取它们。

Kotlin 给了我们覆盖这些获取器和设置器的灵活性,以便改变当我们获取和设置属性及其相关字段的值时会发生什么。

类型

当一个属性使用一个字段时,它被称为后备字段。正如我们将看到的,有些属性不需要支持字段,因为它们可以依赖于 getters 和 setters 中代码的逻辑来使它们有用。

在这一点上,一些使用字段的例子会让事情变得更清楚。

使用属性及其吸气剂、沉降剂和字段的示例

我们可以使用获取器和设置器来控制可以分配给其支持字段的值的范围。例如,考虑下一个添加到Soldier类的代码:

var bullets = 100
get() {
   Log.i("Getter being used","Value = $field")
   return field
}
set(value) {
   field = if (value < 0) 0 else value
   Log.i("Setter being used","New value = $field")
}

前面的代码添加了一个名为bullets的新var属性,并将其初始化为 100。然后我们看到一些新的代码。getter 和 setter 被覆盖。将代码从 getter 和 setter 中剥离出来,以最简单的形式看到这一点:

get() {
   //.. Executes when we try to retrieve the value
}
set(value) {
   //.. Executes when we try to set the value 
}

为了清楚起见,当我们通过Soldier类的实例访问 bullet 的值时,getter 和 setter 中的代码会执行。看看在接下来的代码中这是如何发生的:

// In onCreate or some other function/class from our app
// Create a new instance of the Soldier class
val soldier = Soldier()
// Access the value of bullets
Log.i("bullets = ","${soldier.bullets}")// Getter will execute
// Reduce the number of bullets by one
soldier.bullets --
Log.i("bullets =","${soldier.bullets}")// Setter will execute

在前面的代码中,我们首先创建一个Soldier类的实例,然后获取存储在bullet属性中的值并打印该值。这将触发 getter 代码执行。

接下来,我们将bullet属性存储的值减 1。任何试图更改属性所保持的值的操作都将触发 setter 中的代码。

如果我们执行前面的四行代码,我们将在 logcat 窗口中获得以下输出:

Getter being used: Value = 100
bullets =: 100
Getter being used: Value = 100
Setter being used: New value = 99
Getter being used: Value = 99
bullets =: 99

在我们创建了一个名为soldierSoldier实例之后,我们使用Log.i将该值打印到 logcat 窗口中。当此代码访问由属性存储的值时,getter 代码运行并输出以下内容:

Getter being used: Value = 100

然后,getter 使用下一行代码将值返回给Log.i函数:

return field

当我们创建该属性时,Kotlin 创建了一个支持字段。我们在 getter 或 setter 中访问支持字段的方式是使用名称field。因此,前一行代码的工作方式与它在函数中的工作方式相同,并返回允许调用代码中的Log.i调用打印该值的值,我们将获得下一行输出:

bullets =: 100

下一行代码可能是最有趣的。这里还是为了方便参考:

soldier.bullets --

我们可能猜测这只是触发 setter 执行,但是如果我们检查 logcat 中的下两行输出,我们可以看到已经生成了以下两行输出:

Getter being used: Value = 100
Setter being used: New value = 99

递减(或递增)的动作需要使用 getter(知道要递减什么),然后使用 setter 来更改值。

注意设置器有一个名为value的参数,我们可以在设置器的主体中引用,就像一个常规的函数参数一样。

接下来,使用实例输出bullets属性所保持的值,我们可以看到再次使用了 getter,并且输出是从类中的 getter 代码和使用实例的代码(类外)生成的。接下来再次显示最后两行输出:

Getter being used: Value = 99
bullets =: 99

现在我们可以看另一个使用吸气剂和沉降剂的例子。

如前所述,有时属性根本不需要支持字段。有时允许 getters 和 setters 中的逻辑处理通过属性访问的值就足够了。检查下面的代码,我们可以将其添加到Soldier类中来演示这一点:

var packWeight = 150
val gunWeight = 30
var totalWeight = packWeight + gunWeight
   get() = packWeight + gunWeight

在前面的代码中,我们创建了三个属性:一个名为packWeightvar 属性,我们将使用即将创建的实例来更改它;一个名为gunWeightval属性,我们永远不需要更改它;以及另一个名为totalWeightvar属性,它被初始化为packWeight + gunWeight。有趣的是,我们覆盖了totalWeight的 getter,以便它使用packWeight + gunWeight重新计算它的值。接下来,看看我们如何将这些新属性用于Soldier类的实例,然后我们将查看输出:

// Create a soldier
val strongSoldier = Soldier()

// Print out the totalWeight value
Log.i("totalWeight =","${strongSoldier.totalWeight}")

// Change the value of packWeight
strongSoldier.packWeight = 300

// Print out the totalWeight value again
Log.i("totalWeight =","${strongSoldier.totalWeight}")

在前面的代码中,我们创建了一个名为strongSoldierSoldier实例。接下来,我们将totalWeight的值打印到日志中。第三行代码将packWeight的值更改为300,然后最后一行代码打印出totalWeight的值,这将使用我们被覆盖的 getter。下面是这四行代码的输出:

totalWeight =: 180
totalWeight =: 330

从输出中我们可以看到totalWeight值完全依赖于packWeightgunWeight中存储的值。第一行输出是packWeight ( 150)加到gunWeight ( 30)值的起始值,第二行输出等于packWeight加到gunWeight的新值。

就像函数一样,这个非常灵活的属性系统会引发一些问题。

何时使用超控吸气剂和设置剂

何时利用这些不同的技术伴随着实践和经验;对于特定技术何时合适没有硬性规定。在这个阶段,只需要理解在类的主体中声明的变量(在函数之外)实际上是属性,属性是通过 getters 和 setters 访问的。这些 getters 和 setters 对实例的用户来说是不透明的,除非被类的程序员重写,否则默认情况下由编译器提供。这就是封装的本质;该类的程序员控制该类的工作方式。属性提供对其相关值(称为支持字段)的间接访问,尽管有时不需要该支持字段。

类型

通过将属性称为变量来简化讨论是可以的(我有时也会这样做)。当吸气剂、设置剂和场与手头的讨论无关时,尤其如此。

在下一节中,我们将看到更多使用 getters 和 setters 的方法,所以让我们继续讨论可见性修饰符。

可见性修改器

可见性修饰符用于控制变量、函数甚至整个类的访问/可见性。正如我们将看到的,变量、函数和类可能具有不同的访问级别,这取决于代码中试图从哪里进行访问。这允许类的设计者实践良好的封装,并使他们选择的功能和数据对类的用户可用。作为一个稍微有点做作但有用的例子,曾经与卫星通话并获取全球定位系统数据的一个班级的设计者不允许访问dropOutOfTheSky功能。

这是 Kotlin 中的四个访问修饰符。

公众

将类、函数和属性声明为public意味着它们根本没有被隐藏/封装。事实上,默认可见性是public,因此,我们到目前为止看到和使用的一切都是公开的。我们可以通过在所有类、函数和属性声明之前使用public关键字来明确这一点,但这不是必需的。当某个东西被声明public(或者保留默认值)时,不使用封装。这只是我们偶尔想要的。通常,公开类的核心功能的类的函数将被声明为公共的。

二等兵

我们接下来要讨论的访问修饰符是private。属性、函数和类可以通过在声明前加上private关键字来声明private,如下一个假设代码所示:

private class SatelliteController {
   private var gpsCoordinates = "51.331958,0.029057"

   private fun dropOutOfTheSky() {
   }
}

SatelliteController类被声明为private,这意味着它只在同一个文件中可用(可以实例化)。尝试实例化一个实例(可能来自onCreate,将导致以下错误:

Private

这就提出了这个类到底能不能用的问题。将一个类声明为private比使用我们将继续讨论的剩余修饰符之一要少得多,但是它确实发生了,并且有各种技术使它成为一种可行的策略。然而,更有可能的是,一个SatelliteController类将被声明为具有更易接近的public可见性。

接下来,我们有一个名为gpsCoordinatesprivate属性。假设我们将SatelliteController类更改为公共类,那么我们可以实例化它并继续我们的讨论。即使将SatelliteController 声明为public(或者保留默认值),私有的gpsCoordinates属性对于该类的实例仍然不可见,如下面的截图所示:

Private

正如我们在前面的截图中所看到的那样,gpsCoordinates属性是不可访问的,因为它是private,并且,正如我们在本章前面对属性的讨论中所看到的,当属性保持默认值时,它是可访问的。这些访问修饰符的要点是,类的设计者可以选择何时公开以及公开什么。全球定位系统卫星很可能希望共享全球定位系统坐标。然而,它也很可能不希望类的用户在计算坐标时扮演任何角色。这表明我们希望类的用户能够读取数据,但不能写入/更改数据。这是一个有趣的情况,因为人们的第一反应可能是将该房产变为val房产。这样,用户可以获得数据,但不能更改它。这样做的问题是,GPS 坐标显然会改变,它需要是一个var属性,而不是一个从类外可以改变的var属性。

当我们将一个属性声明为private时,Kotlin 会自动生成 getter 和 setter private。我们可以通过重写 getter 和/或 setter 来改变这种行为。为了解决我们需要一个var属性的问题,这个属性在类外不可更改,在类外可读,在类内可更改,我们将保留默认的 setter,这样它就永远不会在外部更改,并覆盖 getter,这样它就可以在外部读取。看看这个SatelliteController班的重写:

class SatelliteController {
    var gpsCoordinates = "51.331958,0.029057"
    private set

    private fun dropOutOfTheSky() {
    }
}

在前面的代码中,SatelliteController类和gpsCoordinates属性是public。此外,gpsCoordinatesvar属性,因此是可变的。但是,仔细看属性声明后的代码行,因为它将 setter 设置为private,这意味着类外的代码不能访问它来更改它;但是因为它是一个var 属性,类内的代码可以对它做任何它喜欢的事情。

我们现在可以在onCreate函数中编写以下代码来使用该类:

// This still doesn't work which is what we want
// satelliteController.gpsCoordinates = "1.2345, 5.6789"

// But this will print the gpsCoordinates
Log.i("Coords=","$satelliteController.gpsCoordinates")

既然代码将 setter 设为私有,我们就不能从实例中更改值,但是我们可以愉快地阅读它,如前面的代码所示。请注意,设置器不能改变它们的可见性,但是可以(正如我们第一次讨论属性时看到的)覆盖它们的功能。

接下来讨论dropOutOfSky功能的功能,这是private,完全不可访问。只有SateliteController类中的代码才能调用该函数。如果我们希望类的用户能够访问一个函数,正如我们已经看到的那样,我们只需将它保留在默认可见性。SatelliteController类可能有如下代码所示的函数:

class SatelliteController {
    var gpsCoordinates = "51.331958,0.029057"
    private set

    private fun dropOutOfTheSky() {
    }

    fun updateCoordinates(){
        // Recalculate coordinates and update
        // the gpsCoordinates property
        gpsCoordinates = "21.123456, 2.654321"

        // user can now access the new coordinates
        // but still can't change them
    }
}

在之前的代码中,增加了一个公共的updateCoordinates函数。这允许类的实例使用以下代码:

satelliteController.updateCoordinates()

之前的代码将触发updateCoordinates函数的执行,这将导致类在内部更新属性,然后可以访问该属性并提供新值。

这就引出了一个问题:什么数据应该是私有的?应该使用的可见性水平可以部分通过常识、部分通过经验、部分通过提问“谁真正需要访问这些数据以及访问到什么程度?”在本书的剩余部分,我们将练习这三件事。下面是一些更假设的代码,显示了SatelliteController类的一些私有数据和更多私有函数:

class SatelliteController {
    var gpsCoordinates = "51.331958,0.029057"
    private set

    private var bigProblem = false

    private fun dropOutOfTheSky() {
    }

    private fun doDiagnostics() {
      // Maybe set bigProblem to true
      // etc
    }

    private fun recalibrateSensors(){
      // Maybe set bigProblem to true
      // etc
    }

    fun updateCoordinates(){
        // Recalculate coordinates and update
        // the gpsCoordinates property
        gpsCoordinates = "21.123456, 2.654321"

        // user can now access the new coordinates
        // but still can't change them
    }

    fun runMaintenance(){
        doDiagnostics()
        recalibrateSensors()

        if(bigProblem){
            dropOutOfTheSky()
        }

    }
}

在前面的代码中,有一个新的私有Boolean属性叫做bigProblem。只能在内部访问。它甚至不能从外部读取。有三个新的功能,一个名为runMaintenance的公共财产,运行两个私人功能,doDiagnosticscalibrateSensors。如果需要,这两个功能可以访问和更改bigProblem的值。在runMaintenance功能中,检查bigProblem是否为真,如果为真,则调用dropOutOfTheSky功能。

类型

显然,在真正卫星的代码中,除了从天而降之外,很可能会首先寻求解决方案。

让我们看看最后两个可见性修改器。

受保护

当使用protected可见性修改器时,其效果比publicprivate更加细致入微。当一个函数或属性被声明为protected时,它几乎是私有的——但不完全是。我们将在下一章中探讨的继承的另一个关键 OOP 主题允许我们编写类,然后编写继承该类功能的另一个类。protected修饰符将允许函数和属性对这些类可见,但对所有其他代码隐藏。

我们将在整本书中进一步探讨这一点。

内部

内部修饰语比其他修饰语更接近公众。它会将属性/函数暴露给同一包中的任何代码。如果你考虑到一些应用只有一个包,那么这是相当松散的可见性。我们不会用太多,为了完整起见,我只是想让你知道。

可见性修改器摘要

我们所涵盖的内容,尽管有几页长,但只是触及了可见性修改器的表面。关键是它们是存在的,它们的目的是帮助封装,并使您的代码不容易出现错误,并且更具可重用性。结合属性、函数、getter 和 setter,Kotlin 非常灵活,我们可以全天继续使用更多的例子来说明何时何地使用每个可见性修饰符,以及何时何地如何以不同的方式覆盖 getter 和 setter。使用这些技术来构建工作程序要有用得多。这是我们将在整本书中做的,我将经常提到为什么我们使用特定的可见性修饰符,或者为什么我们以特定的方式使用 getter/setter。我也鼓励你做本章末尾的基础课演示应用。

施工人员

在这一章中,我们一直在实例化对象(类的实例),并深入探讨了各种语法。有一小部分代码我们一直忽略到现在。下一段代码我们已经看过几次了,但我强调了其中的一小部分,以便我们可以进一步讨论:

val soldier = Soldier()

代码末尾初始化对象的括号看起来就像我们调用函数时上一章的代码(没有任何参数)。事实上,这正是正在发生的事情。当我们声明一个类时,Kotlin(在幕后)提供了一个名为构造器的特殊函数来准备实例。

到目前为止,在本章中,我们已经在一行中声明并初始化了所有实例。通常,我们需要在初始化中使用更多的逻辑,并且通常我们需要允许初始化类实例的代码传入一些值(就像函数一样)。这就是构造函数的原因。

通常,这个默认构造函数是我们所需要的,我们可以忘记它,但是有时我们需要做更多的工作来设置我们的实例,以便它可以使用。Kotlin 允许我们声明自己的构造函数,并给了我们三个主要选项:主构造函数、次构造函数和init块。

主构造函数

主构造函数是用类声明声明的。看看下面这段代码,它定义了一个构造函数,允许类的用户传入两个值。正如我们已经预料到的那样,该代码将放在名为Book.kt的文件中:

class Book(val title: String, var copiesSold: Int) {
   // Here we put our code as normal
   // But title and copiesSold are properties that
   // are already declared and initialized
}

在前面的代码中,我们已经声明了一个名为Book的类,并提供了一个接受两个参数的构造函数。它需要一个不可变的String值和一个在初始化时传递给它的可变的Int值。提供一个类似这样的构造函数,然后用它来实例化一个声明并初始化titlecopiesSold属性的实例。没有必要以通常的方式声明或初始化它们。

看看下面这段代码,它展示了如何实例化这个类的实例:

// Instantiate a Book using the primary constructor
val book = Book("Animal Farm", 20000000)

在前面的代码中,使用主构造函数实例化了一个名为book的对象,属性titlecopiesSold分别初始化为Animal Farm20000000(两千万)。

就像使用函数一样,您可以将构造函数塑造成具有任意组合、类型和数量的参数。

主构造函数的潜在缺点是属性从传入的参数中取值,没有任何灵活性。如果我们需要在将传入的值赋给属性之前对它们进行一些计算,会怎么样?幸运的是,我们有办法解决这个问题。

二级建造师

辅助构造函数是与类声明分开声明的构造函数,但仍在类体中。关于辅助构造函数需要注意的一点是,不能在参数中声明属性,还必须从辅助构造函数的代码中调用主构造函数。辅助构造函数的优势在于,您可以编写一些逻辑(代码)来初始化您的属性。请看下面的代码,它显示了这一点。我们还将同时引入一个新的关键词:

// Perhaps the user of the class 
// doesn't know the time as it
// is yet to be confirmed
class Meeting(val day: String, val person: String) {
    var time: String = "To be decided"
    // The user of the class can
    // supply the day, time and person 
    // of a meeting
    constructor(day: String, person: String, time: String)
            :this(day, person ){

        // "this" refers to the current instance
        this.time = time
        // time (the property) now equals time
        // that was passed in as a parameter
    }
}

在前面的代码中,我们声明了一个名为Meeting的类。主构造函数声明了两个属性,一个叫做day,一个叫做person。接下来,一个名为time的属性被声明并初始化为To be decided的值。

后面是二级建造师。请注意,参数前面有constructor关键字。您还会注意到,辅助构造函数包含三个参数,两个与主构造函数相同,还有一个名为time

请注意,time参数与之前声明和初始化的time属性不是同一个实体。辅助构造函数只包含“丢弃”参数,它们不会像主构造函数那样成为持久属性。这允许我们首先调用传入dayperson的主构造函数,然后(在辅助构造函数的主体中)将通过time参数传入的值赋给time属性。

类型

如果签名都不同,可以有多个辅助构造函数。将通过匹配调用/实例化代码的参数来调用适当的辅助构造函数。

我们需要谈谈这个

字面上,我的意思是,我们需要谈谈this这个关键词。当我们在一个类中使用this时,它具有引用当前实例的效果——因此它会对自己起作用。

因此this(day, person)代码调用初始化dayperson属性的主构造函数。此外,this.time = time代码具有将通过time参数传入的值分配给实际的time属性(this.time)的效果。

只是为了以防不明显。Meeting类需要额外的功能来使它变得有价值,比如setTimegetMeetingDetails,可能还有其他功能。

当类的用户不知道时间(通过主构造函数)或知道时间(通过次构造函数)时,我们的类可以创建Meeting类的实例。

使用会议类

我们将通过调用我们的任何一个构造函数来实例化我们的实例,如下面的代码所示:

// Book two meetings
// First when we don't yet know the time
val meeting = Meeting("Thursday", "Bob")

// And secondly when we do know the time
val anotherMeeting = Meeting("Wednesday","Dave","3 PM")

在前面的代码中,我们初始化了两个Meeting类的实例,一个叫做meeting,另一个叫做anotherMeeting。对于第一个实例化,我们调用主构造函数是因为我们不知道时间,对于第二个实例化,我们调用辅助构造函数是因为我们知道时间。

如果需要,我们可以有多个辅助构造函数,前提是它们都调用主构造函数。

初始化块

Kotlin 被设计成一种简洁的语言,通常有一种更简洁的方法来初始化我们的属性。如果类不依赖于多个不同的签名,那么我们可以坚持使用更简洁的主构造函数,并在init块中提供任何所需的初始化逻辑:

init{
  // This code runs when the class is instantiated
  // and can be used to initialize properties
}

这大概是足够的理论;让我们在一个工作应用中使用我们一直在谈论的一切。接下来,我们将编写一个使用类的小应用,包括一个主构造函数和一个init 块。

基本类应用和使用初始化块

您可以在代码下载中获取该应用的完整代码。它在Chapter10/Basic Classes文件夹中。但是继续阅读来创建自己的工作示例是最有用的。

我们将利用本章所学知识创建几个不同的课程,将理论付诸实践。我们还将看到我们的第一个例子,通过将一个类作为参数传递到另一个类的函数中,类如何相互作用。我们在理论上已经知道如何做到这一点,只是还没有在实践中看到。

我们还将看到通过使用init块首次实例化类时初始化数据的另一种方法。

我们将创建一个小应用,以模拟船只、码头和海战为理念。

本章和下一章中应用的输出将只是 logcat 窗口的文本。在第 12 章将我们的 Kotlin 连接到 UI 和 Nullability 中,我们将把前五章(关于安卓 UI)学到的所有东西和后面六章(关于 Kotlin)学到的所有东西结合起来,让我们的应用变得生动起来。

使用空活动模板创建项目。调用应用Basic Classes。现在我们将创建一个名为Destroyer的新类:

  1. 右键单击项目浏览器窗口中的com.gamecodeschool.basicclasses(或无论你的包名是什么)文件夹。
  2. 选择新建 | Kotlin 文件/类
  3. 名称:字段中,键入Destroyer
  4. 在下拉框中,选择
  5. 点击确定按钮,将新类添加到项目中。
  6. 重复前面的五个步骤,再创建两个类,一个叫做Carrier,另一个叫做ShipYard

新的类是为我们创建的,有一个类声明和用于代码的花括号。自动生成的代码还包括包声明,根据您创建项目时的选择,包声明会有所不同。这就是我的代码现在的样子。

内部Destroyer.kt:

package com.gamecodeschool.basicclasses

class Destroyer {
}

内部Carrier.kt:

package com.gamecodeschool.basicclasses

class Carrier {
}

内部ShipYard.kt:

package com.gamecodeschool.basicclasses

class ShipYard {
}

让我们从编码Destroyer类的第一部分开始。接下来是构造函数、一些属性和一个init块。将代码添加到项目中,研究它,然后我们将回顾我们所做的工作:

class Destroyer(name: String) {
    // What is the name of this ship
    var name: String = ""
        private set

    // What type of ship is it
    // Always a destroyer
    val type = "Destroyer"

    // How much the ship can take before sinking
    private var hullIntegrity = 200

    // How many shots left in the arsenal
    var ammo = 1
    // Cannot be directly set externally
        private set

    // No external access whatsoever
    private var shotPower = 60

    // Has the ship been sunk
    private var sunk = false

    // This code runs as the instance is being initialized
    init {
        // So we can use the name parameter
        this.name = "$type $name"
    }

首先要注意的是构造函数接收到一个名为nameString值。它没有申报valvar物业。因此,它不是一个属性,它只是一个常规参数,在实例初始化后将不复存在。我们将很快看到如何利用这一点。

在前面的代码中,我们声明了一些属性。请注意,除了初始化为DestroyerString val类型之外,大多数都是可变的var。还要注意的是,除了两个以外,大多数都是private接入。

type 属性是公共的,因此可以通过类的实例完全访问。该name物业也是公共的,但有一个private二传手。这将为获取值提供对实例的访问,但保护后备字段(值)不被实例更改。

hullIntegrityammoshotPowersunk属性都是private属性,通过实例不可访问,至少是直接不可访问。请务必记下分配给这些属性的值和类型。

前面代码的最后一部分是一个init块,其中name属性是通过将类型和名称属性与中间的空格连接起来进行初始化的。

接下来,添加如下takeDamage功能:

fun takeDamage(damageTaken: Int) {
   if (!sunk) {
        hullIntegrity -= damageTaken
        Log.i("$name damage taken =","$damageTaken")
        Log.i("$name hull integrity =","$hullIntegrity")

        if (hullIntegrity <= 0) {
               Log.d("Destroyer", "$name has been sunk")
               sunk = true
        }
  } else {
         // Already sunk
         Log.d("Error", "Ship does not exist")
  }
}

takeDamage函数中,if表达式检查sunk布尔不是真的。如果船还没有沉没,那么通过减去作为参数传入的damageTaken的值来减少hullIntegrity。因此,即使是private,该实例也会间接影响hullIntegrity。重点是它只能以一种由类的程序员决定的方式这样做;在这种情况下——我们。正如我们将会看到的,所有私有财产最终都会以某种方式被操纵。

此外,如果船还没有沉没,两个Log.i调用会将受损情况和剩余的船体完整性信息输出到日志窗口。最后,在未沉没场景(!sunk)中,嵌套的if表达式检查hullIntegrity是否小于零。如果是,则打印一条消息,指示船只已经沉没,并且sunk布尔设置为真。

damageTaken函数被调用并且sunk变量为真时,else 块将执行,并且将打印一条消息,该船不存在,因为它已经被击沉。

接下来,添加shootShell功能,该功能将与takeDamage功能协同工作。或者更准确地说,一个船舶实例的takeDamage功能将与其他船舶实例的shootShell功能协同工作,我们很快就会看到:

fun shootShell():Int {
  // Let the calling code no how much damage to do
  return if (ammo > 0) {
         ammo--
         shotPower
  }else{
        0
  }
}

shootShell功能中,如果船上有弹药,ammo属性减 1,shotPower值返回调用代码。如果船上没有弹药了(ammo不大于零),那么0的值返回到调用代码。

最后,对于Destroyer级增加serviceShip功能,将ammo设置为10,将hullIntegrity设置为100,使船只做好再次受到伤害(通过takeDamage)和造成伤害(通过shootShell)的充分准备:

fun serviceShip() {
    ammo = 10
    hullIntegrity = 100
}

接下来,我们可以快速对Carrier类进行编码,因为它是如此的相似。只需记下分配给typehullIntegrity的值的细微差别。还要注意,我们用attacksRemainingattackPower代替ammoshotPower。此外,shootShell已经被launchAerialAttack取代,这似乎更适合航母。将以下代码添加到Carrier类中:

class Carrier (name: String){
    // What is the name of this ship
    var name: String = ""
        private set

    // What type of ship is it
    // Always a destroyer
    val type = "Carrier"

    // How much the ship can take before sinking
    private var hullIntegrity = 100

    // How many shots left in the arsenal
    var attacksRemaining = 1
    // Cannot be directly set externally
        private set

    private var attackPower = 120

    // Has the ship been sunk
    private var sunk = false

    // This code runs as the instance is being initialized
    init {
        // So we can use the name parameter
        this.name = "$type $name"
    }

    fun takeDamage(damageTaken: Int) {
        if (!sunk) {
            hullIntegrity -= damageTaken
            Log.d("$name damage taken =","$damageTaken")
            Log.d("$name hull integrity =","$hullIntegrity")

            if (hullIntegrity <= 0) {
                Log.d("Carrier", "$name has been sunk")
                sunk = true
            }
        } else {
            // Already sunk
            Log.d("Error", "Ship does not exist")
        }
    }

    fun launchAerialAttack() :Int {
        // Let the calling code no how much damage to do
        return if (attacksRemaining > 0) {
            attacksRemaining--
            attackPower
        }else{
            0
        }
    }

    fun serviceShip() {
        attacksRemaining = 20
        hullIntegrity = 200
    }
}

在我们开始使用新类之前,最后的代码是ShipYard类。它有两个简单的功能:

class ShipYard {

    fun serviceDestroyer(destroyer: Destroyer){
        destroyer.serviceShip()
    }

    fun serviceCarrier(carrier: Carrier){
        carrier.serviceShip()
    }
}

第一个函数serviceDestroyer将一个Destroyer实例作为参数,在该函数中只调用该实例的serviceShip函数。第二个函数serviceCarrier具有相同的效果,但是以一个Carrier实例作为参数。虽然这两个函数简短而简单,但它们后来的用法将很快揭示一些与类及其实例相关的有趣的细微差别。

现在我们将创建一些实例,并通过模拟一场虚构的海战来使我们的类工作。将此代码添加到MainActivity类的onCreate函数中:

val friendlyDestroyer = Destroyer("Invincible")
val friendlyCarrier = Carrier("Indomitable")

val enemyDestroyer = Destroyer("Grey Death")
val enemyCarrier = Carrier("Big Grey Death")

val friendlyShipyard = ShipYard()

// Uh oh!
friendlyDestroyer.takeDamage(enemyDestroyer.shootShell())
friendlyDestroyer.takeDamage(enemyCarrier.launchAerialAttack())

// Fight back
enemyCarrier.takeDamage(friendlyCarrier.launchAerialAttack())
enemyCarrier.takeDamage(friendlyDestroyer.shootShell())

// Take stock of the supplies situation
Log.d("${friendlyDestroyer.name} ammo = ",
         "${friendlyDestroyer.ammo}")

Log.d("${friendlyCarrier.name} attacks = ",
         "${friendlyCarrier.attacksRemaining}")

// Dock at the shipyard
friendlyShipyard.serviceCarrier(friendlyCarrier)
friendlyShipyard.serviceDestroyer(friendlyDestroyer)

// Take stock of the supplies situation again
Log.d("${friendlyDestroyer.name} ammo = ",
         "${friendlyDestroyer.ammo}")

Log.d("${friendlyCarrier.name} attacks = ",
         "${friendlyCarrier.attacksRemaining}")

// Finish off the enemy
enemyDestroyer.takeDamage(friendlyDestroyer.shootShell())
enemyDestroyer.takeDamage(friendlyCarrier.launchAerialAttack())
enemyDestroyer.takeDamage(friendlyDestroyer.shootShell())

让我们回顾一下那个代码。代码从实例化两艘友军船(friendlyDestroyerfriendlyCarrier)和两艘敌舰(enemyDestroyerenemyCarrier)开始。此外,一个名为friendlyShipyardShipyard实例也被实例化,为接下来不可避免的大屠杀做准备:

val friendlyDestroyer = Destroyer("Invincible")
val friendlyCarrier = Carrier("Indomitable")

val enemyDestroyer = Destroyer("Grey Death")
val enemyCarrier = Carrier("Big Grey Death")

val friendlyShipyard = ShipYard()

接下来friendlyDestroyer物体受到两次伤害。一次来自enemyDestroyer,一次来自enemyCarrier。这是通过friendlyDestroyertakeDamage功能分别传入两个敌人的shootShelllaunchAerialAttack功能的返回值来实现的:

// Uh oh!
friendlyDestroyer.takeDamage(enemyDestroyer.shootShell())
friendlyDestroyer.takeDamage(enemyCarrier.launchAerialAttack())

接下来,友军通过对enemyCarrier物体进行两次攻击进行反击,一次是从friendlyCarrier物体经由launchAerialAttack,另一次是从friendlyDestroyer物体经由shootShell:

// Fight back
enemyCarrier.takeDamage(friendlyCarrier.launchAerialAttack())
enemyCarrier.takeDamage(friendlyDestroyer.shootShell())

然后,两艘友好船只的状态被输出到日志窗口:

// Take stock of the supplies situation
Log.d("${friendlyDestroyer.name} ammo = ",
         "${friendlyDestroyer.ammo}")

Log.d("${friendlyCarrier.name} attacks = ",
         "${friendlyCarrier.attacksRemaining}")

现在依次在每个适当的实例上调用Shipyard实例的适当的函数。没有enemyShipyard对象,所以他们无法修复和重新武装:

// Dock at the shipyard
friendlyShipyard.serviceCarrier(friendlyCarrier)
friendlyShipyard.serviceDestroyer(friendlyDestroyer)

接下来,统计数据会再次打印出来,这样我们在参观造船厂后就可以看到不同之处:

// Take stock of the supplies situation again
Log.d("${friendlyDestroyer.name} ammo = ",
         "${friendlyDestroyer.ammo}")

Log.d("${friendlyCarrier.name} attacks = ",
         "${friendlyCarrier.attacksRemaining}")

然后,也许不可避免地,友军消灭了敌人:

// Finish off the enemy
enemyDestroyer.takeDamage(friendlyDestroyer.shootShell())
enemyDestroyer.takeDamage(friendlyCarrier.launchAerialAttack())
enemyDestroyer.takeDamage(friendlyDestroyer.shootShell())

运行应用,然后我们可以在 logcat 窗口中检查以下输出:

Destroyer Invincible damage taken =: 60
Destroyer Invincible hull integrity =: 140
Destroyer Invincible damage taken =: 120
Destroyer Invincible hull integrity =: 20
Carrier Big Grey Death damage taken =: 120
Carrier Big Grey Death hull integrity =: -20
Carrier: Carrier Big Grey Death has been sunk
Error: Ship does not exist
Destroyer Invincible ammo =: 0
Carrier Indomitable attacks =: 0
Destroyer Invincible ammo =: 10
Carrier Indomitable attacks =: 20
Destroyer Grey Death damage taken =: 60
Destroyer Grey Death hull integrity =: 140
Destroyer Grey Death damage taken =: 120
Destroyer Grey Death hull integrity =: 20
Destroyer Grey Death damage taken =: 60
Destroyer Grey Death hull integrity =: -40
Destroyer: Destroyer Grey Death has been sunk

这里又是输出,这次分解成几个部分,这样我们就可以清楚地看到哪个代码产生了哪几行输出。

友军驱逐舰受到攻击,船体接近破裂点:

Destroyer Invincible damage taken =: 60
Destroyer Invincible hull integrity =: 140
Destroyer Invincible damage taken =: 120
Destroyer Invincible hull integrity =: 20

敌人的航母被攻击和击沉:

Carrier Big Grey Death damage taken =: 120
Carrier Big Grey Death hull integrity =: -20
Carrier: Carrier Big Grey Death has been sunk

敌方航母再次受到攻击,但因为被击沉,takeDamage功能中的else区块被执行:

Error: Ship does not exist

当前的弹药/可用攻击统计被打印出来,对友军来说情况不妙:

Destroyer Invincible ammo =: 0
Carrier Indomitable attacks =: 0

快速参观造船厂,情况会好得多:

Destroyer Invincible ammo =: 10
Carrier Indomitable attacks =: 20

友军全副武装,修复完毕,干掉了剩下的驱逐舰:

Destroyer Grey Death damage taken =: 60
Destroyer Grey Death hull integrity =: 140
Destroyer Grey Death damage taken =: 120
Destroyer Grey Death hull integrity =: 20
Destroyer Grey Death damage taken =: 60
Destroyer Grey Death hull integrity =: -40
Destroyer: Destroyer Grey Death has been sunk

如果有任何不匹配的地方,一定要再次检查代码和输出。

参考文献介绍

此时你的脑海中可能会有一个挥之不去的想法。再来看看Shipyard类的两个函数:

fun serviceDestroyer(destroyer: Destroyer){
        destroyer.serviceShip()
}

fun serviceCarrier(carrier: Carrier){
        carrier.serviceShip()
}

当我们调用这些函数并将friendlyDestroyerfriendlyCarrier传递给它们相应的service…函数时,我们从前后输出中看到实例内的值发生了变化。通常,如果我们想保留函数的结果,我们需要使用返回值。正在发生的事情是,与以常规类型作为参数的函数不同,当我们传递一个类的实例时,我们实际上是在传递一个对实例本身的引用——不仅仅是其中值的副本,而是实际的实例。

此外,所有不同的船舶相关实例都是用val声明的,那么我们到底是如何改变任何属性的呢?这个难题的简单答案是,我们没有改变引用本身,只是改变了其中的属性,但显然有必要进行更全面的讨论。

我们将开始探索参考资料,然后深入挖掘其他相关主题,例如第 12 章将我们的 Kotlin 连接到 UI 和可空性中的安卓设备内部的内存。现在,只要知道当我们将数据传递给一个函数时,如果它是一个类类型,我们传递的引用就相当于(尽管不是实际上的)真正的实际实例本身就足够了。

总结

我们终于写完了第一课。我们已经看到,我们可以在与类同名的文件中实现一个类。在我们实例化类的对象/实例之前,类本身什么也不做。一旦我们有了类的实例,我们就可以使用它的特殊变量,称为属性,以及它的非私有函数。正如我们在 Basic Classes 应用中证明的那样,类的每个实例都有自己独特的属性,就像当你买一辆工厂制造的汽车时,你会得到自己的方向盘、卫星导航和加速条纹一样。我们还遇到了引用的概念,这意味着当我们将类的实例传递给函数时,接收函数可以访问实际的实例。

所有这些信息将引发更多问题。OOP 就是这样。因此,让我们通过在下一章中更仔细地研究继承来尝试巩固所有这些类的东西。