Swift有另一种方法来构建称为类的复杂数据类型。它们看起来类似于结构体,但有许多重要的区别,包括:
- 您的类没有自动的成员初始化器;您需要编写自己的初始化器。
- 您可以将一个类定义为基于另一个类,添加任何您想要的新内容。
- 当你创建一个类的实例时,它被称为一个对象。如果复制该对象,默认情况下,两个副本都指向同一个数据——更改一个,副本也会更改。
这三个都是巨大的差异,所以在继续之前我将更深入地讨论它们。
初始化对象
如果我们要将Person
结构体转换为Person
类,Swift不会让我们这样写:
class Person {
var clothes: String
var shoes: String
}
这是因为我们将这两个属性声明为String
,如果您还记得这意味着它们必须有一个值。这在结构体中很好,因为Swift会自动为我们生成一个成员初始化器,强制我们为这两个属性提供值,但这不会在类中发生,因此Swift无法确定它们是否会被给定值。
有三种解决方案:
- 1、使这两个值成为可选字符串;
- 2、为它们提供默认值;
- 3、编写自己的初始化器。
第一个选项很笨拙,因为它在我们的代码中引入了不需要的选项。第二个选项可以工作,但如果不使用这些默认值,则会有点浪费。这就剩下了第三个选项,实际上它是正确的:编写我们自己的初始化器。
为此,在类中创建一个名为init()
的方法,该方法接受我们关心的两个参数:
class Person {
var clothes: String
var shoes: String
init(clothes: String, shoes: String) {
self.clothes = clothes
self.shoes = shoes
}
}
在这份代码中有两件事可能会对您产生影响。
首先,不要在init()
方法之前编写func
,因为它是特殊的。其次,因为传入的参数名与要分配的属性名相同,所以使用self.
为了让你的意思更清楚——“这个对象的clothes
属性应该设置为传入的clothes
参数。”如果你愿意的话,你可以给他们取唯一的名字——这取决于你自己。
重要提示:Swift要求所有非可选属性在初始化方法结束时或在初始化方法内调用任何其他方法时(以先到者为准)都有一个值。
类继承
类和结构体之间的第二个区别是类可以在其他类之上构建更大的东西,即类继承。即使是在最基本的程序中,这也是Cocoa Touch中广泛使用的一种技术,因此您应该掌握它。
让我们从一些简单的事情开始:一个有属性的Singer
类,这是他们的名字和年龄。至于方法,将有一个简单的初始化方法来处理属性的设置,另外还有一个sing()
方法,它输出一些单词:
class Singer {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func sing() {
print("La la la la")
}
}
我们现在可以通过调用该初始化器来创建该对象的实例,然后读取其属性并调用其方法:
代码语言:javascript复制var taylor = Singer(name: "Taylor", age: 25)
taylor.name
taylor.age
taylor.sing()
这是我们的基本类,但我们将在此基础上继续:我想定义一个CountrySinger
类,它拥有Singer
类所做的一切,但是当我在它上调用sing()
时,我想打印“Trucks, guitars, and liquor”。
当然,你可以把原Singer
复制粘贴到一个叫做CountrySinger
的新类中,但这是一种懒散的编程方式,如果你以后对Singer
进行更改,却忘了把它们复制过来,它会回来困扰你。相反,Swift有一个更聪明的解决方案:我们可以将CountrySinger
定义为基于Singer
,它将获得其所有属性和方法,以便我们建立:
class CountrySinger: Singer {
}
这个冒号是什么魔力:它意味着“CountrySinger
扩展了Singer
”,现在,这个新的CountrySinger
类(称为子类)还没有添加任何内容到Singer
(称为父类或超类)。我们希望它有自己的sing()
方法,但在Swift中需要学习一个新的关键字:override
。这意味着“我知道这个方法是由我的父类实现的,但是我想为这个子类更改它。”
使用override
关键字很有帮助,因为它使您的意图变得清晰。它还允许Swift检查你的代码:如果你不使用override
Swift不允许你更改从你的超类得到的方法,或者如果你使用override
而没有任何东西可以重写,Swift会指出你的错误。
所以,我们需要使用override func
,如下所示:
class CountrySinger: Singer {
override func sing() {
print("Trucks, guitars, and liquor")
}
}
现在修改taylor
对象的创建方式:
var taylor = CountrySinger(name: "Taylor", age: 25)
taylor.sing()
如果您将CountrySinger
替换Singer
,则应该能够在结果窗口中看到不同的消息。
现在,为了使事情更完整,我们将定义一个新的类,称为HeavyMetalSinger
。但这一次我们将要存储一个新的属性,称为noiseLevel
定义这个特定的重金属歌手对麦克风尖叫的声音的噪音登记。
这就产生了一个问题,这是一个需要以非常特殊的方式解决的问题:
- Swift希望所有非可选属性都具有值。
- 我们的
Singer
类没有噪音等级。 - 因此,我们需要为
HeavyMetalSinger
创建一个能接受噪声级的自定义初始化器。 - 这个新的初始化器还需要知道重金属歌手的
name
和age
,这样它就可以把它传递给它的超类Singer
。 - 向超类传递数据是通过方法调用完成的,并且在给定所有属性值之前,不能在初始化器中进行方法调用。
- 因此,我们需要先设置自己的属性
(noiseLevel)
,然后传递其他参数供超类使用。
这听起来可能非常复杂,但在代码中很简单。下面是HeavyMetalSinger
类,它有自己的sing()
方法:
class HeavyMetalSinger: Singer {
var noiseLevel: Int
init(name: String, age: Int, noiseLevel: Int) {
self.noiseLevel = noiseLevel
super.init(name: name, age: age)
}
override func sing() {
print("Grrrrr rargh rargh rarrrrgh!")
}
}
注意它的初始值设定项是如何接受三个参数,然后调用super.init()
将name
和age
传递给Singer
超类的——但只有在设置了它自己的属性之后。在处理对象时,你会看到super
经常被使用,它的意思是“在我继承的类上调用一个方法”。它通常被用来表示“让我的父类先做它需要做的所有事情,然后再做额外的事情。”
类继承是一个大主题,所以如果还不清楚,不要担心。然而,还有一件事你需要知道:类继承通常跨越许多层。例如,A可以从B继承,B可以从C继承,C可以从D继承,等等。这使您可以构建功能并在多个类上重用,有助于保持代码的模块化和易于理解。
和Objective-C混合使用
如果你想让苹果操作系统的某个部分调用Swift类的方法,你需要用一个特殊的属性来标记它:@objc
。这是“Objective-C”的缩写,该属性有效地将该方法标记为可用于运行旧的Objective-C代码——几乎所有的iOS、macOS、watchOS和tvOS。例如,如果您要求系统在一秒钟后调用您的方法,则需要用@objc
标记它。
现在不要太担心@objc
,我不仅会在后面的上下文中解释它,Xcode还会在需要时告诉您。或者,如果您不想对单个方法使用@objc
,您可以将@objcMembers
放在类之前,以自动将其所有方法提供给Objective-C。
值与引用
当您复制一个结构体时,整个东西都是重复的,包括它的所有值。这意味着更改结构体的一个副本不会更改其他副本——它们都是单独的。对于类,对象的每个副本都指向同一个原始对象,因此如果更改一个,它们都会更改。Swift调用结构体“值类型”,因为它们只指向一个值,而类“引用类型”,因为对象只是对实际值的共享引用。
这是一个重要的区别,这意味着结构体和类之间的选择是一个重要的区别:
- 如果您希望有一个共享状态被传递和修改,那么您需要的是类。您可以将它们传递到函数中,或者将它们存储在数组中,在其中进行修改,并将这些更改反映到程序的其余部分中。
- 如果要避免一个副本不能影响所有其他副本的共享状态,则需要使用结构体。您可以将它们传递到函数中,或者将它们存储在数组中,在其中进行修改,并且它们不会在引用它们的其他位置发生更改。
如果我总结一下结构体和类之间的关键区别,我会说:类提供了更多的灵活性,而结构体提供了更多的安全性。作为一个基本规则,您应该始终使用结构,直到您有了使用类的特定原因。
本文来自Hacking with Swift 给 swift 初学者的入门课程 Swift for Complete Beginners 的 Classes