Object
是使面向对象编程如此强大的原因。对于一个对象,我们可以模拟真实世界的对象,比如一罐震动饮料,或者虚拟对象,比如视频游戏中的角色。然后,这些对象可以在我们的应用程序中进行交互,以模拟现实世界的行为或我们在虚拟世界中想要的行为。
在计算机应用程序中,如果没有蓝图告诉应用程序从对象中期望什么属性和操作,我们就无法创建对象。在大多数面向对象的语言中,此蓝图以类的形式出现。类是一个构造,它允许我们将对象的属性和操作封装到单个类型中,该类型对我们试图在代码中表示的对象建模。
我们在类中使用初始化器来创建类的实例。我们通常使用这些初始化器来设置对象属性的初始值,或者执行类所需的任何其他初始化。一旦我们创建了一个类的实例,我们就可以在我们的代码中使用它。
所有关于面向对象编程的解释都很好,但是没有什么比实际代码更好地演示了这些概念。让我们看看如何使用Swift中的类来模拟颠簸罐和冰箱来保持颠簸冷。我们将从以下 Jolt 罐建模开始:
class Jolt {
var volume: Double
var caffeine: Double
var temperature: Double
var canSize: Double
var description: String
init(volume: Double, caffeine: Double, temperature: Double) {
self.volume = volume
self.caffeine = caffeine
self.temperature = temperature
self.description = "Jolt energy drink"
self.canSize = 24
}
func drinking(amount: Double) {
volume -= amount
}
func temperatureChange(change: Double) {
temperature += change
}
}
在这个 Jolt 类中,我们定义了五个属性。这些特性包括体积 (罐子中颠簸的量) 、咖啡因 (罐子中有多少咖啡因) 、温度 (罐子的当前温度) 、描述 (产品描述) 和 cansize (罐头本身的大小)。然后,我们定义一个初始化程序,当我们创建类的实例时,该初始化程序将用于启动对象的属性。此初始化程序将确保在创建实例时正确初始化所有属性。最后,我们为 can 定义了两个动作。这两个动作是 drinking
(当有人从罐子里喝水时称为) 和temperatureChange
(当罐子的温度变化时称为)。
现在,让我们看看如何模拟一个冰箱,我们可以用它来保持我们的颠簸罐冷,因为没有人喜欢温暖的颠簸罐:
class Cooler {
var temperature: Double
var cansOfJolt = [Jolt]()
var maxCans: Int
init(temperature: Double, maxCans: Int) {
self.temperature = temperature
self.maxCans = maxCans
}
func addJolt(jolt: Jolt) -> Bool {
if cansOfJolt.count < maxCans {
cansOfJolt.append(jolt) return true
} else {
return false
}
}
func removeJolt() -> Jolt? {
if cansOfJolt.count > 0 {
return cansOfJolt.removeFirst()
} else {
return nil
}
}
}
我们模拟冰箱的方式与我们模拟颠簸罐的方式相似。
我们从定义冰箱的三个特性开始。这三个特性是 temperature
(冰箱中的当前温度) 、cansOfJolt
(冰箱中的震动罐) 和 maxcans
(冰箱可以容纳的最大罐数)。然后,当我们创建冰箱类的实例时,使用初始化程序来启动属性。最后,我们定义了 Cooler
的两个 action
。它们是 addJolt
(用于向冰箱中添加一罐震动) 或 removeJolt
(用于从冰箱中移除一罐震动)。现在我们有了Jolt和cool类,让我们看看如何将这两个类一起使用:
var cooler = Cooler(temperature: 38.0, maxCans: 12)
for _ in 0...5 {
let can = Jolt(volume: 23.5, caffeine: 280,temperature: 45)
let _ = cooler.addJolt(can)
}
let jolt = cooler.removeJolt()
jolt?.drinking(5)
print("Jolt Left in can: (jolt?.volume)")
在此示例中,我们使用初始化程序创建了一个 Cooler
类的实例来设置默认属性。然后,我们创建了Jolt
类的六个实例,并使用 for-in
循环将它们添加到冰箱中。最后,我们从冰箱中取出一罐 Jolt,喝了一些。Jolt 和咖啡因Jolt 的提神饮料。还有什么更好的呢?
这种设计似乎很适合我们简单的例子; 然而,它真的没有那么灵活。虽然我真的很喜欢咖啡因,但我妻子不喜欢; 她更喜欢不含咖啡因的健怡可乐。采用我们目前的冰箱设计,当她去冰箱中添加一些无咖啡因的健怡可乐时,我们必须告诉她这是不可能的,因为我们的冰箱只接受 Jolt
的例子。这不会很好,因为这不是现实世界中冰箱的工作方式,也因为我不想告诉我妻子她不能喝健怡可乐 (相信我,没人想告诉她她不能喝健怡可乐)。那么,我们如何使这个设计更加灵活呢?
这个问题的答案是多态性。多态性(Polymorphism) 来自希腊语 Greek words Poly (for many) and Morph (forms)。在计算机科学中,当我们想要使用单个接口来表示代码中的多种类型时,我们使用多态性。多态性使我们能够以统一的方式与多种类型进行交互。当我们通过统一接口与多个对象进行交互时,我们能够随时添加符合该接口的其他对象类型。然后,我们可以在代码中使用这些附加类型,而无需进行任何更改。
使用面向对象的编程语言,我们可以通过子类化实现多态性和代码重用。子类化(subclassing)是指一个类从另一个超类派生。例如,如果我们有一个模拟典型人员的 Person
类,那么我们可以对 Person
类进行子类化以创建 Student
类。然后,Student
类将继承 Person
类的所有属性和方法,可以覆盖其继承的任何属性和方法和/或添加其自己的附加属性和方法。并且,我们可以添加也从 Person
超类派生的其他类,并且我们可以使用 Person
类提供的接口与所有这些子类进行交互。
当一个类从另一个类派生时,原始类,即我们从中派生新类的类,被称为super或父类,新类被称为child或子类。在我们的 person-student 示例中,Person
是super或父类,Student 是 child或子类。在本书中,我们将使用术语超类和子类。
多态性是通过子类化实现的,因为我们可以通过超类呈现的接口与所有子类的实例进行交互。例如,如果我们有三个子类 (Student
, Programmer
, and Fireman
),它们都是 Person
类的子类,可以通过 Person
类提供的接口与所有三个子类进行交互。如果 Person
类有一个名为 run()
的方法,那么我们可以确保Person
类的所有子类都有一个名为run()
的方法 (要么是 Person
类的方法,要么是来自覆盖 Person
类的方法的子类中的方法)。因此,我们可以使用 run()
方法与所有子类进行交互。
让我们看看多态性如何帮助我们在冰箱中添加除 Jolt 以外的饮料。
在我们的原始示例中,我们能够在 Jolt
类中对罐装大小进行硬编码,因为 Jolt
能量饮料仅以24盎司罐装出售 (苏打水有不同的大小,但是能量饮料只在24盎司的罐子里出售)。以下枚举定义了我们的冰箱将接受的 can 尺寸:
enum DrinkSize {
case Can12
case Can16
case Can24
case Can32
}
DrinkSize
枚举让我们在冰箱中使用12、16、24和32盎司的饮料。
现在,让我们看看所有的饮料类型都将源自一个基类或超类,我们定义超类为 Drink
:
class Drink {
var volume: Double
var caffeine: Double
var temperature: Double
var drinkSize: DrinkSize
var description: String
init(volume: Double, caffeine: Double, temperature: Double, drinkSize: DrinkSize) {
self.volume = volume
self.caffeine = caffeine
self.temperature = temperature
self.drinkSize = drinkSize
self.description = "Drink base class"
}
func drinking(amount: Double) {
volume -= amount
}
func temperatureChange(change: Double) {
temperature -= change
}
}
This Drink class is very similar to our original Jolt class. We defined the same five properties that we had in our original Jolt class; however, drinkSize is now defined to be of the DrinkSize type rather than Double. We defined a single initializer for our Drink class that initializes all the five properties of the class. Finally, we have the same two methods that were in the original Jolt class, which are drinking() and temperatureChange(). One thing to take note of is, in the Drink class, our description is set to Drink base class.
这个 Drink
类与我们最初的 Jolt
类非常相似。我们定义了与原始 Jolt
类相同的五个属性; 然而,drinkSize
现在被定义为 DrinkSize
类型,而不是 Double
类型。我们为我们的饮料类定义了一个初始化程序,它初始化了类的所有五个属性。最后,我们有与原始Jolt类相同的两种方法,即drinking()
和temperatureChange()
。需要注意的是,在 Drink
类中,我们的描述了饮料基类的集合。
现在,让我们创建 Jolt
类,它将是Drink
类的子类。此类将从 Drink
类继承所有属性和方法:
class Jolt: Drink {
init(temperature: Double) {
super.init(volume: 23.5, caffeine: 280, temperature: temperature, drinkSize:DrinkSize.Can24)
self.description = "Jolt energy drink"
}
}
正如我们在 Jolt
类中看到的,不需要重新定义饮料超类的属性和方法。我们将为 Jolt
类添加一个初始化程序。这个初始化器只要求提供易拉罐的温度。所有其他值是为一罐 Jolt 的默认值。
现在,让我们看看如何创建 Cooler
类,它将接受除 Jolt 之外的其他饮料类型:
class Cooler {
var temperature: Double
var cansOfDrinks = [Drink]()
var maxCans: Int
init(temperature: Double, maxCans: Int) {
self.temperature = temperature
self.maxCans = maxCans
}
func addDrink(drink: Drink) -> Bool {
if cansOfDrinks.count < maxCans {
cansOfDrinks.append(drink)
return true
} else {
return false
}
}
func removeDrink() -> Drink? {
if cansOfDrinks.count > 0 {
return cansOfDrinks.removeFirst()
} else {
return nil
}
}
}
这个新的 Cooler
类与原来的 Cooler
类几乎完全一样,除了我们将所有对 Jolt
类的引用替换为对 Drink
类的引用。由于 Jolt
类是 Drink
类的子类,因此我们可以在需要 Drink
类实例的任何地方使用它。让我们看看这是如何工作的。以下代码将创建 Cooler
类的实例。向冰箱中加入六罐 Jolt,从冰箱中取出一罐,然后喝一杯:
let cooler = Cooler(temperature: 4, maxCans: 27)
for _ in 0...5 {
let can = Jolt(temperature: 26)
let _ = cooler.addDrink(drink: can)
}
let jolt = cooler.removeDrink()
jolt?.drinking(5)
print("Jolt left in can: \(jolt?.volume)")
在此示例中,我们使用了 Jolt
类的实例,而 Drink
类的实例是必须的。这是多态性的作用。现在我们有一个装有 Jolt 的冰箱,我们准备去旅行了。我妻子当然想带她不含咖啡因的健怡可乐,问是否可以放一些在冰箱里保持凉爽。众所周知,我们不想剥夺她的健怡可乐,所以很快创造了一个 CaffeineFreeDietCoke
类,可以和冰箱一起使用。代码如下:
class CaffeineFreeDietCoke: Drink {
init(volume: Double, temperature: Double, drinkSize: DrinkSize) {
super.init(volume: volume, caffeine: 0, temperature: temperature, drinkSize: drinkSize)
self.description = "Caffeine Free Diet Coke"
}
}
CaffeineFreeDietCoke
类与 Jolt
类非常相似。它们都是 Drink
类的子类,并且各自定义一个初始化类的初始化程序。关键是它们都是 Drink
类的子类,这意味着我们可以在冰箱中使用这两个类的实例。因此,当我妻子带了六瓶无咖啡因的健怡可乐时,我们可以把它们放在冰箱里,就像 Jolt 一样。以下代码演示了这一点:
let cooler = Cooler(temperature: 4, maxCans: 27)
for _ in 0...5 {
let can = Jolt(temperature: 26)
let _ = cooler.addDrink(drink: can)
}
for _ in 0...5 {
let can = CaffeineFreeDietCoke(volume: 15.5, temperature: 45, drinkSize: DrinkSize.Can16)
}
在此示例中,我们创建了一个冰箱实例; 我们添加了六罐 Jolt 和六罐无咖啡因健怡可乐。使用多态性,如这里所示,允许我们创建我们需要的 Drink
类的尽可能多的子类,它们都可以与 Cooler
器类一起使用,而无需更改 Cooler
类的代码。这使得我们的代码非常灵活。
那么,当我们从冰箱里拿一个罐子时会发生什么呢?显然,如果我妻子抓住一罐 Jolt,她会想把它放回去,换一个不同的罐子。但是她怎么知道她能抓住哪个呢?
var foundCan = false
var wifeDrink: Drink?
while(!foundCan) {
if let can = cooler.removeDrink() {
if can is CaffeineFreeDietCoke {
foundCan = true
wifeDrink = can
} else {
let _ = cooler.addDrink(drink: can)
}
}
}
if let drink = wifeDrink {
print("Got: " + drink.description)
}
在代码中,我们有一个 while
循环,该循环持续循环直到 foundCan
布尔值设置为 true
。在 while
循环中,我们从冰箱中取出一种饮料,然后使用类型检查运算符 (is
) 来查看我们移除的罐是否是 CaffeineFreeDietCoke
类的实例。如果是,那么我们将把 foundCan
布尔值设置为true,并把 wifeDrink
变量设置为我们刚刚从冰箱中移除的饮料的实例。如果不是,那么我们会把罐子放回冰箱里,然后再拿一罐。
在前面的示例中,我们展示了如何将 Swift
用作面向对象的编程语言,多态性使我们的代码非常灵活和易于扩展; 但这种设计有几个缺点。在我们继续进行面向协议的编程之前,让我们看一下其中的两个缺点。然后,我们将看到面向协议的编程使这种设计更好。
第一个缺点是饮料的构造器 (Jolt
、CaffeineFreeDietCoke
和 DietCoke
) 。当我们初始化子类时,需要调用超类的构造器。这是一把双刃剑。虽然调用超类的构造器给了我们一致的初始化,但如果不小心,它也会带来不正确的初始化。例如,我们使用以下代码创建了另一个名为 DietCoke 的 Drink
类
class DietCoke: Drink {
init(volume: Double, temperature: Double, drinkSize: DrinkSize) {
super.init(volume: volume, caffeine: 45, temperature: temperature, drinkSize: drinkSize)
}
}
如果我们仔细观察,我们会发现在 DietCoke
类的构造器中,未设置 description
属性。因此,这个类的描述会是 Drink
基类,这不是我们想要的。
当我们创建像这样的子类时,我们需要小心,以确保所有的属性都被正确地设置,并且不能假设超类的构造器将正确地设置所有的属性。
第二个设计缺点是我们使用引用类型。虽然那些熟悉面向对象编程的人可能不认为这是一个缺点,引用类型在很多情况下是首选,但在我们的设计中,将 drink
类型定义为价值类型更有意义。如果你不熟悉引用类型和值类型是如何工作的,我们将在第2章,我们的类型选择中深入研究它们。
当我们传递引用类型的实例时 (即,我们传递数组到函数或集合),我们传递的是对原始实例的引用。当我们传递值类型的实例时,我们传递原始实例的新副本。如果我们不仔细检查以下代码,让我们看看使用引用类型可能导致的问题:
var jolts = [Drink]()
var myJolt = Jolt(temperature: 27)
for _ in 0...5 {
jolts.append(myJolt)
}
jolts[0].drinking(10)
for (index, can) in jolts.enumerated() {
print("Can \(index) amount left: \(can.volume)")
}
示例中,我们创建了一个数组,可以包含 Drink
类的实例或 Drink
子类的实例。然后,我们创建了 Jolt
类的实例,并使用它用六罐Jolt填充我们的数组。接下来,我们喝了数组中的第一个易拉罐,并打印出数组中每个罐子的剩余体积。如果我们运行此代码,我们将看到以下结果:
Can 0 amount Left: 13.5
Can 1 amount Left: 13.5
Can 2 amount Left: 13.5
Can 3 amount Left: 13.5
Can 4 amount Left: 13.5
Can 5 amount Left: 13.5
从结果可以看到,数组中所有罐子都有相同数量的 Jolt 。这是因为我们创建了 Jolt
类的单个实例,然后在jolts
数组中添加了对该单一实例的六个引用。因此,当我们喝了数组中的第一个易拉罐,实际上对数组中所有易拉罐都喝了一口。
对于一个有经验的面向对象的程序员来说,犯这样的错误似乎是不可能的; 然而,初级开发人员或不熟悉面向对象编程的开发人员经常发生这种情况。对于复杂构造器的类经常发生此错误。我们可以通过采用构建器模式来避免此问题,或者通过在我们的自定义类中实现复制方法来复制实例。
关于面向对象的编程和子类化,需要注意的另一件事是,如前面的示例所示,一个类只能有一个超类。例如,我们Jolt
类的超类是 Drink
类。这可能会导致单个超类非常臃肿,并非所有子类都需要的代码。这是开发中非常常见的问题。
下一节,让我们看看如何使用面向协议的编程来实现饮料和冰箱的示例。